From 633e9b013bb290960e206ff3a1bea42fc4693c65 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 10 Jul 2024 18:25:38 +0400 Subject: [PATCH 01/14] Change version --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 2f50425e88..0fd74872f9 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "10.14.3", + "app": "10.14.2", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0" From 3768a2e358da9e868bcadc0fc7d8b44cbe92478a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 14 Jul 2024 20:26:25 +0400 Subject: [PATCH 02/14] Bump version --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 0fd74872f9..2f50425e88 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "10.14.2", + "app": "10.14.3", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0" From c4b406528a45c51f6a90619249e01d1446fa8e1d Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 30 Jul 2024 09:42:56 +0800 Subject: [PATCH 03/14] Fix chat preview hit test (cherry picked from commit 0cb2a8041671bd14bd9c6c5e440d2033a6210a60) --- submodules/TelegramUI/Sources/ChatControllerNode.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 079ddffb4c..4ca47e5657 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -3671,6 +3671,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let textNode = node as? TextAccessibilityOverlayNode { let _ = textNode return result + } else if let _ = node as? LinkHighlightingNode { + return result } } } From 76c07d07c25b22604eab5abcea721a8839d4a081 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 2 Aug 2024 11:31:56 +0800 Subject: [PATCH 04/14] Fix bot preview api usage (cherry picked from commit 32af470ffd00cc7f854964f66c9ad4f5a7a2a54c) --- .../Messages/StoryListContext.swift | 2 +- .../Peers/UpdateCachedPeerData.swift | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 296abd0c85..d92634a4df 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -2326,7 +2326,7 @@ public final class BotPreviewStoryListContext: StoryListContext { guard let peer, let inputUser = apiInputUser(peer) else { return .single((nil, nil)) } - return _internal_requestBotPreview(network: account.network, peerId: peerId, inputUser: inputUser, language: language) + return _internal_requestBotAdminPreview(network: account.network, peerId: peerId, inputUser: inputUser, language: language) |> map { botPreview in return (botPreview, peer) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 4f09b015a3..8f1c5fa8e2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -198,8 +198,12 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } let botPreview: Signal - if let user = maybePeer as? TelegramUser, let _ = user.botInfo { - botPreview = _internal_requestBotPreview(network: network, peerId: user.id, inputUser: inputUser, language: nil) + if let user = maybePeer as? TelegramUser, let botInfo = user.botInfo { + if botInfo.flags.contains(.canEdit) { + botPreview = _internal_requestBotAdminPreview(network: network, peerId: user.id, inputUser: inputUser, language: nil) + } else { + botPreview = _internal_requestBotUserPreview(network: network, peerId: user.id, inputUser: inputUser) + } } else { botPreview = .single(nil) } @@ -835,7 +839,7 @@ extension CachedPeerAutoremoveTimeout.Value { } } -func _internal_requestBotPreview(network: Network, peerId: PeerId, inputUser: Api.InputUser, language: String?) -> Signal { +func _internal_requestBotAdminPreview(network: Network, peerId: PeerId, inputUser: Api.InputUser, language: String?) -> Signal { return network.request(Api.functions.bots.getPreviewInfo(bot: inputUser, langCode: language ?? "")) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -864,3 +868,30 @@ func _internal_requestBotPreview(network: Network, peerId: PeerId, inputUser: Ap } } } + +func _internal_requestBotUserPreview(network: Network, peerId: PeerId, inputUser: Api.InputUser) -> Signal { + return network.request(Api.functions.bots.getPreviewMedias(bot: inputUser)) + |> map(Optional.init) + |> `catch` { _ -> Signal<[Api.BotPreviewMedia]?, NoError> in + return .single(nil) + } + |> map { result -> CachedUserData.BotPreview? in + guard let result else { + return nil + } + return CachedUserData.BotPreview( + items: result.compactMap { item -> CachedUserData.BotPreview.Item? in + switch item { + case let .botPreviewMedia(date, media): + let value = textMediaAndExpirationTimerFromApiMedia(media, peerId) + if let media = value.media { + return CachedUserData.BotPreview.Item(media: media, timestamp: date) + } else { + return nil + } + } + }, + alternativeLanguageCodes: [] + ) + } +} From 3e0694adce4d885a213a4f325c1ffc44bfca02cf Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 2 Aug 2024 21:15:24 +0800 Subject: [PATCH 05/14] Fix app search (cherry picked from commit a89adb664e0d55bfb8568d806b2bf19cf89dfada) --- .../Sources/ChatListSearchListPaneNode.swift | 120 +++++++++++++++++- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index f86f73e0a5..b94d22859a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -742,7 +742,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { if case .channels = key { headerType = .channels } else if case .apps = key { - headerType = .channels + //TODO:localize + headerType = .text("APPS", AnyHashable("apps")) } else { if filter.contains(.onlyGroups) { headerType = .chats @@ -762,8 +763,17 @@ public enum ChatListSearchEntry: Comparable, Identifiable { if case .savedMessagesChats = location { isSavedMessages = true } + + var status: ContactsPeerItemStatus = .none + if case let .user(user) = primaryPeer, let _ = user.botInfo { + if let subscriberCount = user.subscriberCount { + status = .custom(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil) + } else { + status = .custom(string: presentationData.strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil) + } + } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { contactPeer in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: status, badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { contactPeer in if case let .peer(maybePeer, maybeChatPeer) = contactPeer, let peer = maybePeer, let chatPeer = maybeChatPeer { interaction.peerSelected(chatPeer, peer, nil, nil) } else { @@ -1946,8 +1956,108 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } } else if let query, key == .apps { - let _ = query - foundLocalPeers = .single(([], [:], Set())) + foundLocalPeers = combineLatest( + context.engine.peers.recentApps(), + context.engine.peers.recommendedAppPeerIds() + ) + |> mapToSignal { local, recommended -> Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set), NoError> in + var peerIds: [EnginePeer.Id] = [] + + for peer in local { + if !peerIds.contains(peer) { + peerIds.append(peer) + } + } + if let recommended { + for id in recommended { + if !peerIds.contains(id) { + peerIds.append(id) + } + } + } + + return context.engine.data.subscribe( + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + ), + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in + return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) + } + ), + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in + return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) + } + ), + TelegramEngine.EngineData.Item.NotificationSettings.Global() + ) + |> map { peers, notificationSettings, unreadCounts, globalNotificationSettings -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in + var resultPeers: [EngineRenderedPeer] = [] + var unread: [EnginePeer.Id: (Int32, Bool)] = [:] + + let queryTokens = stringIndexTokens(query.lowercased(), transliteration: .combined) + + var matchingIds: [EnginePeer.Id] = [] + for peerId in local { + guard let maybePeer = peers[peerId], let peer = maybePeer else { + continue + } + if peer.indexName.matchesByTokens(queryTokens) { + if !matchingIds.contains(peerId) { + matchingIds.append(peerId) + } + } + } + + if let recommended { + for id in recommended { + guard let maybePeer = peers[id], let peer = maybePeer else { + continue + } + + if peer.indexName.matchesByTokens(queryTokens) { + if !matchingIds.contains(id) { + matchingIds.append(id) + } + } + } + } + + for id in matchingIds { + guard let maybePeer = peers[id], let peer = maybePeer else { + continue + } + resultPeers.append(EngineRenderedPeer(peer: peer)) + var isMuted = false + if let peerNotificationSettings = notificationSettings[peer.id] { + if case let .muted(until) = peerNotificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + isMuted = true + } else if case .default = peerNotificationSettings.muteState { + if case .user = peer { + isMuted = !globalNotificationSettings.privateChats.enabled + } else if case .legacyGroup = peer { + isMuted = !globalNotificationSettings.groupChats.enabled + } else if case let .channel(channel) = peer { + switch channel.info { + case .group: + isMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + isMuted = !globalNotificationSettings.channels.enabled + } + } + } + } + let unreadCount = unreadCounts[peer.id] + if let unreadCount = unreadCount, unreadCount > 0 { + unread[peer.id] = (Int32(unreadCount), isMuted) + } + } + return (peers: resultPeers, unread: unread, recentlySearchedPeerIds: Set()) + } + } } else { foundLocalPeers = .single((peers: [], unread: [:], recentlySearchedPeerIds: Set())) @@ -2029,6 +2139,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else if peersFilter.contains(.doNotSearchMessages) { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } else if key == .apps { + foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else { if !finalQuery.isEmpty { addAppLogEvent(postbox: context.account.postbox, type: "search_global_query") From 55e6e45324489918ae18b50c07ede4223a4665d1 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 2 Aug 2024 22:37:59 +0800 Subject: [PATCH 06/14] Update currencies.json (cherry picked from commit dc4c8cc10a0b26a8a2ab735e415fac31e9cf6f3b) --- submodules/TelegramUI/Resources/currencies.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Resources/currencies.json b/submodules/TelegramUI/Resources/currencies.json index a4ffa9567d..2b2c35ccc0 100644 --- a/submodules/TelegramUI/Resources/currencies.json +++ b/submodules/TelegramUI/Resources/currencies.json @@ -1447,5 +1447,14 @@ "symbolOnLeft": true, "spaceBetweenAmountAndSymbol": false, "decimalDigits": 2 - } + }, + "BYN": { + "code": "BYN", + "symbol": "BYN", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + } } From ccf5ebb1e9c5ade6d03a7c3f00686372eedba55e Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 2 Aug 2024 22:38:15 +0800 Subject: [PATCH 07/14] Fix pending bot uploads (cherry picked from commit 01a6551c92e0ed6966850c2a55f2e88e9b3f2c85) --- .../Messages/PendingStoryManager.swift | 2 +- .../TelegramEngine/Messages/Stories.swift | 21 ++++++++++++++++ .../Messages/StoryListContext.swift | 25 ++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index cf8b471f2c..f78a3a6aa6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -460,7 +460,7 @@ final class PendingStoryManager { }) } else { pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo) - |> deliverOn(self.queue)).start(next: { [weak self] event in + |> deliverOn(self.queue)).start(next: { [weak self] event in guard let `self` = self else { return } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 0ff890c31b..d26c411426 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1028,6 +1028,7 @@ private struct PendingStoryIdMappingKey: Hashable { } private let pendingStoryIdMapping = Atomic<[PendingStoryIdMappingKey: Int32]>(value: [:]) +private let pendingBotPreviewIdMapping = Atomic<[PendingStoryIdMappingKey: MediaId]>(value: [:]) func _internal_lookUpPendingStoryIdMapping(peerId: PeerId, stableId: Int32) -> Int32? { return pendingStoryIdMapping.with { dict in @@ -1045,6 +1046,22 @@ private func _internal_putPendingStoryIdMapping(peerId: PeerId, stableId: Int32, } } +func _internal_lookUpPendingBotPreviewIdMapping(peerId: PeerId, stableId: Int32) -> MediaId? { + return pendingBotPreviewIdMapping.with { dict in + return dict[PendingStoryIdMappingKey(peerId: peerId, stableId: stableId)] + } +} + +private func _internal_putPendingBotPreviewIdMapping(peerId: PeerId, stableId: Int32, id: MediaId) { + let _ = pendingBotPreviewIdMapping.modify { dict in + var dict = dict + + dict[PendingStoryIdMappingKey(peerId: peerId, stableId: stableId)] = id + + return dict + } +} + func _internal_uploadStoryImpl( postbox: Postbox, network: Network, @@ -1329,6 +1346,10 @@ func _internal_uploadBotPreviewImpl( let addedItem = CachedUserData.BotPreview.Item(media: resultMediaValue, timestamp: date) + if let mediaId = resultMediaValue.id { + _internal_putPendingBotPreviewIdMapping(peerId: toPeerId, stableId: stableId, id: mediaId) + } + if language == nil { transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in guard var current = current as? CachedUserData else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index d92634a4df..e22036f50a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -2196,6 +2196,13 @@ public final class BotPreviewStoryListContext: StoryListContext { self.nextId += 1 self.pendingIdMapping[item.stableId] = mappedId } + + if let mediaId = _internal_lookUpPendingBotPreviewIdMapping(peerId: self.peerId, stableId: item.stableId) { + if let botPreview, botPreview.items.contains(where: { $0.media.id == mediaId }) { + continue + } + } + if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == peerId, itemLanguage == language { items.append(State.Item( id: StoryId(peerId: peerId, id: mappedId), @@ -2551,8 +2558,24 @@ public final class BotPreviewStoryListContext: StoryListContext { } private func pushLanguageItems() { - var items = self.localItems + var items: [State.Item] = [] + for item in self.localItems { + var stableId: Int32? + inner: for (from, to) in self.pendingIdMapping { + if to == item.id.id { + stableId = from + break inner + } + } + if let stableId, let mediaId = _internal_lookUpPendingBotPreviewIdMapping(peerId: self.peerId, stableId: stableId) { + if self.remoteItems.contains(where: { $0.storyItem.media.id == mediaId }) { + continue + } + } + items.append(item) + } items.append(contentsOf: self.remoteItems) + self.stateValue = State( peerReference: self.stateValue.peerReference, items: items, From eff4ad5e780bc5a51c9ae4eed05e045ee069440c Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 2 Aug 2024 22:38:34 +0800 Subject: [PATCH 08/14] Don't delay message deletion when not necessary (cherry picked from commit ad4c5a181ba4b8739c83f3e5f47c2e0a8559c6a3) --- .../Sources/ChatControllerAdminBanUsers.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index 568e230870..fbad3ba82c 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -340,6 +340,20 @@ extension ChatControllerImpl { } func beginDeleteMessagesWithUndo(messageIds: Set, type: InteractiveMessagesDeletionType) { + var deleteImmediately = false + if case .forEveryone = type { + deleteImmediately = true + } else if case .scheduledMessages = self.presentationInterfaceState.subject { + deleteImmediately = true + } else if case .peer(self.context.account.peerId) = self.chatLocation { + deleteImmediately = true + } + + if deleteImmediately { + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: type).startStandalone() + return + } + self.chatDisplayNode.historyNode.ignoreMessageIds = Set(messageIds) let undoTitle = self.presentationData.strings.Chat_MessagesDeletedToast_Text(Int32(messageIds.count)) From 4a44c61c8d2a6f2df4b1e7b736184471898b940f Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 2 Aug 2024 22:48:32 +0800 Subject: [PATCH 09/14] Fix custom truncation token last line width (cherry picked from commit eb22c98e4f7d5ff9d38338d8f6eb9991a2e0e059) --- .../Sources/InteractiveTextComponent.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 677c047215..2868bce605 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -494,6 +494,9 @@ public final class InteractiveTextNodeLayout: NSObject { public var trailingLineWidth: CGFloat { if let lastSegment = self.segments.last, let lastLine = lastSegment.lines.last { var width = lastLine.frame.maxX + if let additionalTrailingLine = lastLine.additionalTrailingLine { + width += additionalTrailingLine.1 + } if let blockQuote = lastSegment.blockQuote { if lastLine.frame.intersects(blockQuote.frame) { @@ -1606,7 +1609,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn spoilerWords: [], embeddedItems: [], attachments: [], - additionalTrailingLine: (truncationToken, 0.0) + additionalTrailingLine: (truncationToken, truncationTokenWidth) ) } } From aff6322def909a6489813303678762040315b87b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 2 Aug 2024 23:29:14 +0200 Subject: [PATCH 10/14] Various fixes --- .../Sources/AccountContext.swift | 6 +- .../Sources/ChatController.swift | 4 +- .../AccountContext/Sources/Premium.swift | 2 +- .../Sources/AnimatedStickerNode.swift | 2 +- .../Sources/AttachmentController.swift | 2 +- submodules/BrowserUI/BUILD | 1 + .../Sources/BrowserAddressBarComponent.swift | 24 +- .../BrowserAddressListItemComponent.swift | 18 +- .../Sources/BrowserBookmarksScreen.swift | 2 +- ...owserExceptionDomainAlertContentNode.swift | 299 ++++++++++++++ .../Sources/BrowserInstantPageContent.swift | 61 ++- .../BrowserUI/Sources/BrowserScreen.swift | 48 ++- .../BrowserUI/Sources/BrowserWebContent.swift | 124 ++++-- submodules/BrowserUI/Sources/Utils.swift | 44 +++ submodules/Camera/BUILD | 1 + submodules/Camera/Sources/Camera.swift | 1 + submodules/Camera/Sources/CameraMetrics.swift | 368 +----------------- .../Sources/ChatListSearchListPaneNode.swift | 7 +- .../Sources/ChatTextLinkEditController.swift | 2 +- .../Navigation/MinimizedContainer.swift | 5 +- .../Navigation/NavigationController.swift | 12 +- .../Sources/DrawingLinkEntityView.swift | 2 +- .../Sources/DrawingTextEntityView.swift | 12 + .../Sources/InstantPageController.swift | 10 - .../InstantPageReferenceController.swift | 4 +- .../Sources/InstantPageTextItem.swift | 10 +- .../Sources/LegacyPaintStickersContext.swift | 6 +- .../TwoFactorAuthDataInputScreen.swift | 4 +- .../Sources/CachedFaqInstantPage.swift | 6 +- .../WebBrowserSettingsController.swift | 16 +- .../Chat/ChatRecentActionsController/BUILD | 1 + .../ChatRecentActionsControllerNode.swift | 11 +- .../Sources/EmojiTextAttachmentView.swift | 4 +- .../Drawing/CodableDrawingEntity.swift | 7 +- .../Sources/Drawing/DrawingTextEntity.swift | 12 +- .../Sources/CreateLinkScreen.swift | 11 +- .../Sources/MediaEditorScreen.swift | 5 +- .../Sources/MinimizedContainer.swift | 43 +- .../Sources/MinimizedHeaderNode.swift | 2 +- .../MinimizedContainer/Sources/Utils.swift | 5 +- .../Sources/MultiAnimationRenderer.swift | 3 + .../Sources/PeerInfoScreen.swift | 4 +- .../Sources/ShareWithPeersScreen.swift | 63 +-- .../Sources/StarsPurchaseScreen.swift | 20 +- .../Sources/StarsTransactionsScreen.swift | 2 +- .../Sources/StarsTransferScreen.swift | 2 +- .../StoryItemSetContainerComponent.swift | 4 +- .../Chat/ChatControllerOpenWebApp.swift | 274 ++++++------- .../TelegramUI/Sources/ChatController.swift | 20 +- .../TelegramUI/Sources/OpenChatMessage.swift | 20 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 19 +- submodules/TelegramUI/Sources/OpenUrl.swift | 27 +- .../Sources/SharedAccountContext.swift | 8 +- .../Sources/TelegramRootController.swift | 14 + .../TelegramUI/Sources/TextLinkHandling.swift | 7 +- .../Sources/MediaAutoDownloadSettings.swift | 10 + .../UrlEscaping/Sources/UrlEscaping.swift | 10 +- .../UrlHandling/Sources/UrlHandling.swift | 69 +++- submodules/Utils/DeviceModel/BUILD | 20 + .../DeviceModel/Sources/DeviceModel.swift | 368 ++++++++++++++++++ submodules/WebUI/BUILD | 1 + .../WebUI/Sources/WebAppController.swift | 36 +- 62 files changed, 1386 insertions(+), 819 deletions(-) create mode 100644 submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift create mode 100644 submodules/Utils/DeviceModel/BUILD create mode 100644 submodules/Utils/DeviceModel/Sources/DeviceModel.swift diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index e8a808b9a0..d7b3ecb81a 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -305,6 +305,7 @@ public enum ResolvedUrl { case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?) case invoice(slug: String, invoice: TelegramMediaInvoice?) case premiumOffer(reference: String?) + case starsTopup(amount: Int64?) case chatFolder(slug: String) case story(peerId: PeerId, id: Int32) case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) @@ -755,7 +756,7 @@ public class MediaEditorTransitionOutExternalState { } public protocol MediaEditorScreenResult { - + var target: Stories.PendingTarget { get } } public protocol TelegramRootControllerInterface: NavigationController { @@ -963,7 +964,8 @@ public protocol SharedAccountContext: AnyObject { func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) func openImagePicker(context: AccountContext, completion: @escaping (UIImage) -> Void, present: @escaping (ViewController) -> Void) func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) - func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) + func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? + func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 3840144291..75ffce2a03 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -292,12 +292,12 @@ public struct ChatControllerInitialAttachBotStart { } public struct ChatControllerInitialBotAppStart { - public let botApp: BotApp + public let botApp: BotApp? public let payload: String? public let justInstalled: Bool public let compact: Bool - public init(botApp: BotApp, payload: String?, justInstalled: Bool, compact: Bool) { + public init(botApp: BotApp?, payload: String?, justInstalled: Bool, compact: Bool) { self.botApp = botApp self.payload = payload self.justInstalled = justInstalled diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 52ee4ce8d4..183c5aeea7 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -123,7 +123,7 @@ public enum BoostSubject: Equatable { } public enum StarsPurchasePurpose: Equatable { - case generic + case generic(requiredStars: Int64?) case transfer(peerId: EnginePeer.Id, requiredStars: Int64) case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool) case gift(peerId: EnginePeer.Id) diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 7e3ccaeb2a..d9eb6947ce 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -653,7 +653,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke strongSelf.frameUpdated(frame.index, frame.totalFrames) strongSelf.currentFrameIndex = frame.index - strongSelf.currentFrameCount = frame.totalFrames; + strongSelf.currentFrameCount = frame.totalFrames strongSelf.currentFrameRate = frameRate if frame.isLastFrame { diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 9841bfd570..60646efeb7 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -713,7 +713,7 @@ public class AttachmentController: ViewController, MinimizableController { } if case .ended = recognizer.state { if let lastController = self.currentControllers.last { - if let controller = self.controller, controller.shouldMinimizeOnSwipe?(self.currentType) == true { + if let controller = self.controller, let layout = self.validLayout, !layout.metrics.isTablet, controller.shouldMinimizeOnSwipe?(self.currentType) == true { self.minimize() return } diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index b49526f0c2..9f7432956e 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -48,6 +48,7 @@ swift_library( "//submodules/SearchBarNode", "//submodules/TelegramUI/Components/SaveProgressScreen", "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/Utils/DeviceModel", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift index dcb1cd88c8..da83dddb91 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -281,15 +281,7 @@ final class AddressBarContentComponent: Component { } let isActive = self.textField?.isFirstResponder ?? false - var title: String = "" - if let parsedUrl = URL(string: component.url) { - title = parsedUrl.host ?? component.url - if title.hasPrefix("www.") { - title.removeSubrange(title.startIndex ..< title.index(title.startIndex, offsetBy: 4)) - } - title = title.idnaDecoded ?? title - } - + let title = getDisplayUrl(component.url, hostOnly: true) self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, isTablet: component.metrics.isTablet, transition: transition) return availableSize @@ -449,19 +441,7 @@ final class AddressBarContentComponent: Component { textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) } - var address = self.component?.url ?? "" - if let components = URLComponents(string: address) { - if #available(iOS 16.0, *), let encodedHost = components.encodedHost { - if let decodedHost = components.host, encodedHost != decodedHost { - address = address.replacingOccurrences(of: encodedHost, with: decodedHost) - } - } else if let encodedHost = components.host { - if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { - address = address.replacingOccurrences(of: encodedHost, with: decodedHost) - } - } - } - + let address = getDisplayUrl(self.component?.url ?? "", trim: false) if textField.text != address { textField.text = address self.clearIconView.isHidden = address.isEmpty diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift index aca503c26d..f41a5395ea 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -176,23 +176,7 @@ final class BrowserAddressListItemComponent: Component { if case let .Loaded(content) = component.webPage.content { title = content.title ?? content.url - var address = content.url - if let components = URLComponents(string: address) { - if #available(iOS 16.0, *), let encodedHost = components.encodedHost { - if let decodedHost = components.host, encodedHost != decodedHost { - address = address.replacingOccurrences(of: encodedHost, with: decodedHost) - } - } else if let encodedHost = components.host { - if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { - address = address.replacingOccurrences(of: encodedHost, with: decodedHost) - } - } - } - address = address.replacingOccurrences(of: "https://www.", with: "") - address = address.replacingOccurrences(of: "https://", with: "") - address = address.replacingOccurrences(of: "tonsite://", with: "") - address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - subtitle = address + subtitle = getDisplayUrl(content.url) parsedUrl = URL(string: content.url) diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index 64ec20912b..b77e831e36 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -367,7 +367,7 @@ public final class BrowserBookmarksScreen: ViewController { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) self.title = self.presentationData.strings.WebBrowser_Bookmarks_Title self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in diff --git a/submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift b/submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift new file mode 100644 index 0000000000..b3f347b7a4 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift @@ -0,0 +1,299 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import PhotoResources +import CheckNode +import Markdown + +private let textFont = Font.regular(13.0) +private let boldTextFont = Font.semibold(13.0) + +private func formattedText(_ text: String, color: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { + return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: color), linkAttribute: { _ in return nil}), textAlignment: textAlignment) +} + +private final class BrowserExceptionDomainAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let domain: String + + private let titleNode: ASTextNode + private let textNode: ASTextNode + + private let allowWriteCheckNode: InteractiveCheckNode + private let allowWriteLabelNode: ASTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + private var iconDisposable: Disposable? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + var allowWriteAccess: Bool = true { + didSet { + self.allowWriteCheckNode.setSelected(self.allowWriteAccess, animated: true) + } + } + + init(account: Account, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, domain: String, requestWriteAccess: Bool, actions: [TextAlertAction]) { + self.strings = strings + self.domain = domain + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 0 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.allowWriteCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) + self.allowWriteCheckNode.setSelected(true, animated: false) + self.allowWriteLabelNode = ASTextNode() + self.allowWriteLabelNode.maximumNumberOfLines = 4 + self.allowWriteLabelNode.isUserInteractionEnabled = true + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + if requestWriteAccess { + self.addSubnode(self.allowWriteCheckNode) + self.addSubnode(self.allowWriteLabelNode) + } + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.allowWriteCheckNode.valueChanged = { [weak self] value in + if let strongSelf = self { + strongSelf.allowWriteAccess = !strongSelf.allowWriteAccess + } + } + + self.updateTheme(theme) + } + + deinit { + self.iconDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.allowWriteLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.allowWriteTap(_:)))) + } + + @objc private func allowWriteTap(_ gestureRecognizer: UITapGestureRecognizer) { + if self.allowWriteCheckNode.isUserInteractionEnabled { + self.allowWriteAccess = !self.allowWriteAccess + } + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: "Open in Browser", font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: "Do you want to open this link in your default browser?", font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.allowWriteLabelNode.attributedText = formattedText("Always open links from **\(self.domain)** in browser", color: theme.primaryColor) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width , 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let titleSize = self.titleNode.measure(size) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 13.0 + + let textSize = self.textNode.measure(size) + var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize) + origin.y += textSize.height + + var entriesHeight: CGFloat = 0.0 + + if self.allowWriteLabelNode.supernode != nil { + origin.y += 16.0 + entriesHeight += 16.0 + + let checkSize = CGSize(width: 22.0, height: 22.0) + let condensedSize = CGSize(width: size.width - 76.0, height: size.height) + + let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize) + transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize)) + transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize)) + origin.y += allowWriteSize.height + entriesHeight += allowWriteSize.height + } + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var contentWidth = max(textSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 17.0 + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + textFrame.origin.x = floorToScreenPixels((resultSize.width - textFrame.width) / 2.0) + transition.updateFrame(node: self.textNode, frame: textFrame) + + return resultSize + } +} + +public func browserExceptionDomainAlertController(context: AccountContext, domain: String, completion: @escaping (Bool) -> Void) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let theme = presentationData.theme + let strings = presentationData.strings + + var dismissImpl: ((Bool) -> Void)? + var getContentNodeImpl: (() -> BrowserExceptionDomainAlertContentNode?)? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: "Continue", action: { + if let allowWriteAccess = getContentNodeImpl?()?.allowWriteAccess { + completion(allowWriteAccess) + } else { + completion(false) + } + dismissImpl?(true) + })] + + let contentNode = BrowserExceptionDomainAlertContentNode(account: context.account, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, domain: domain, requestWriteAccess: true, actions: actions) + getContentNodeImpl = { [weak contentNode] in + return contentNode + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 8661daac51..f334322e60 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -414,8 +414,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg if let state = self.initialState { didSetScrollOffset = true contentOffset = CGPoint(x: 0.0, y: CGFloat(state.contentOffset)) - } - else if let anchor = self.initialAnchor, !anchor.isEmpty { + } else if let anchor = self.initialAnchor, !anchor.isEmpty { self.initialAnchor = nil if let items = self.currentLayout?.items { didSetScrollOffset = true @@ -1312,35 +1311,35 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func presentReferenceView(item: InstantPageTextItem, referenceAnchor: String) { -// guard let theme = self.theme, let webPage = self.webPage else { -// return -// } -// -// var targetAnchor: InstantPageTextAnchorItem? -// for (name, (line, _)) in item.anchors { -// if name == referenceAnchor { -// let anchors = item.lines[line].anchorItems -// for anchor in anchors { -// if anchor.name == referenceAnchor { -// targetAnchor = anchor -// break -// } -// } -// } -// } -// -// guard let anchorText = targetAnchor?.anchorText else { -// return -// } -// -// let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in -// self?.openUrl(url) -// }, openUrlIn: { [weak self] url in -// self?.openUrlIn(url) -// }, present: { [weak self] c, a in -// self?.present(c, a) -// }) -// self.present(controller, nil) + guard let webPage = self.webPage else { + return + } + + var targetAnchor: InstantPageTextAnchorItem? + for (name, (line, _)) in item.anchors { + if name == referenceAnchor { + let anchors = item.lines[line].anchorItems + for anchor in anchors { + if anchor.name == referenceAnchor { + targetAnchor = anchor + break + } + } + } + } + + guard let anchorText = targetAnchor?.anchorText else { + return + } + + let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in + self?.openUrl(url) + }, openUrlIn: { [weak self] url in + self?.openUrlIn(url) + }, present: { [weak self] c, a in + self?.present(c, a) + }) + self.present(controller, nil) } private func scrollToAnchor(_ anchor: String) { diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index c9ce6c8ed8..70fd9ebdc3 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -18,6 +18,7 @@ import MinimizedContainer import InstantPageUI import NavigationStackComponent import LottieComponent +import WebKit private let settingsTag = GenericComponentViewTag() @@ -384,13 +385,13 @@ private final class BrowserScreenComponent: CombinedComponent { bottomInset: toolbarBottomInset, sideInset: environment.safeInsets.left, item: toolbarContent, - collapseFraction: collapseFraction + collapseFraction: 0.0 ), availableSize: context.availableSize, transition: context.transition ) context.add(toolbar - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0 + toolbar.size.height * collapseFraction)) .appear(ComponentTransition.Appear { _, view, transition in transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: view.frame.height), to: CGPoint(), additive: true) }) @@ -489,13 +490,13 @@ public class BrowserScreen: ViewController, MinimizableController { case expand } - fileprivate final class Node: ViewControllerTracingNode { + final class Node: ViewControllerTracingNode { private weak var controller: BrowserScreen? private let context: AccountContext private let contentContainerView = UIView() fileprivate let contentNavigationContainer = ComponentView() - fileprivate var content: [BrowserContent] = [] + private(set) var content: [BrowserContent] = [] fileprivate var contentState: BrowserContentState? private var contentStateDisposable = MetaDisposable() @@ -785,13 +786,14 @@ public class BrowserScreen: ViewController, MinimizableController { let browserContent: BrowserContent switch content { case let .webPage(url): - let webContent = BrowserWebContent(context: self.context, presentationData: self.presentationData, url: url) + let webContent = BrowserWebContent(context: self.context, presentationData: self.presentationData, url: url, preferredConfiguration: self.controller?.preferredConfiguration) webContent.cancelInteractiveTransitionGestures = { [weak self] in if let self, let view = self.controller?.view { cancelInteractiveTransitionGestures(view: view) } } browserContent = webContent + self.controller?.preferredConfiguration = nil case let .instantPage(webPage, anchor, sourceLocation): let instantPageContent = BrowserInstantPageContent(context: self.context, presentationData: self.presentationData, webPage: webPage, anchor: anchor, url: webPage.content.url ?? "", sourceLocation: sourceLocation) instantPageContent.openPeer = { [weak self] peer in @@ -846,7 +848,9 @@ public class BrowserScreen: ViewController, MinimizableController { return } if controller.isMinimized { - + if let navigationController = controller.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { + minimizedContainer.removeController(controller) + } } else { controller.dismiss() } @@ -1328,7 +1332,8 @@ public class BrowserScreen: ViewController, MinimizableController { right: layout.safeInsets.right ), navigationBarHeight: navigationBarHeight, - scrollingPanelOffsetFraction: self.scrollingPanelOffsetFraction + scrollingPanelOffsetFraction: self.scrollingPanelOffsetFraction, + hasBottomPanel: !layout.metrics.isTablet || self.presentationState.isSearching ) )) ) @@ -1372,7 +1377,7 @@ public class BrowserScreen: ViewController, MinimizableController { private let context: AccountContext private let subject: Subject - + private var preferredConfiguration: WKWebViewConfiguration? private var openPreviousOnClose = false private var validLayout: ContainerViewLayout? @@ -1389,9 +1394,20 @@ public class BrowserScreen: ViewController, MinimizableController { // "application/vnd.openxmlformats-officedocument.presentationml.presentation" ] - public init(context: AccountContext, subject: Subject, openPreviousOnClose: Bool = false) { + public init(context: AccountContext, subject: Subject, preferredConfiguration: WKWebViewConfiguration? = nil, openPreviousOnClose: Bool = false) { + var subject = subject + if case let .webPage(url) = subject, let parsedUrl = URL(string: url) { + if parsedUrl.host?.hasSuffix(".ton") == true { + var urlComponents = URLComponents(string: url) + urlComponents?.scheme = "tonsite" + if let updatedUrl = urlComponents?.url?.absoluteString { + subject = .webPage(url: updatedUrl) + } + } + } self.context = context self.subject = subject + self.preferredConfiguration = preferredConfiguration self.openPreviousOnClose = openPreviousOnClose super.init(navigationBarPresentationData: nil) @@ -1409,7 +1425,7 @@ public class BrowserScreen: ViewController, MinimizableController { preconditionFailure() } - private var node: Node { + var node: Node { return self.displayNode as! Node } @@ -1529,17 +1545,20 @@ private final class BrowserContentComponent: Component { let insets: UIEdgeInsets let navigationBarHeight: CGFloat let scrollingPanelOffsetFraction: CGFloat + let hasBottomPanel: Bool init( content: BrowserContent, insets: UIEdgeInsets, navigationBarHeight: CGFloat, - scrollingPanelOffsetFraction: CGFloat + scrollingPanelOffsetFraction: CGFloat, + hasBottomPanel: Bool ) { self.content = content self.insets = insets self.navigationBarHeight = navigationBarHeight self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction + self.hasBottomPanel = hasBottomPanel } static func ==(lhs: BrowserContentComponent, rhs: BrowserContentComponent) -> Bool { @@ -1555,6 +1574,9 @@ private final class BrowserContentComponent: Component { if lhs.scrollingPanelOffsetFraction != rhs.scrollingPanelOffsetFraction { return false } + if lhs.hasBottomPanel != rhs.hasBottomPanel { + return false + } return true } @@ -1574,9 +1596,9 @@ private final class BrowserContentComponent: Component { let collapsedHeight: CGFloat = 24.0 let topInset: CGFloat = component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + (component.insets.top + collapsedHeight) * component.scrollingPanelOffsetFraction - let bottomInset = (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction) + let bottomInset = component.hasBottomPanel ? (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction) : 0.0 let insets = UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right) - let fullInsets = UIEdgeInsets(top: component.insets.top + component.navigationBarHeight, left: component.insets.left, bottom: 49.0 + component.insets.bottom, right: component.insets.right) + let fullInsets = UIEdgeInsets(top: component.insets.top + component.navigationBarHeight, left: component.insets.left, bottom: component.hasBottomPanel ? 49.0 + component.insets.bottom : 0.0, right: component.insets.right) component.content.updateLayout(size: availableSize, insets: insets, fullInsets: fullInsets, safeInsets: component.insets, transition: transition) transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize)) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 0c0f883e0d..de137be650 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -20,6 +20,7 @@ import MultilineTextComponent import UrlEscaping import UrlHandling import SaveProgressScreen +import DeviceModel private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -151,6 +152,22 @@ private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { } } +private func computedUserAgent() -> String { + func getFirmwareVersion() -> String? { + var size = 0 + sysctlbyname("kern.osversion", nil, &size, nil, 0) + + var str = [CChar](repeating: 0, count: size) + sysctlbyname("kern.osversion", &str, &size, nil, 0) + + return String(cString: str) + } + + let osVersion = UIDevice.current.systemVersion + let firmwareVersion = getFirmwareVersion() ?? "15E148" + return DeviceModel.current.isIpad ? "Version/\(osVersion) Safari/605.1.15" : "Version/\(osVersion) Mobile/\(firmwareVersion) Safari/604.1" +} + final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData @@ -186,42 +203,49 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU private var tempFile: TempBoxFile? - init(context: AccountContext, presentationData: PresentationData, url: String) { + init(context: AccountContext, presentationData: PresentationData, url: String, preferredConfiguration: WKWebViewConfiguration? = nil) { self.context = context self.uuid = UUID() self.presentationData = presentationData - let configuration = WKWebViewConfiguration() - - var proxyServerHost = "magic.org" - if let data = context.currentAppConfiguration.with({ $0 }).data, let hostValue = data["ton_proxy_address"] as? String { - proxyServerHost = hostValue - } - configuration.setURLSchemeHandler(TonSchemeHandler(proxyServerHost: proxyServerHost), forURLScheme: "tonsite") - configuration.allowsInlineMediaPlayback = true - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - configuration.mediaTypesRequiringUserActionForPlayback = [] - } else { - configuration.mediaPlaybackRequiresUserAction = false - } - - let contentController = WKUserContentController() - let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false) - contentController.addUserScript(videoScript) - let touchScript = WKUserScript(source: setupTouchObservers, injectionTime: .atDocumentStart, forMainFrameOnly: false) - contentController.addUserScript(touchScript) - configuration.userContentController = contentController - var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? - let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: false) - contentController.addUserScript(eventProxyScript) - contentController.add(WeakScriptMessageHandler { message in - handleScriptMessageImpl?(message) - }, name: "performAction") + let configuration: WKWebViewConfiguration + if let preferredConfiguration { + configuration = preferredConfiguration + } else { + configuration = WKWebViewConfiguration() + var proxyServerHost = "magic.org" + if let data = context.currentAppConfiguration.with({ $0 }).data, let hostValue = data["ton_proxy_address"] as? String { + proxyServerHost = hostValue + } + configuration.setURLSchemeHandler(TonSchemeHandler(proxyServerHost: proxyServerHost), forURLScheme: "tonsite") + configuration.allowsInlineMediaPlayback = true + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + configuration.mediaTypesRequiringUserActionForPlayback = [] + } else { + configuration.mediaPlaybackRequiresUserAction = false + } + + let contentController = WKUserContentController() + let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(videoScript) + let touchScript = WKUserScript(source: setupTouchObservers, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(touchScript) + + let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(eventProxyScript) + contentController.add(WeakScriptMessageHandler { message in + handleScriptMessageImpl?(message) + }, name: "performAction") + + configuration.userContentController = contentController + configuration.applicationNameForUserAgent = computedUserAgent() + } + self.webView = WebView(frame: CGRect(), configuration: configuration) self.webView.allowsLinkPreview = true - + if #available(iOS 11.0, *) { self.webView.scrollView.contentInsetAdjustmentBehavior = .never } @@ -239,18 +263,19 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU let request = URLRequest(url: parsedUrl) self.webView.load(request) - title = parsedUrl.host ?? "" + title = getDisplayUrl(url, hostOnly: true) } self.errorView = ComponentHostView() - self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .webPage) + self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.1, readingProgress: 0.0, contentType: .webPage) self.statePromise = Promise(self._state) super.init(frame: .zero) self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.isOpaque = false self.webView.allowsBackForwardNavigationGestures = true self.webView.scrollView.delegate = self @@ -528,7 +553,14 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating) - let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top)) + let currentBounds = self.webView.scrollView.bounds + let offsetToBottomEdge = max(0.0, self.webView.scrollView.contentSize.height - currentBounds.maxY) + var bottomInset = insets.bottom + if offsetToBottomEdge < 128.0 { + bottomInset = fullInsets.bottom + } + + let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - bottomInset)) var refresh = false if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width { refresh = true @@ -539,11 +571,10 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.reloadInputViews() } - self.webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: fullInsets.bottom, right: 0.0) - self.webView.customBottomInset = max(insets.bottom, safeInsets.bottom) + self.webView.customBottomInset = safeInsets.bottom * (1.0 - insets.bottom / fullInsets.bottom) - self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) - self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) +// self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) +// self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) if let error = self.currentError { let errorSize = self.errorView.update( @@ -681,7 +712,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU // }) // } else { if let url = navigationAction.request.url?.absoluteString { - if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url)) { + if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url)) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") { decisionHandler(.cancel, preferences) self.minimize() self.openAppUrl(url) @@ -725,7 +756,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { - self.currentError = nil + if let _ = self.currentError { + self.currentError = nil + if let (size, insets, fullInsets, safeInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: .immediate) + } + } self.updateFontState(self.currentFontState, force: true) } @@ -739,7 +775,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - if [-1003, -1100].contains((error as NSError).code) { + if [-1003, -1100, 102].contains((error as NSError).code) { self.currentError = error } else { self.currentError = nil @@ -756,7 +792,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.minimize() self.openAppUrl(url) } else { - self.open(url: url, new: true) + return self.open(url: url, configuration: configuration, new: true) } } } @@ -764,7 +800,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } func webViewDidClose(_ webView: WKWebView) { - self.close() + Queue.mainQueue().after(0.5, { + self.close() + }) } @available(iOS 15.0, *) @@ -895,17 +933,19 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.present(alertController, nil) } - private func open(url: String, new: Bool) { + @discardableResult private func open(url: String, configuration: WKWebViewConfiguration? = nil, new: Bool) -> WKWebView? { let subject: BrowserScreen.Subject = .webPage(url: url) if new, let navigationController = self.getNavigationController() { navigationController._keepModalDismissProgress = true self.minimize() - let controller = BrowserScreen(context: self.context, subject: subject, openPreviousOnClose: true) + let controller = BrowserScreen(context: self.context, subject: subject, preferredConfiguration: configuration, openPreviousOnClose: true) navigationController._keepModalDismissProgress = true navigationController.pushViewController(controller) + return (controller.node.content.last as? BrowserWebContent)?.webView } else { self.pushContent(subject) } + return nil } private func share(url: String) { diff --git a/submodules/BrowserUI/Sources/Utils.swift b/submodules/BrowserUI/Sources/Utils.swift index 7caff6dd87..71489f05a3 100644 --- a/submodules/BrowserUI/Sources/Utils.swift +++ b/submodules/BrowserUI/Sources/Utils.swift @@ -108,3 +108,47 @@ func getPrimaryUrl(message: Message) -> String? { } return primaryUrl } + +private let asciiChars = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) + +func getDisplayUrl(_ url: String, hostOnly: Bool = false, trim: Bool = true) -> String { + if hostOnly { + var title = url + if let parsedUrl = URL(string: url) { + title = parsedUrl.host ?? url + if title.hasPrefix("www.") { + title.removeSubrange(title.startIndex ..< title.index(title.startIndex, offsetBy: 4)) + } + if let decoded = title.idnaDecoded, title != decoded { + if decoded.lowercased().rangeOfCharacter(from: asciiChars) == nil { + title = decoded + } + } + } + return title + } else { + var address = url + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + if decodedHost.lowercased().rangeOfCharacter(from: asciiChars) == nil { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + if decodedHost.lowercased().rangeOfCharacter(from: asciiChars) == nil { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + } + if trim { + address = address.replacingOccurrences(of: "https://www.", with: "") + address = address.replacingOccurrences(of: "https://", with: "") + address = address.replacingOccurrences(of: "tonsite://", with: "") + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + } + return address + } +} diff --git a/submodules/Camera/BUILD b/submodules/Camera/BUILD index 07377c40c1..a5150befbb 100644 --- a/submodules/Camera/BUILD +++ b/submodules/Camera/BUILD @@ -58,6 +58,7 @@ swift_library( "//submodules/Display:Display", "//submodules/ImageBlur:ImageBlur", "//submodules/TelegramCore:TelegramCore", + "//submodules/Utils/DeviceModel", ], visibility = [ "//visibility:public", diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 922e9af872..ca84dca958 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -4,6 +4,7 @@ import SwiftSignalKit import AVFoundation import CoreImage import TelegramCore +import DeviceModel final class CameraSession { private let singleSession: AVCaptureSession? diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index c1c8a3e429..4d5c684da8 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -1,4 +1,5 @@ import Foundation +import DeviceModel public extension Camera { enum Metrics { @@ -56,370 +57,3 @@ public extension Camera { } } } - -enum DeviceModel: CaseIterable, Equatable { - static var allCases: [DeviceModel] { - return [ - .iPodTouch1, - .iPodTouch2, - .iPodTouch3, - .iPodTouch4, - .iPodTouch5, - .iPodTouch6, - .iPodTouch7, - .iPhone, - .iPhone3G, - .iPhone3GS, - .iPhone4, - .iPhone4S, - .iPhone5, - .iPhone5C, - .iPhone5S, - .iPhone6, - .iPhone6Plus, - .iPhone6S, - .iPhone6SPlus, - .iPhoneSE, - .iPhone7, - .iPhone7Plus, - .iPhone8, - .iPhone8Plus, - .iPhoneX, - .iPhoneXS, - .iPhoneXR, - .iPhone11, - .iPhone11Pro, - .iPhone11ProMax, - .iPhone12, - .iPhone12Mini, - .iPhone12Pro, - .iPhone12ProMax, - .iPhone13, - .iPhone13Mini, - .iPhone13Pro, - .iPhone13ProMax, - .iPhone14, - .iPhone14Plus, - .iPhone14Pro, - .iPhone14ProMax, - .iPhone15, - .iPhone15Plus, - .iPhone15Pro, - .iPhone15ProMax - ] - } - - case iPodTouch1 - case iPodTouch2 - case iPodTouch3 - case iPodTouch4 - case iPodTouch5 - case iPodTouch6 - case iPodTouch7 - - case iPhone - case iPhone3G - case iPhone3GS - - case iPhone4 - case iPhone4S - - case iPhone5 - case iPhone5C - case iPhone5S - - case iPhone6 - case iPhone6Plus - case iPhone6S - case iPhone6SPlus - - case iPhoneSE - - case iPhone7 - case iPhone7Plus - case iPhone8 - case iPhone8Plus - - case iPhoneX - case iPhoneXS - case iPhoneXSMax - case iPhoneXR - - case iPhone11 - case iPhone11Pro - case iPhone11ProMax - - case iPhoneSE2ndGen - - case iPhone12 - case iPhone12Mini - case iPhone12Pro - case iPhone12ProMax - - case iPhone13 - case iPhone13Mini - case iPhone13Pro - case iPhone13ProMax - - case iPhoneSE3rdGen - - case iPhone14 - case iPhone14Plus - case iPhone14Pro - case iPhone14ProMax - - case iPhone15 - case iPhone15Plus - case iPhone15Pro - case iPhone15ProMax - - case unknown(String) - - var modelId: [String] { - switch self { - case .iPodTouch1: - return ["iPod1,1"] - case .iPodTouch2: - return ["iPod2,1"] - case .iPodTouch3: - return ["iPod3,1"] - case .iPodTouch4: - return ["iPod4,1"] - case .iPodTouch5: - return ["iPod5,1"] - case .iPodTouch6: - return ["iPod7,1"] - case .iPodTouch7: - return ["iPod9,1"] - case .iPhone: - return ["iPhone1,1"] - case .iPhone3G: - return ["iPhone1,2"] - case .iPhone3GS: - return ["iPhone2,1"] - case .iPhone4: - return ["iPhone3,1", "iPhone3,2", "iPhone3,3"] - case .iPhone4S: - return ["iPhone4,1", "iPhone4,2", "iPhone4,3"] - case .iPhone5: - return ["iPhone5,1", "iPhone5,2"] - case .iPhone5C: - return ["iPhone5,3", "iPhone5,4"] - case .iPhone5S: - return ["iPhone6,1", "iPhone6,2"] - case .iPhone6: - return ["iPhone7,2"] - case .iPhone6Plus: - return ["iPhone7,1"] - case .iPhone6S: - return ["iPhone8,1"] - case .iPhone6SPlus: - return ["iPhone8,2"] - case .iPhoneSE: - return ["iPhone8,4"] - case .iPhone7: - return ["iPhone9,1", "iPhone9,3"] - case .iPhone7Plus: - return ["iPhone9,2", "iPhone9,4"] - case .iPhone8: - return ["iPhone10,1", "iPhone10,4"] - case .iPhone8Plus: - return ["iPhone10,2", "iPhone10,5"] - case .iPhoneX: - return ["iPhone10,3", "iPhone10,6"] - case .iPhoneXS: - return ["iPhone11,2"] - case .iPhoneXSMax: - return ["iPhone11,4", "iPhone11,6"] - case .iPhoneXR: - return ["iPhone11,8"] - case .iPhone11: - return ["iPhone12,1"] - case .iPhone11Pro: - return ["iPhone12,3"] - case .iPhone11ProMax: - return ["iPhone12,5"] - case .iPhoneSE2ndGen: - return ["iPhone12,8"] - case .iPhone12: - return ["iPhone13,2"] - case .iPhone12Mini: - return ["iPhone13,1"] - case .iPhone12Pro: - return ["iPhone13,3"] - case .iPhone12ProMax: - return ["iPhone13,4"] - case .iPhone13: - return ["iPhone14,5"] - case .iPhone13Mini: - return ["iPhone14,4"] - case .iPhone13Pro: - return ["iPhone14,2"] - case .iPhone13ProMax: - return ["iPhone14,3"] - case .iPhoneSE3rdGen: - return ["iPhone14,6"] - case .iPhone14: - return ["iPhone14,7"] - case .iPhone14Plus: - return ["iPhone14,8"] - case .iPhone14Pro: - return ["iPhone15,2"] - case .iPhone14ProMax: - return ["iPhone15,3"] - case .iPhone15: - return ["iPhone15,4"] - case .iPhone15Plus: - return ["iPhone15,5"] - case .iPhone15Pro: - return ["iPhone16,1"] - case .iPhone15ProMax: - return ["iPhone16,2"] - case let .unknown(modelId): - return [modelId] - } - } - - var modelName: String { - switch self { - case .iPodTouch1: - return "iPod touch 1G" - case .iPodTouch2: - return "iPod touch 2G" - case .iPodTouch3: - return "iPod touch 3G" - case .iPodTouch4: - return "iPod touch 4G" - case .iPodTouch5: - return "iPod touch 5G" - case .iPodTouch6: - return "iPod touch 6G" - case .iPodTouch7: - return "iPod touch 7G" - case .iPhone: - return "iPhone" - case .iPhone3G: - return "iPhone 3G" - case .iPhone3GS: - return "iPhone 3GS" - case .iPhone4: - return "iPhone 4" - case .iPhone4S: - return "iPhone 4S" - case .iPhone5: - return "iPhone 5" - case .iPhone5C: - return "iPhone 5C" - case .iPhone5S: - return "iPhone 5S" - case .iPhone6: - return "iPhone 6" - case .iPhone6Plus: - return "iPhone 6 Plus" - case .iPhone6S: - return "iPhone 6S" - case .iPhone6SPlus: - return "iPhone 6S Plus" - case .iPhoneSE: - return "iPhone SE" - case .iPhone7: - return "iPhone 7" - case .iPhone7Plus: - return "iPhone 7 Plus" - case .iPhone8: - return "iPhone 8" - case .iPhone8Plus: - return "iPhone 8 Plus" - case .iPhoneX: - return "iPhone X" - case .iPhoneXS: - return "iPhone XS" - case .iPhoneXSMax: - return "iPhone XS Max" - case .iPhoneXR: - return "iPhone XR" - case .iPhone11: - return "iPhone 11" - case .iPhone11Pro: - return "iPhone 11 Pro" - case .iPhone11ProMax: - return "iPhone 11 Pro Max" - case .iPhoneSE2ndGen: - return "iPhone SE (2nd gen)" - case .iPhone12: - return "iPhone 12" - case .iPhone12Mini: - return "iPhone 12 mini" - case .iPhone12Pro: - return "iPhone 12 Pro" - case .iPhone12ProMax: - return "iPhone 12 Pro Max" - case .iPhone13: - return "iPhone 13" - case .iPhone13Mini: - return "iPhone 13 mini" - case .iPhone13Pro: - return "iPhone 13 Pro" - case .iPhone13ProMax: - return "iPhone 13 Pro Max" - case .iPhoneSE3rdGen: - return "iPhone SE (3rd gen)" - case .iPhone14: - return "iPhone 14" - case .iPhone14Plus: - return "iPhone 14 Plus" - case .iPhone14Pro: - return "iPhone 14 Pro" - case .iPhone14ProMax: - return "iPhone 14 Pro Max" - case .iPhone15: - return "iPhone 15" - case .iPhone15Plus: - return "iPhone 15 Plus" - case .iPhone15Pro: - return "iPhone 15 Pro" - case .iPhone15ProMax: - return "iPhone 15 Pro Max" - case let .unknown(modelId): - if modelId.hasPrefix("iPhone") { - return "Unknown iPhone" - } else if modelId.hasPrefix("iPod") { - return "Unknown iPod" - } else if modelId.hasPrefix("iPad") { - return "Unknown iPad" - } else { - return "Unknown Device" - } - } - } - - var isIpad: Bool { - return self.modelId.first?.hasPrefix("iPad") ?? false - } - - static let current = DeviceModel() - - private init() { - var systemInfo = utsname() - uname(&systemInfo) - let modelCode = withUnsafePointer(to: &systemInfo.machine) { - $0.withMemoryRebound(to: CChar.self, capacity: 1) { - ptr in String.init(validatingUTF8: ptr) - } - } - var result: DeviceModel? - if let modelCode { - for model in DeviceModel.allCases { - if model.modelId.contains(modelCode) { - result = model - break - } - } - } - if let result { - self = result - } else { - self = .unknown(modelCode ?? "") - } - } -} diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index b94d22859a..31d76ca9f3 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2908,9 +2908,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openUrl: { url, _, _, message in interaction.openUrl(url) }, openInstantPage: { [weak self] message, data in - if let (webpage, anchor) = instantPageAndAnchor(message: message) { - let pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: .channel), anchor: anchor) - self?.navigationController?.pushViewController(pageController) + if let self, let navigationController = self.navigationController { + if let controller = self.context.sharedContext.makeInstantPageController(context: self.context, message: message, sourcePeerType: .channel) { + navigationController.pushViewController(controller) + } } }, longTap: { action, message in }, getHiddenMedia: { diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift index 8ed99a1fb8..c802ca3928 100644 --- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift +++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift @@ -433,7 +433,7 @@ public func chatTextLinkEditController(sharedContext: SharedAccountContext, upda return } let updatedLink = explicitUrl(contentNode.link) - if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false]) { + if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) { dismissImpl?(true) apply(updatedLink) } else if allowEmpty && contentNode.link.isEmpty { diff --git a/submodules/Display/Source/Navigation/MinimizedContainer.swift b/submodules/Display/Source/Navigation/MinimizedContainer.swift index 58201ab9bd..129a9e7b1e 100644 --- a/submodules/Display/Source/Navigation/MinimizedContainer.swift +++ b/submodules/Display/Source/Navigation/MinimizedContainer.swift @@ -6,12 +6,15 @@ public protocol MinimizedContainer: ASDisplayNode { var controllers: [MinimizableController] { get } var isExpanded: Bool { get } - var willMaximize: (() -> Void)? { get set } + var willMaximize: ((MinimizedContainer) -> Void)? { get set } + var willDismiss: ((MinimizedContainer) -> Void)? { get set } + var didDismiss: ((MinimizedContainer) -> Void)? { get set } var statusBarStyle: StatusBarStyle { get } var statusBarStyleUpdated: (() -> Void)? { get set } func addController(_ viewController: MinimizableController, topEdgeOffset: CGFloat?, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition) + func removeController(_ viewController: MinimizableController) func maximizeController(_ viewController: MinimizableController, animated: Bool, completion: @escaping (Bool) -> Void) func collapse() func dismissAll(completion: @escaping () -> Void) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index dd7c033a3a..60c371dd67 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -153,13 +153,23 @@ open class NavigationController: UINavigationController, ContainableController, open var minimizedContainer: MinimizedContainer? { didSet { self.minimizedContainer?.navigationController = self - self.minimizedContainer?.willMaximize = { [weak self] in + self.minimizedContainer?.willMaximize = { [weak self] _ in guard let self else { return } self.isMaximizing = true self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) } + self.minimizedContainer?.willDismiss = { [weak self] _ in + guard let self else { + return + } + self.minimizedContainer = nil + self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) + } + self.minimizedContainer?.didDismiss = { minimizedContainer in + minimizedContainer.removeFromSupernode() + } self.minimizedContainer?.statusBarStyleUpdated = { [weak self] in guard let self else { return diff --git a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift index 22d81cd41d..19eeba7632 100644 --- a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift @@ -209,7 +209,7 @@ public final class DrawingLinkEntityView: DrawingEntityView, UITextViewDelegate if !self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { string = self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() } else { - string = self.linkEntity.url.uppercased() + string = self.linkEntity.url.uppercased().replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "") } let text = NSMutableAttributedString(string: string) let range = NSMakeRange(0, text.length) diff --git a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift index 4e9b833497..ce68e000e1 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift @@ -624,6 +624,15 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } func getRenderSubEntities() -> [DrawingEntity] { + var explicitlyStaticStickers = Set() + if let customEmojiContainerView = self.customEmojiContainerView { + for (key, view) in customEmojiContainerView.emojiLayers { + if let view = view as? EmojiTextAttachmentView, let numFrames = view.contentLayer.numFrames, numFrames == 1 { + explicitlyStaticStickers.insert(key.id) + } + } + } + let textSize = self.textView.bounds.size let textPosition = self.textEntity.position let scale = self.textEntity.scale @@ -638,6 +647,9 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0) let entity = DrawingStickerEntity(content: .file(.standalone(media: file), .sticker)) + if explicitlyStaticStickers.contains(file.fileId.id) { + entity.isExplicitlyStatic = true + } entity.referenceDrawingSize = CGSize(width: itemSize * 4.0, height: itemSize * 4.0) entity.scale = scale entity.position = textPosition.offsetBy( diff --git a/submodules/InstantPageUI/Sources/InstantPageController.swift b/submodules/InstantPageUI/Sources/InstantPageController.swift index a17188bf80..716dc6a464 100644 --- a/submodules/InstantPageUI/Sources/InstantPageController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageController.swift @@ -8,16 +8,6 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext -public struct InstantPageSourceLocation { - public var userLocation: MediaResourceUserLocation - public var peerType: MediaAutoDownloadPeerType - - public init(userLocation: MediaResourceUserLocation, peerType: MediaAutoDownloadPeerType) { - self.userLocation = userLocation - self.peerType = peerType - } -} - public func instantPageAndAnchor(message: Message) -> (TelegramMediaWebpage, String?)? { for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift index e08a437250..318ecae4ef 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift @@ -7,7 +7,7 @@ import SwiftSignalKit import AccountContext import TelegramUIPreferences -final class InstantPageReferenceController: ViewController { +public final class InstantPageReferenceController: ViewController { private var controllerNode: InstantPageReferenceControllerNode { return self.displayNode as! InstantPageReferenceControllerNode } @@ -23,7 +23,7 @@ final class InstantPageReferenceController: ViewController { private let openUrlIn: (InstantPageUrlItem) -> Void private let present: (ViewController, Any?) -> Void - init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { + public init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.sourceLocation = sourceLocation self.theme = theme diff --git a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift index d0865f7fdd..af427754de 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift @@ -38,10 +38,10 @@ struct InstantPageTextImageItem { let id: EngineMedia.Id } -struct InstantPageTextAnchorItem { - let name: String - let anchorText: NSAttributedString? - let empty: Bool +public struct InstantPageTextAnchorItem { + public let name: String + public let anchorText: NSAttributedString? + public let empty: Bool } public struct InstantPageTextRangeRectEdge: Equatable { @@ -63,7 +63,7 @@ public final class InstantPageTextLine { let strikethroughItems: [InstantPageTextStrikethroughItem] let markedItems: [InstantPageTextMarkedItem] let imageItems: [InstantPageTextImageItem] - let anchorItems: [InstantPageTextAnchorItem] + public let anchorItems: [InstantPageTextAnchorItem] let isRTL: Bool init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], markedItems: [InstantPageTextMarkedItem], imageItems: [InstantPageTextImageItem], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool) { diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index d6dbd05029..79d1597354 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -477,16 +477,18 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender } func lcm(_ x: Int64, _ y: Int64) -> Int64 { + let x = max(x, 1) + let y = max(y, 1) return x / gcd(x, y) * y } - + return combineLatest(durations) |> map { durations in var result: Double let minDuration: Double = 3.0 if durations.count > 1 { let reduced = durations.reduce(1.0) { lhs, rhs -> Double in - return Double(lcm(Int64(lhs * 10.0), Int64(rhs * 10.0))) + return Double(lcm(Int64(lhs * 100.0), Int64(rhs * 100.0))) } result = min(6.0, Double(reduced) / 10.0) } else if let duration = durations.first { diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index 539b78af18..9ec039326d 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -1104,7 +1104,7 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega self.hideButtonNode.isHidden = confirmation case .email: self.inputNode.textField.keyboardType = .emailAddress - self.inputNode.textField.returnKeyType = .done + self.inputNode.textField.returnKeyType = .next self.hideButtonNode.isHidden = true if #available(iOS 12.0, *) { @@ -1134,7 +1134,7 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } case .hint: self.inputNode.textField.keyboardType = .asciiCapable - self.inputNode.textField.returnKeyType = .done + self.inputNode.textField.returnKeyType = .next self.hideButtonNode.isHidden = true self.inputNode.textField.autocorrectionType = .no diff --git a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift index 6c80171197..4885747d7f 100644 --- a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift +++ b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift @@ -5,6 +5,7 @@ import AccountContext import InstantPageUI import InstantPageCache import UrlHandling +import TelegramUIPreferences func faqSearchableItems(context: AccountContext, resolvedUrl: Signal, suggestAccountDeletion: Bool) -> Signal<[SettingsSearchableItem], NoError> { let strings = context.sharedContext.currentPresentationData.with { $0 }.strings @@ -45,8 +46,9 @@ func faqSearchableItems(context: AccountContext, resolvedUrl: Signal ViewControl return controller } -private func cleanDomain(url: String) -> (domain: String, fullUrl: String) { - if let parsedUrl = URL(string: url) { - let host: String? - let scheme = parsedUrl.scheme ?? "https" - if #available(iOS 16.0, *) { - host = parsedUrl.host(percentEncoded: true)?.lowercased() - } else { - host = parsedUrl.host?.lowercased() - } - return (host ?? url, "\(scheme)://\(host ?? "")") - } else { - return (url, url) - } -} - private func fetchDomainExceptionInfo(context: AccountContext, url: String) -> Signal { let (domain, domainUrl) = cleanDomain(url: url) if #available(iOS 13.0, *), let url = URL(string: domainUrl) { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD index e826f371a2..86a3e32102 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD @@ -51,6 +51,7 @@ swift_library( "//submodules/TextFormat", "//submodules/CounterControllerTitleView", "//submodules/TelegramUI/Components/AdminUserActionsSheet", + "//submodules/BrowserUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index b07d025dfc..32d3549ad1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -320,7 +320,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openUrl(url.url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { - strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) + if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType) { + navigationController.pushViewController(controller) + } } }, openWallpaper: { [weak self] message in if let strongSelf = self{ @@ -1223,8 +1225,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } case .chatFolder: break - case let .instantView(webpage, anchor): - strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel), anchor: anchor)) + case let .instantView(webPage, anchor): + let browserController = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel)) + strongSelf.pushController(browserController) case let .join(link): strongSelf.presentController(JoinLinkPreviewController(context: strongSelf.context, link: link, navigateToPeer: { peer, peekData in if let strongSelf = self { @@ -1257,6 +1260,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .premiumOffer: break + case .starsTopup: + break case let .joinVoiceChat(peerId, invite): strongSelf.presentController(VoiceChatJoinScreen(context: strongSelf.context, peerId: peerId, invite: invite, join: { call in }), .window(.root), nil) diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index aa7488a964..6442a231f7 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -776,7 +776,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } public final class EmojiTextAttachmentView: UIView { - private let contentLayer: InlineStickerItemLayer + public let contentLayer: InlineStickerItemLayer public var isActive: Bool = true { didSet { @@ -826,7 +826,7 @@ public final class EmojiTextAttachmentView: UIView { public final class CustomEmojiContainerView: UIView { private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? - private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] + public private(set) var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] public init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) { self.emojiViewProvider = emojiViewProvider diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index b930925be4..811350c9c9 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -1,5 +1,6 @@ import Foundation import TelegramCore +import UrlEscaping public func decodeCodableDrawingEntities(data: Data) -> [CodableDrawingEntity] { if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) { @@ -183,13 +184,9 @@ public enum CodableDrawingEntity: Equatable { return nil } case let .link(entity): - var url = entity.url - if !url.hasPrefix("http://") && !url.hasPrefix("https://") { - url = "https://\(url)" - } return .link( coordinates: coordinates, - url: url + url: explicitUrl(entity.url) ) case let .weather(entity): let color: UInt32 diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift index 02407f5e43..b07bd48a15 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift @@ -88,11 +88,15 @@ public final class DrawingTextEntity: DrawingEntity, Codable { return true } var isAnimated = false - self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in - if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { - isAnimated = true + + if let renderSubEntities = self.renderSubEntities { + for entity in renderSubEntities { + if entity.isAnimated { + isAnimated = true + break + } } - }) + } return isAnimated } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift index b8afdb5081..8b64da17ae 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift @@ -466,14 +466,21 @@ private final class CreateLinkSheetComponent: CombinedComponent { let text = !self.name.isEmpty ? self.name : self.link var effectiveMedia: TelegramMediaWebpage? - if let webpage = self.webpage, case .Loaded = webpage.content, !self.dismissed { + var webpageHasLargeMedia = false + if let webpage = self.webpage, case let .Loaded(content) = webpage.content, !self.dismissed { effectiveMedia = webpage + + if let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault { + webpageHasLargeMedia = true + } else { + webpageHasLargeMedia = true + } } var attributes: [MessageAttribute] = [] attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)])) if !self.dismissed { - attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true)) + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia ?? webpageHasLargeMedia, isManuallyAdded: false, isSafe: true)) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 49c5a40cff..ff60dcfd7d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -5940,13 +5940,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.push(controller) - editCoverImpl = { [weak self, weak controller] in + editCoverImpl = { [weak self] in if let self { self.node.openCoverSelection(exclusive: false) } - if let controller { - controller.dismiss() - } } }) } diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index 7597459eef..b316bf2f11 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -10,6 +10,7 @@ import UIKitRuntimeUtils private let minimizedNavigationHeight: CGFloat = 44.0 private let minimizedTopMargin: CGFloat = 3.0 +private let maximizeLastStandingController = false final class ScrollViewImpl: UIScrollView { var shouldPassthrough: () -> Bool = { return false } @@ -112,7 +113,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.snapshotContainerView.isUserInteractionEnabled = false super.init() - + self.clipsToBounds = true self.cornerRadius = 10.0 applySmoothRoundedCorners(self.layer) @@ -308,7 +309,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll private var presentationDataDisposable: Disposable? public private(set) var isExpanded: Bool = false - public var willMaximize: (() -> Void)? + public var willMaximize: ((MinimizedContainer) -> Void)? + public var willDismiss: ((MinimizedContainer) -> Void)? + public var didDismiss: ((MinimizedContainer) -> Void)? public private(set) var statusBarStyle: StatusBarStyle = .White public var statusBarStyleUpdated: (() -> Void)? @@ -500,10 +503,13 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.currentTransition = .dismiss(itemId: itemId) self.items.removeAll(where: { $0.id == itemId }) - if self.items.count == 1 { + if self.items.count == 1, maximizeLastStandingController { self.isExpanded = false - self.willMaximize?() + self.willMaximize?(self) needsLayout = false + } else if self.items.count == 0 { + self.willDismiss?(self) + self.isExpanded = false } } if let item = self.items.first(where: { $0.id == itemId }), !item.controller.shouldDismissImmediately() { @@ -558,6 +564,15 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.requestUpdate(transition: transition) } + public func removeController(_ viewController: MinimizableController) { + guard let item = self.items.first(where: { $0.controller === viewController }) else { + return + } + + self.items.removeAll(where: { $0.id == item.id }) + self.requestUpdate(transition: .animated(duration: 0.25, curve: .easeInOut)) + } + private enum Transition: Equatable { case minimize(itemId: AnyHashable) case maximize(itemId: AnyHashable) @@ -759,7 +774,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll if let currentTransition = self.currentTransition { if currentTransition.matches(item: item) { continue - } else if case .dismiss = currentTransition, self.items.count == 1 { + } else if case .dismiss = currentTransition, self.items.count == 1 && maximizeLastStandingController { continue } } @@ -789,10 +804,13 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.currentTransition = .dismiss(itemId: item.id) self.items.removeAll(where: { $0.id == item.id }) - if self.items.count == 1 { + if self.items.count == 1, maximizeLastStandingController { self.isExpanded = false - self.willMaximize?() + self.willMaximize?(self) needsLayout = false + } else if self.items.count == 0 { + self.isExpanded = false + self.willDismiss?(self) } if needsLayout { self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) @@ -1097,7 +1115,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll guard let dismissedItemNode = self.itemNodes[itemId] else { return } - if self.items.count == 1 { + if self.items.count == 1, maximizeLastStandingController { if let itemNode = self.itemNodes.first(where: { $0.0 != itemId })?.value, let navigationController = self.navigationController { itemNode.item.beforeMaximize(navigationController, { [weak self] in guard let self else { @@ -1137,6 +1155,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y)) } else { + let isLast = self.items.isEmpty transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y), completion: { _ in self.isApplyingTransition = false if self.currentTransition == currentTransition { @@ -1146,7 +1165,15 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll self.itemNodes[itemId] = nil dismissedItemNode.removeFromSupernode() + + if isLast { + self.didDismiss?(self) + } }) + if isLast { + let dismissOffset = collapsedHeight(layout: layout) + transition.updatePosition(layer: self.bottomEdgeView.layer, position: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: dismissOffset)) + } } case .dismissAll: let dismissOffset = collapsedHeight(layout: layout) diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift index 0944758d99..43e0faa569 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift @@ -73,7 +73,7 @@ final class MinimizedHeaderNode: ASDisplayNode { guard let self else { return } - let titles = titles.compactMap { $0 } + let titles = titles.compactMap { $0 }.filter { !$0.isEmpty } if titles.count == 1, let title = titles.first { self.title = title } else if let title = titles.last { diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift index ee5f726e16..c90bad1ee5 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/Utils.swift @@ -113,7 +113,10 @@ func interitemSpacing(itemCount: Int, boundingSize: CGSize, insets: UIEdgeInsets func frameForIndex(index: Int, size: CGSize, insets: UIEdgeInsets, itemCount: Int, boundingSize: CGSize) -> CGRect { let spacing = interitemSpacing(itemCount: itemCount, boundingSize: boundingSize, insets: insets) - let y = additionalInsetTop + insets.top + spacing * CGFloat(index) + var y = additionalInsetTop + insets.top + spacing * CGFloat(index) + if itemCount == 1 { + y += 72.0 + } let origin = CGPoint(x: insets.left, y: y) return CGRect(origin: origin, size: CGSize(width: size.width - insets.left - insets.right, height: size.height)) diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 0d010bdde4..1f39885a99 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -17,6 +17,7 @@ private var nextRenderTargetId: Int64 = 1 open class MultiAnimationRenderTarget: SimpleLayer { public let id: Int64 + public var numFrames: Int? let deinitCallbacks = Bag<() -> Void>() let updateStateCallbacks = Bag<() -> Void>() @@ -545,6 +546,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } target.contents = loadedFrame.image.cgImage + target.numFrames = item.numFrames if let blurredRepresentationTarget = target.blurredRepresentationTarget { blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage @@ -580,6 +582,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { completion(false, true) return } + target.numFrames = item.numFrames if let loadedFrame = loadedFrame { if let cgImage = loadedFrame.image.cgImage { if hadIntermediateUpdate { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 2ae84f8593..100fc0b918 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3351,7 +3351,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if let foundGalleryMessage = foundGalleryMessage { - strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) + if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType) { + navigationController.pushViewController(controller) + } } }, openWallpaper: { _ in }, openTheme: { _ in diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 42f157d5d9..e575681b3e 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -1690,8 +1690,12 @@ final class ShareWithPeersScreenComponent: Component { title: item.title, image: item.image, hasNext: false, - action: { + action: { [weak self] in + guard let self, let component = self.component else { + return + } component.editCover() + self.saveAndDismiss() } )), environment: {}, @@ -1993,6 +1997,36 @@ final class ShareWithPeersScreenComponent: Component { private var currentHasChannels: Bool? private var currentHasCover: Bool? + func saveAndDismiss() { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { + return + } + let base: EngineStoryPrivacy.Base + if self.selectedCategories.contains(.everyone) { + base = .everyone + } else if self.selectedCategories.contains(.closeFriends) { + base = .closeFriends + } else if self.selectedCategories.contains(.contacts) { + base = .contacts + } else if self.selectedCategories.contains(.selectedContacts) { + base = .nobody + } else { + base = .nobody + } + component.completion( + self.sendAsPeerId, + EngineStoryPrivacy( + base: base, + additionallyIncludePeers: self.selectedPeers + ), + self.selectedOptions.contains(.screenshot), + self.selectedOptions.contains(.pin), + self.component?.stateContext.stateValue?.peers.filter { self.selectedPeers.contains($0.id) } ?? [], + false + ) + controller.requestDismiss() + } + func update(component: ShareWithPeersScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { guard !self.isDismissed else { return availableSize @@ -2475,33 +2509,10 @@ final class ShareWithPeersScreenComponent: Component { component: AnyComponent(Button( content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), action: { [weak self] in - guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { + guard let self else { return } - let base: EngineStoryPrivacy.Base - if self.selectedCategories.contains(.everyone) { - base = .everyone - } else if self.selectedCategories.contains(.closeFriends) { - base = .closeFriends - } else if self.selectedCategories.contains(.contacts) { - base = .contacts - } else if self.selectedCategories.contains(.selectedContacts) { - base = .nobody - } else { - base = .nobody - } - component.completion( - self.sendAsPeerId, - EngineStoryPrivacy( - base: base, - additionallyIncludePeers: self.selectedPeers - ), - self.selectedOptions.contains(.screenshot), - self.selectedOptions.contains(.pin), - self.component?.stateContext.stateValue?.peers.filter { self.selectedPeers.contains($0.id) } ?? [], - false - ) - controller.requestDismiss() + self.saveAndDismiss() } ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), environment: {}, diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 74529be6e7..a5257985bd 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -208,7 +208,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let textString: String switch context.component.purpose { - case .generic: + case let .generic(requiredStars): + let _ = requiredStars textString = strings.Stars_Purchase_GetStarsInfo case .gift: textString = strings.Stars_Purchase_GiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string @@ -299,8 +300,13 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { if let products = state.products, let balance = context.component.balance { var minimumCount: Int64? if let requiredStars = context.component.purpose.requiredStars { - minimumCount = requiredStars - balance + if case .generic = context.component.purpose { + minimumCount = requiredStars + } else { + minimumCount = requiredStars - balance + } } + for product in products { if let minimumCount, minimumCount > product.count && !(items.isEmpty && product.id == products.last?.id) { continue @@ -810,8 +816,12 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { let titleText: String switch context.component.purpose { - case .generic: - titleText = strings.Stars_Purchase_GetStars + case let .generic(requiredStars): + if let requiredStars { + titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) + } else { + titleText = strings.Stars_Purchase_GetStars + } case .gift: titleText = strings.Stars_Purchase_GiftStars case let .transfer(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars): @@ -1216,6 +1226,8 @@ private extension StarsPurchasePurpose { var requiredStars: Int64? { switch self { + case let .generic(requiredStars): + return requiredStars case let .transfer(_, requiredStars): return requiredStars case let .subscription(_, requiredStars, _): diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index e6329ee119..d0ff4ecdb2 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -806,7 +806,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { guard let self else { return } - let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, completion: { [weak self] stars in + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic(requiredStars: nil), completion: { [weak self] stars in guard let self else { return } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 84cdd6589f..913a0eff7e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -484,7 +484,7 @@ private final class SheetContent: CombinedComponent { } else if let peerId = state?.botPeer?.id { purpose = .transfer(peerId: peerId, requiredStars: invoice.totalAmount) } else { - purpose = .generic + purpose = .generic(requiredStars: nil) } let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen( context: accountContext, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 1da76de05d..e4b726ed6c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -6118,7 +6118,7 @@ public final class StoryItemSetContainerComponent: Component { self.openStoryEditing() }))) - if case .file = component.slice.item.storyItem.media { + if case .file = component.slice.item.storyItem.media, component.slice.item.storyItem.isPinned { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6316,7 +6316,7 @@ public final class StoryItemSetContainerComponent: Component { self.openStoryEditing() }))) - if case .file = component.slice.item.storyItem.media { + if case .file = component.slice.item.storyItem.media, component.slice.item.storyItem.isPinned { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index b190c0cb86..5d89917c5c 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -402,156 +402,160 @@ public extension ChatControllerImpl { } } - func presentBotApp(botApp: BotApp, botPeer: EnginePeer, payload: String?, compact: Bool, concealed: Bool = false, commit: @escaping () -> Void = {}) { + func presentBotApp(botApp: BotApp?, botPeer: EnginePeer, payload: String?, compact: Bool, concealed: Bool = false, commit: @escaping () -> Void = {}) { guard let peerId = self.chatLocation.peerId else { return } self.attachmentController?.dismiss(animated: true, completion: nil) - let openBotApp: (Bool, Bool) -> Void = { [weak self] allowWrite, justInstalled in - guard let strongSelf = self else { - return - } - commit() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if !$0.contains(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.append(.requestInProgress) - return updatedContexts.sorted() - } - return $0 - } - }) - - let updateProgress = { [weak self] in - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if let index = $0.firstIndex(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts - } - return $0 - } - }) - } - } - } - - let botAddress = botPeer.addressName ?? "" - strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), compact: compact, allowWrite: allowWrite) - |> afterDisposed { - updateProgress() - }) - |> deliverOnMainQueue).startStrict(next: { [weak self] result in + if let botApp { + let openBotApp: (Bool, Bool) -> Void = { [weak self] allowWrite, justInstalled in guard let strongSelf = self else { return } - let context = strongSelf.context - let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize)) - var presentImpl: ((ViewController, Any?) -> Void)? - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit) - }, requestSwitchInline: { [weak self] query, chatTypes, completion in - ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) - }, completion: { [weak self] in - self?.chatDisplayNode.historyNode.scrollToEndOfHistory() - }, getNavigationController: { [weak self] in - return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + commit() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if !$0.contains(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.append(.requestInProgress) + return updatedContexts.sorted() + } + return $0 + } }) - controller.navigationPresentation = .flatModal - strongSelf.currentWebAppController = controller - strongSelf.push(controller) - presentImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - - if justInstalled { - let content: UndoOverlayContent = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil) - controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) - } - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - } - })) - } - - let _ = combineLatest( - queue: Queue.mainQueue(), - ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id), - self.context.engine.messages.attachMenuBots(), - self.context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - ).startStandalone(next: { [weak self] noticed, attachMenuBots, attachMenuBot in - guard let self else { - return - } - - var isAttachMenuBotInstalled: Bool? - if let _ = attachMenuBot { - if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) { - isAttachMenuBotInstalled = true - } else { - isAttachMenuBotInstalled = false - } - } - - let context = self.context - if !noticed || botApp.flags.contains(.notActivated) || isAttachMenuBotInstalled == false { - if let isAttachMenuBotInstalled, let attachMenuBot { - if !isAttachMenuBotInstalled { - let controller = webAppTermsAlertController(context: context, updatedPresentationData: self.updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite) - |> deliverOnMainQueue).startStandalone(error: { _ in - }, completed: { - openBotApp(allowWrite, true) + let updateProgress = { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } + return $0 + } }) + } + } + } + + let botAddress = botPeer.addressName ?? "" + strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), compact: compact, allowWrite: allowWrite) + |> afterDisposed { + updateProgress() + }) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let strongSelf = self else { + return + } + let context = strongSelf.context + let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize)) + var presentImpl: ((ViewController, Any?) -> Void)? + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in + presentImpl?(c, a) + }, commit: commit) + }, requestSwitchInline: { [weak self] query, chatTypes, completion in + ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) + }, completion: { [weak self] in + self?.chatDisplayNode.historyNode.scrollToEndOfHistory() + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController + }) + controller.navigationPresentation = .flatModal + strongSelf.currentWebAppController = controller + strongSelf.push(controller) + + presentImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + + if justInstalled { + let content: UndoOverlayContent = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil) + controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) + } + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) + } + + let _ = combineLatest( + queue: Queue.mainQueue(), + ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id), + self.context.engine.messages.attachMenuBots(), + self.context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + ).startStandalone(next: { [weak self] noticed, attachMenuBots, attachMenuBot in + guard let self else { + return + } + + var isAttachMenuBotInstalled: Bool? + if let _ = attachMenuBot { + if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) { + isAttachMenuBotInstalled = true + } else { + isAttachMenuBotInstalled = false + } + } + + let context = self.context + if !noticed || botApp.flags.contains(.notActivated) || isAttachMenuBotInstalled == false { + if let isAttachMenuBotInstalled, let attachMenuBot { + if !isAttachMenuBotInstalled { + let controller = webAppTermsAlertController(context: context, updatedPresentationData: self.updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite) + |> deliverOnMainQueue).startStandalone(error: { _ in + }, completed: { + openBotApp(allowWrite, true) + }) + }) + self.present(controller, in: .window(.root)) + } else { + openBotApp(false, false) + } + } else { + let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + openBotApp(allowWrite, false) + }, showMore: { [weak self] in + if let self { + self.openResolved(result: .peer(botPeer._asPeer(), .info(nil)), sourceMessageId: nil) + } }) self.present(controller, in: .window(.root)) - } else { - openBotApp(false, false) } } else { - let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - openBotApp(allowWrite, false) - }, showMore: { [weak self] in - if let self { - self.openResolved(result: .peer(botPeer._asPeer(), .info(nil)), sourceMessageId: nil) - } - }) - self.present(controller, in: .window(.root)) + openBotApp(false, false) } - } else { - openBotApp(false, false) - } - }) + }) + } else { + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) + } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 30c8cab465..0d1d596666 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2532,8 +2532,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.effectiveNavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { strongSelf.chatDisplayNode.dismissInput() - strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) - + if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType) { + navigationController.pushViewController(controller) + } if case .overlay = strongSelf.presentationInterfaceState.mode { strongSelf.chatDisplayNode.dismissAsOverlay() } @@ -9211,10 +9212,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id)) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in if let strongSelf = self, let peer { - strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact, concealed: concealed, commit: { - dismissWebAppControllers() + if let botApp = botAppStart.botApp { + strongSelf.presentBotApp(botApp: botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact, concealed: concealed, commit: { + dismissWebAppControllers() + commit() + }) + } else { + strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) commit() - }) + } } }) default: @@ -9256,7 +9262,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break default: progress?.set(.single(false)) - self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController) + if let controller = self.context.sharedContext.makeInstantPageController(context: self.context, message: message, sourcePeerType: nil) { + navigationController.pushViewController(controller) + } return } } diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index c0e002509a..cade6b3377 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -381,18 +381,16 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { return false } -func openChatInstantPageImpl(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) { - if let (webpage, anchor) = instantPageAndAnchor(message: message) { - let sourceLocation = InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: sourcePeerType ?? .channel) - - let pageController: ViewController - if context.sharedContext.immediateExperimentalUISettings.browserExperiment { - pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)) - } else { - pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor) - } - navigationController.pushViewController(pageController) +func makeInstantPageControllerImpl(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? { + guard let (webpage, anchor) = instantPageAndAnchor(message: message) else { + return nil } + let sourceLocation = InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: sourcePeerType ?? .channel) + return makeInstantPageControllerImpl(context: context, webPage: webpage, anchor: anchor, sourceLocation: sourceLocation) +} + +func makeInstantPageControllerImpl(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController { + return BrowserScreen(context: context, subject: .instantPage(webPage: webPage, anchor: anchor, sourceLocation: sourceLocation)) } func openChatWallpaperImpl(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 167e85acec..c92885e83d 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -248,15 +248,10 @@ func openResolvedUrlImpl( } }) present(controller, nil) - case let .instantView(webpage, anchor): + case let .instantView(webPage, anchor): let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel) - let pageController: ViewController - if context.sharedContext.immediateExperimentalUISettings.browserExperiment { - pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)) - } else { - pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor) - } - navigationController?.pushViewController(pageController) + let browserController = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation) + navigationController?.pushViewController(browserController) case let .join(link): dismissInput() @@ -661,6 +656,14 @@ func openResolvedUrlImpl( if let navigationController = navigationController { navigationController.pushViewController(controller, animated: true) } + case let .starsTopup(amount): + dismissInput() + if let starsContext = context.starsContext { + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: .generic(requiredStars: amount), completion: { _ in }) + if let navigationController = navigationController { + navigationController.pushViewController(controller, animated: true) + } + } case let .joinVoiceChat(peerId, invite): let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 4897df2846..a30266f109 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -918,6 +918,20 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } handleResolvedUrl(.premiumMultiGift(reference: reference)) + } else if parsedUrl.host == "stars_topup" { + var amount: Int64? + if let components = URLComponents(string: "/?" + query) { + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "amount" { + amount = Int64(value) + } + } + } + } + } + handleResolvedUrl(.starsTopup(amount: amount)) } else if parsedUrl.host == "addlist" { if let components = URLComponents(string: "/?" + query) { var slug: String? @@ -1047,7 +1061,14 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur let _ = (settings |> deliverOnMainQueue).startStandalone(next: { settings in - if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" { + var isTonSite = false + if let host = parsedUrl.host, host.lowercased().hasSuffix(".ton") { + isTonSite = true + } else if let scheme = parsedUrl.scheme, scheme.lowercased().hasPrefix("tonsite") { + isTonSite = true + } + + if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" && !isTonSite { let openInOptions = availableOpenInOptions(context: context, item: .url(url: url)) if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) { if case let .openUrl(openInUrl) = option.action() { @@ -1067,8 +1088,8 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur break } } - - if settings.defaultWebBrowser == nil && !isExceptedDomain { + + if (settings.defaultWebBrowser == nil && !isExceptedDomain) || isTonSite { let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) navigationController?.pushViewController(controller) } else { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 0c4101bd01..d040458dc2 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1846,8 +1846,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } - public func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) { - openChatInstantPageImpl(context: context, message: message, sourcePeerType: sourcePeerType, navigationController: navigationController) + public func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? { + return makeInstantPageControllerImpl(context: context, message: message, sourcePeerType: sourcePeerType) + } + + public func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController { + return makeInstantPageControllerImpl(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation) } public func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index a450ca906a..291257a38b 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -735,8 +735,22 @@ public final class TelegramRootController: NavigationController, TelegramRootCon //Xcode 16 #if canImport(ContactProvider) extension MediaEditorScreen.Result: @retroactive MediaEditorScreenResult { + public var target: Stories.PendingTarget { + if let sendAsPeerId = self.options.sendAsPeerId { + return .peer(sendAsPeerId) + } else { + return .myStories + } + } } #else extension MediaEditorScreen.Result: MediaEditorScreenResult { + public var target: Stories.PendingTarget { + if let sendAsPeerId = self.options.sendAsPeerId { + return .peer(sendAsPeerId) + } else { + return .myStories + } + } } #endif diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index 91d11008b4..5c3d6a7f65 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -15,6 +15,7 @@ import JoinLinkPreviewUI import PresentationDataUtils import UrlWhitelist import UndoUI +import BrowserUI func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) { let presentImpl: (ViewController, Any?) -> Void = { controllerToPresent, _ in @@ -87,8 +88,10 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n case let .stickerPack(name, _): let packReference: StickerPackReference = .name(name) controller.present(StickerPackScreen(context: context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) - case let .instantView(webpage, anchor): - (controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: peerId.flatMap(MediaResourceUserLocation.peer) ?? .other, peerType: .group), anchor: anchor)) + case let .instantView(webPage, anchor): + let sourceLocation = InstantPageSourceLocation(userLocation: peerId.flatMap(MediaResourceUserLocation.peer) ?? .other, peerType: .group) + let browserController = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation) + (controller.navigationController as? NavigationController)?.pushViewController(browserController, animated: true) case let .join(link): controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in openResolvedPeerImpl(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) diff --git a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift index 74c5eced05..5e2beb637c 100644 --- a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift @@ -491,6 +491,16 @@ public enum MediaAutoDownloadPeerType { case channel } +public struct InstantPageSourceLocation { + public var userLocation: MediaResourceUserLocation + public var peerType: MediaAutoDownloadPeerType + + public init(userLocation: MediaResourceUserLocation, peerType: MediaAutoDownloadPeerType) { + self.userLocation = userLocation + self.peerType = peerType + } +} + public func effectiveAutodownloadCategories(settings: MediaAutoDownloadSettings, networkType: MediaAutoDownloadNetworkType) -> MediaAutoDownloadCategories { let connection = settings.connectionSettings(for: networkType) switch connection.preset { diff --git a/submodules/UrlEscaping/Sources/UrlEscaping.swift b/submodules/UrlEscaping/Sources/UrlEscaping.swift index 83a6fed01c..bbf09a6488 100644 --- a/submodules/UrlEscaping/Sources/UrlEscaping.swift +++ b/submodules/UrlEscaping/Sources/UrlEscaping.swift @@ -22,7 +22,7 @@ public extension CharacterSet { }() } -public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": true, "https": true]) -> Bool { +public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": true, "https": true, "tonsite": true]) -> Bool { if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedUrl), let scheme = url.scheme?.lowercased(), let requiresTopLevelDomain = validSchemes[scheme], let host = url.host, (!requiresTopLevelDomain || host.contains(".")) && url.user == nil { if requiresTopLevelDomain { let components = host.components(separatedBy: ".") @@ -39,8 +39,12 @@ public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": tr public func explicitUrl(_ url: String) -> String { var url = url - if !url.hasPrefix("http") && !url.hasPrefix("https") && url.range(of: "://") == nil { - url = "https://\(url)" + if !url.lowercased().hasPrefix("http:") && !url.lowercased().hasPrefix("https:") && !url.lowercased().hasPrefix("tonsite:") && url.range(of: "://") == nil { + if let parsedUrl = URL(string: "http://\(url)"), parsedUrl.host?.hasSuffix(".ton") == true { + url = "tonsite://\(url)" + } else { + url = "https://\(url)" + } } return url } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index af85edc262..bc5e329a88 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -292,7 +292,20 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) } } return .startAttach(peerName, value, choose) - } else if queryItem.name == "story" { + } else if queryItem.name == "startapp" { + var compact = false + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "mode", value == "compact" { + compact = true + break + } + } + } + } + return .peer(.name(peerName), .appStart("", queryItem.value, compact)) + } else if queryItem.name == "story" { if let id = Int32(value) { return .peer(.name(peerName), .story(id)) } @@ -321,6 +334,19 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) return .peer(.name(peerName), .boost) } else if queryItem.name == "profile" { return .peer(.name(peerName), .profile) + } else if queryItem.name == "startapp" { + var compact = false + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "mode", value == "compact" { + compact = true + break + } + } + } + } + return .peer(.name(peerName), .appStart("", nil, compact)) } } } @@ -720,18 +746,26 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } } case let .appStart(name, payload, compact): - return .single(.progress) |> then(context.engine.messages.getBotApp(botId: peer.id, shortName: name, cached: false) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { botApp -> Signal in - if let botApp { - return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false, compact: compact))))) + if name.isEmpty { + if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { + return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: nil, payload: payload, justInstalled: false, compact: compact))))) } else { return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) } - }) + } else { + return .single(.progress) |> then(context.engine.messages.getBotApp(botId: peer.id, shortName: name, cached: false) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { botApp -> Signal in + if let botApp { + return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false, compact: compact))))) + } else { + return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) + } + }) + } case let .channelMessage(id, timecode): if case let .channel(channel) = peer, channel.flags.contains(.isForum) { let messageId = MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: id) @@ -1299,3 +1333,18 @@ public func resolveInstantViewUrl(account: Account, url: String) -> Signal (domain: String, fullUrl: String) { + if let parsedUrl = URL(string: url) { + let host: String? + let scheme = parsedUrl.scheme ?? "https" + if #available(iOS 16.0, *) { + host = parsedUrl.host(percentEncoded: true)?.lowercased() + } else { + host = parsedUrl.host?.lowercased() + } + return (host ?? url, "\(scheme)://\(host ?? "")") + } else { + return (url, url) + } +} diff --git a/submodules/Utils/DeviceModel/BUILD b/submodules/Utils/DeviceModel/BUILD new file mode 100644 index 0000000000..16d3d3f356 --- /dev/null +++ b/submodules/Utils/DeviceModel/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DeviceModel", + module_name = "DeviceModel", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/LegacyComponents", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift new file mode 100644 index 0000000000..ba30871c3b --- /dev/null +++ b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift @@ -0,0 +1,368 @@ +import Foundation + +public enum DeviceModel: CaseIterable, Equatable { + public static var allCases: [DeviceModel] { + return [ + .iPodTouch1, + .iPodTouch2, + .iPodTouch3, + .iPodTouch4, + .iPodTouch5, + .iPodTouch6, + .iPodTouch7, + .iPhone, + .iPhone3G, + .iPhone3GS, + .iPhone4, + .iPhone4S, + .iPhone5, + .iPhone5C, + .iPhone5S, + .iPhone6, + .iPhone6Plus, + .iPhone6S, + .iPhone6SPlus, + .iPhoneSE, + .iPhone7, + .iPhone7Plus, + .iPhone8, + .iPhone8Plus, + .iPhoneX, + .iPhoneXS, + .iPhoneXR, + .iPhone11, + .iPhone11Pro, + .iPhone11ProMax, + .iPhone12, + .iPhone12Mini, + .iPhone12Pro, + .iPhone12ProMax, + .iPhone13, + .iPhone13Mini, + .iPhone13Pro, + .iPhone13ProMax, + .iPhone14, + .iPhone14Plus, + .iPhone14Pro, + .iPhone14ProMax, + .iPhone15, + .iPhone15Plus, + .iPhone15Pro, + .iPhone15ProMax + ] + } + + case iPodTouch1 + case iPodTouch2 + case iPodTouch3 + case iPodTouch4 + case iPodTouch5 + case iPodTouch6 + case iPodTouch7 + + case iPhone + case iPhone3G + case iPhone3GS + + case iPhone4 + case iPhone4S + + case iPhone5 + case iPhone5C + case iPhone5S + + case iPhone6 + case iPhone6Plus + case iPhone6S + case iPhone6SPlus + + case iPhoneSE + + case iPhone7 + case iPhone7Plus + case iPhone8 + case iPhone8Plus + + case iPhoneX + case iPhoneXS + case iPhoneXSMax + case iPhoneXR + + case iPhone11 + case iPhone11Pro + case iPhone11ProMax + + case iPhoneSE2ndGen + + case iPhone12 + case iPhone12Mini + case iPhone12Pro + case iPhone12ProMax + + case iPhone13 + case iPhone13Mini + case iPhone13Pro + case iPhone13ProMax + + case iPhoneSE3rdGen + + case iPhone14 + case iPhone14Plus + case iPhone14Pro + case iPhone14ProMax + + case iPhone15 + case iPhone15Plus + case iPhone15Pro + case iPhone15ProMax + + case unknown(String) + + public var modelId: [String] { + switch self { + case .iPodTouch1: + return ["iPod1,1"] + case .iPodTouch2: + return ["iPod2,1"] + case .iPodTouch3: + return ["iPod3,1"] + case .iPodTouch4: + return ["iPod4,1"] + case .iPodTouch5: + return ["iPod5,1"] + case .iPodTouch6: + return ["iPod7,1"] + case .iPodTouch7: + return ["iPod9,1"] + case .iPhone: + return ["iPhone1,1"] + case .iPhone3G: + return ["iPhone1,2"] + case .iPhone3GS: + return ["iPhone2,1"] + case .iPhone4: + return ["iPhone3,1", "iPhone3,2", "iPhone3,3"] + case .iPhone4S: + return ["iPhone4,1", "iPhone4,2", "iPhone4,3"] + case .iPhone5: + return ["iPhone5,1", "iPhone5,2"] + case .iPhone5C: + return ["iPhone5,3", "iPhone5,4"] + case .iPhone5S: + return ["iPhone6,1", "iPhone6,2"] + case .iPhone6: + return ["iPhone7,2"] + case .iPhone6Plus: + return ["iPhone7,1"] + case .iPhone6S: + return ["iPhone8,1"] + case .iPhone6SPlus: + return ["iPhone8,2"] + case .iPhoneSE: + return ["iPhone8,4"] + case .iPhone7: + return ["iPhone9,1", "iPhone9,3"] + case .iPhone7Plus: + return ["iPhone9,2", "iPhone9,4"] + case .iPhone8: + return ["iPhone10,1", "iPhone10,4"] + case .iPhone8Plus: + return ["iPhone10,2", "iPhone10,5"] + case .iPhoneX: + return ["iPhone10,3", "iPhone10,6"] + case .iPhoneXS: + return ["iPhone11,2"] + case .iPhoneXSMax: + return ["iPhone11,4", "iPhone11,6"] + case .iPhoneXR: + return ["iPhone11,8"] + case .iPhone11: + return ["iPhone12,1"] + case .iPhone11Pro: + return ["iPhone12,3"] + case .iPhone11ProMax: + return ["iPhone12,5"] + case .iPhoneSE2ndGen: + return ["iPhone12,8"] + case .iPhone12: + return ["iPhone13,2"] + case .iPhone12Mini: + return ["iPhone13,1"] + case .iPhone12Pro: + return ["iPhone13,3"] + case .iPhone12ProMax: + return ["iPhone13,4"] + case .iPhone13: + return ["iPhone14,5"] + case .iPhone13Mini: + return ["iPhone14,4"] + case .iPhone13Pro: + return ["iPhone14,2"] + case .iPhone13ProMax: + return ["iPhone14,3"] + case .iPhoneSE3rdGen: + return ["iPhone14,6"] + case .iPhone14: + return ["iPhone14,7"] + case .iPhone14Plus: + return ["iPhone14,8"] + case .iPhone14Pro: + return ["iPhone15,2"] + case .iPhone14ProMax: + return ["iPhone15,3"] + case .iPhone15: + return ["iPhone15,4"] + case .iPhone15Plus: + return ["iPhone15,5"] + case .iPhone15Pro: + return ["iPhone16,1"] + case .iPhone15ProMax: + return ["iPhone16,2"] + case let .unknown(modelId): + return [modelId] + } + } + + public var modelName: String { + switch self { + case .iPodTouch1: + return "iPod touch 1G" + case .iPodTouch2: + return "iPod touch 2G" + case .iPodTouch3: + return "iPod touch 3G" + case .iPodTouch4: + return "iPod touch 4G" + case .iPodTouch5: + return "iPod touch 5G" + case .iPodTouch6: + return "iPod touch 6G" + case .iPodTouch7: + return "iPod touch 7G" + case .iPhone: + return "iPhone" + case .iPhone3G: + return "iPhone 3G" + case .iPhone3GS: + return "iPhone 3GS" + case .iPhone4: + return "iPhone 4" + case .iPhone4S: + return "iPhone 4S" + case .iPhone5: + return "iPhone 5" + case .iPhone5C: + return "iPhone 5C" + case .iPhone5S: + return "iPhone 5S" + case .iPhone6: + return "iPhone 6" + case .iPhone6Plus: + return "iPhone 6 Plus" + case .iPhone6S: + return "iPhone 6S" + case .iPhone6SPlus: + return "iPhone 6S Plus" + case .iPhoneSE: + return "iPhone SE" + case .iPhone7: + return "iPhone 7" + case .iPhone7Plus: + return "iPhone 7 Plus" + case .iPhone8: + return "iPhone 8" + case .iPhone8Plus: + return "iPhone 8 Plus" + case .iPhoneX: + return "iPhone X" + case .iPhoneXS: + return "iPhone XS" + case .iPhoneXSMax: + return "iPhone XS Max" + case .iPhoneXR: + return "iPhone XR" + case .iPhone11: + return "iPhone 11" + case .iPhone11Pro: + return "iPhone 11 Pro" + case .iPhone11ProMax: + return "iPhone 11 Pro Max" + case .iPhoneSE2ndGen: + return "iPhone SE (2nd gen)" + case .iPhone12: + return "iPhone 12" + case .iPhone12Mini: + return "iPhone 12 mini" + case .iPhone12Pro: + return "iPhone 12 Pro" + case .iPhone12ProMax: + return "iPhone 12 Pro Max" + case .iPhone13: + return "iPhone 13" + case .iPhone13Mini: + return "iPhone 13 mini" + case .iPhone13Pro: + return "iPhone 13 Pro" + case .iPhone13ProMax: + return "iPhone 13 Pro Max" + case .iPhoneSE3rdGen: + return "iPhone SE (3rd gen)" + case .iPhone14: + return "iPhone 14" + case .iPhone14Plus: + return "iPhone 14 Plus" + case .iPhone14Pro: + return "iPhone 14 Pro" + case .iPhone14ProMax: + return "iPhone 14 Pro Max" + case .iPhone15: + return "iPhone 15" + case .iPhone15Plus: + return "iPhone 15 Plus" + case .iPhone15Pro: + return "iPhone 15 Pro" + case .iPhone15ProMax: + return "iPhone 15 Pro Max" + case let .unknown(modelId): + if modelId.hasPrefix("iPhone") { + return "Unknown iPhone" + } else if modelId.hasPrefix("iPod") { + return "Unknown iPod" + } else if modelId.hasPrefix("iPad") { + return "Unknown iPad" + } else { + return "Unknown Device" + } + } + } + + public var isIpad: Bool { + return self.modelId.first?.hasPrefix("iPad") ?? false + } + + public static let current = DeviceModel() + + private init() { + var systemInfo = utsname() + uname(&systemInfo) + let modelCode = withUnsafePointer(to: &systemInfo.machine) { + $0.withMemoryRebound(to: CChar.self, capacity: 1) { + ptr in String.init(validatingUTF8: ptr) + } + } + var result: DeviceModel? + if let modelCode { + for model in DeviceModel.allCases { + if model.modelId.contains(modelCode) { + result = model + break + } + } + } + if let result { + self = result + } else { + self = .unknown(modelCode ?? "") + } + } +} diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 70f24be4cf..19b80d186e 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -40,6 +40,7 @@ swift_library( "//submodules/ShareController", "//submodules/UndoUI", "//submodules/OverlayStatusController", + "//submodules/TelegramUIPreferences", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 9b32c4a186..954c427f96 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -29,6 +29,7 @@ import ShareController import UndoUI import AvatarNode import OverlayStatusController +import TelegramUIPreferences private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] @@ -333,9 +334,6 @@ public final class WebAppController: ViewController, AttachmentContainable { if let parsedUrl = URL(string: url) { self.webView?.load(URLRequest(url: parsedUrl)) } - - self.checkBotIdAndUrl(url) - if let keepAliveSignal = controller.keepAliveSignal { self.keepAliveDisposable = (keepAliveSignal |> deliverOnMainQueue).start(error: { [weak self] _ in @@ -355,7 +353,6 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let strongSelf = self else { return } - strongSelf.checkBotIdAndUrl(result.url) if let parsedUrl = URL(string: result.url) { strongSelf.queryId = result.queryId strongSelf.webView?.load(URLRequest(url: parsedUrl)) @@ -368,17 +365,16 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let self, let controller = self.controller else { return } - guard case let .peer(peer, params) = result, let peer, case let .withBotApp(appStart) = params else { + guard case let .peer(peer, params) = result, let peer, case let .withBotApp(appStart) = params, let botApp = appStart.botApp else { controller.dismiss() return } - let _ = (self.context.engine.messages.requestAppWebView(peerId: peer.id, appReference: .id(id: appStart.botApp.id, accessHash: appStart.botApp.accessHash), payload: appStart.payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), compact: appStart.compact, allowWrite: true) + let _ = (self.context.engine.messages.requestAppWebView(peerId: peer.id, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: appStart.payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), compact: appStart.compact, allowWrite: true) |> deliverOnMainQueue).startStandalone(next: { [weak self] result in guard let self, let parsedUrl = URL(string: result.url) else { return } - self.checkBotIdAndUrl(result.url) - self.controller?.titleView?.title = WebAppTitle(title: appStart.botApp.title, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: controller.botVerified) + self.controller?.titleView?.title = WebAppTitle(title: botApp.title, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: controller.botVerified) self.webView?.load(URLRequest(url: parsedUrl)) }) }) @@ -390,9 +386,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } strongSelf.queryId = result.queryId strongSelf.webView?.load(URLRequest(url: parsedUrl)) - - strongSelf.checkBotIdAndUrl(result.url) - + if let keepAliveSignal = result.keepAliveSignal { strongSelf.keepAliveDisposable = (keepAliveSignal |> deliverOnMainQueue).start(error: { [weak self] _ in @@ -412,14 +406,6 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - func checkBotIdAndUrl(_ url: String) { -// if url.hasPrefix("https://walletbot.me"), let botId = self.controller?.botId.id._internalGetInt64Value(), botId != 1985737506 { -// let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: "Bot id mismatch, please report steps to app developer", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { -// })]) -// self.controller?.present(alertController, in: .window(.root)) -// } - } - @objc fileprivate func mainButtonPressed() { if let mainButtonState = self.mainButtonState, !mainButtonState.isVisible || !mainButtonState.isEnabled { return @@ -845,7 +831,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } switch result { case let .instantView(webPage, anchor): - let controller = InstantPageController(context: strongSelf.context, webPage: webPage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate), anchor: anchor) + let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate)) strongSelf.controller?.getNavigationController()?.pushViewController(controller) default: strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) @@ -1164,15 +1150,7 @@ public final class WebAppController: ViewController, AttachmentContainable { transitionOut: nil ) let controller = self.context.sharedContext.makeStoryMediaEditorScreen(context: self.context, source: source, text: text, link: linkUrl.flatMap { ($0, linkName) }, completion: { result, commit in -// let targetPeerId: EnginePeer.Id - let target: Stories.PendingTarget -// if let sendAsPeerId = result.options.sendAsPeerId { -// target = .peer(sendAsPeerId) -// targetPeerId = sendAsPeerId -// } else { - target = .myStories -// targetPeerId = self.context.account.peerId -// } + let target: Stories.PendingTarget = result.target externalState.storyTarget = target if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { From 0e4c46412d549bd12c7fc95bd35f880c51c3fc2c Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 2 Aug 2024 23:29:32 +0200 Subject: [PATCH 11/14] Bump version --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 394e4986a7..671aec9b37 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "10.15", + "app": "10.15.1", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0" From 18f23089714927c5885b7dfdbd169e078daf9494 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 9 Aug 2024 20:38:27 +0200 Subject: [PATCH 12/14] Fix build --- .../Sources/StarsTransactionsScreen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index a939524fcc..39ff8d8111 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -972,7 +972,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { guard let self else { return } - let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic(requiredStars: nil), completion: { [weak self] stars in + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, completion: { [weak self] stars in guard let self else { return } From ab4a155172fcf6f3db8bb02b0d408f4d31f655b8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 9 Aug 2024 21:03:23 +0200 Subject: [PATCH 13/14] Fix build --- .../Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 634a975049..30d333d5ca 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -532,7 +532,7 @@ private final class SheetContent: CombinedComponent { } else if let peerId = state?.botPeer?.id { purpose = .transfer(peerId: peerId, requiredStars: invoice.totalAmount) } else { - purpose = .generic(requiredStars: nil) + purpose = .generic } let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen( context: accountContext, From babdec7c07cd203de5e2d6c56a7b3ec1fd25ef8d Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 13 Aug 2024 07:27:30 +0800 Subject: [PATCH 14/14] Fix reaction limit --- .../BrowserUI/Sources/BrowserDocumentContent.swift | 2 +- .../Sources/PeerAllowedReactionsScreen.swift | 3 +++ .../PeerInfoScreen/Sources/PeerInfoScreen.swift | 12 ++++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index bb3774ba87..44597c4fbd 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -346,7 +346,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) } - func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { self.updateFontState(self.currentFontState, force: true) } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index a8bbe1b016..b347a46bfd 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -1402,6 +1402,9 @@ public class PeerAllowedReactionsScreen: ViewControllerComponentContainer { case .empty: isEnabled = false } + if let starsAllowed = reactionSettings.starsAllowed, starsAllowed { + isEnabled = true + } } var missingReactionFiles: [Int64] = [] diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 246826732e..41a613f8f5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1993,9 +1993,17 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL case .all: label = presentationData.strings.PeerInfo_LabelAllReactions case .empty: - label = presentationData.strings.PeerInfo_ReactionsDisabled + if let starsAllowed = reactionSettings.starsAllowed, starsAllowed { + label = "1" + } else { + label = presentationData.strings.PeerInfo_ReactionsDisabled + } case let .limited(reactions): - label = "\(reactions.count)" + var countValue = reactions.count + if let starsAllowed = reactionSettings.starsAllowed, starsAllowed { + countValue += 1 + } + label = "\(countValue)" } } else { label = ""