diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5fccbe524c..81aa77bc65 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13650,3 +13650,20 @@ Sorry for the inconvenience."; "Story.ViewGift" = "View Gift"; "Camera.OpenChat" = "Open Chat"; + +"Conversation.AddToContactsLong" = "Add to Contacts"; + +"PeerInfo.PaneRecommendedBots" = "Similar Bots"; + +"SharedMedia.SimilarChannelCount_1" = "%@ channel"; +"SharedMedia.SimilarChannelCount_any" = "%@ channels"; + +"SharedMedia.SimilarBotCount_1" = "%@ bot"; +"SharedMedia.SimilarBotCount_any" = "%@ bots"; + +"PeerInfo.SimilarBots.ShowMore" = "Show More Bots"; +"PeerInfo.SimilarBots.ShowMoreInfo" = "Subscribe to [Telegram Premium]()\nto unlock up to **100** similar bots."; + +"Gift.View.Context.Share" = "Share"; +"Gift.View.Context.CopyLink" = "Copy Link"; +"Gift.View.Context.Transfer" = "Transfer"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 01baed1ccb..882f544521 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1103,9 +1103,11 @@ public protocol SharedAccountContext: AnyObject { func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: (() -> Void)?) -> ViewController func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: (() -> Void)?) -> ViewController + func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController + func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) - func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, actionCompleted: (() -> Void)?) -> ViewController + func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController diff --git a/submodules/AccountContext/Sources/ShareController.swift b/submodules/AccountContext/Sources/ShareController.swift index a7cf5d763a..3281342e1f 100644 --- a/submodules/AccountContext/Sources/ShareController.swift +++ b/submodules/AccountContext/Sources/ShareController.swift @@ -7,6 +7,11 @@ import TelegramUIPreferences import AnimationCache import MultiAnimationRenderer +public enum StorySharingSubject { + case messages([Message]) + case gift(StarGift.UniqueGift) +} + public protocol ShareControllerAccountContext: AnyObject { var accountId: AccountRecordId { get } var accountPeerId: EnginePeer.Id { get } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index b95b44fe7f..b50d94405c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -382,17 +382,12 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size) } - var hasCloseButton = false - if case .xmasPremiumGift = item.notice { - hasCloseButton = true - } else if case .setupBirthday = item.notice { - hasCloseButton = true - } else if case .birthdayPremiumGift = item.notice { - hasCloseButton = true - } else if case .premiumGrace = item.notice { - hasCloseButton = true - } else if case .starsSubscriptionLowBalance = item.notice { + let hasCloseButton: Bool + switch item.notice { + case .xmasPremiumGift, .setupBirthday, .birthdayPremiumGift, .premiumGrace, .starsSubscriptionLowBalance, .setupPhoto: hasCloseButton = true + default: + hasCloseButton = false } if let okButtonLayout, let cancelButtonLayout { diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 65fe2836a5..d15cd4ab70 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -221,25 +221,17 @@ private enum ContactListNodeEntry: Comparable, Identifiable { })] } - - var storyStats: (total: Int, unseen: Int, hasUnseenCloseFriends: Bool)? if let customSubtitle { status = .custom(string: NSAttributedString(string: customSubtitle), multiline: false, isActive: false, icon: nil) - } else if let storyData { - storyStats = (storyData.count, storyData.unseenCount, storyData.hasUnseenCloseFriends) - - let text: String - text = presentationData.strings.ChatList_ArchiveStoryCount(Int32(storyData.count)) - status = .custom(string: NSAttributedString(string: text), multiline: false, isActive: false, icon: nil) } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in - interaction.openPeer(peer, .generic, nil, nil) - }, disabledAction: { _ in - if case let .peer(peer, _, _) = peer { - interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) - } - }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: storyStats, openStories: { peer, sourceNode in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in + interaction.openPeer(peer, .generic, nil, nil) + }, disabledAction: { _ in + if case let .peer(peer, _, _) = peer { + interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) + } + }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: nil, openStories: { peer, sourceNode in if case let .peer(peerValue, _) = peer, let peerValue { interaction.openStories(peerValue, sourceNode) } @@ -581,15 +573,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis case let .custom(showSelf, sections): if !topPeers.isEmpty { var index: Int = 0 - - if showSelf, let accountPeer { - if let peer = topPeers.first(where: { $0.id == accountPeer.id }) { - let header = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_ThisIsYou.uppercased(), AnyHashable(10)), theme: theme, strings: strings) - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, strings.Premium_Gift_ContactSelection_BuySelf)) - existingPeerIds.insert(.peer(peer.id)) - } - } - + var sectionId: Int = 2 for (title, peerIds, hasActions) in sections { var allSelected = true @@ -647,6 +631,14 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis sectionId += 1 } + if showSelf, let accountPeer { + if let peer = topPeers.first(where: { $0.id == accountPeer.id }) { + let header = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_ThisIsYou.uppercased(), AnyHashable(10)), theme: theme, strings: strings) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, strings.Premium_Gift_ContactSelection_BuySelf)) + existingPeerIds.insert(.peer(peer.id)) + } + } + var hasDeselectAll = !(selectionState?.selectedPeerIndices ?? [:]).isEmpty if !sections.isEmpty, let selectionState { var hasNonBirthdayPeers = false diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index e50f772e3a..7f65ce1f9d 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -810,6 +810,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { selectionView.handlePan(gestureRecognizer) } else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .message = stickerEntity.content { selectionView.handlePan(gestureRecognizer) + } else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .gift = stickerEntity.content { + selectionView.handlePan(gestureRecognizer) } else { var isTrappedInBin = false let scale = 100.0 / selectedEntityView.bounds.size.width diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index ca595aadc5..b98216aa6c 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -3096,6 +3096,8 @@ public final class DrawingToolsInteraction { isAdditional = isAdditionalValue } else if case .message = entity.content { isMessage = true + } else if case .gift = entity.content { + isMessage = true } } else if entityView.entity is DrawingLinkEntity { isLink = true diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 9de13e9719..27bb68504b 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -664,6 +664,8 @@ public final class ShareController: ViewController { var fromPublicChannel = false if case let .messages(messages) = self.subject, let message = messages.first, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { fromPublicChannel = true + } else if case let .url(link) = self.subject, link.contains("t.me/nft/") { + fromPublicChannel = true } self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index 902f14b131..8acbcb2030 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -121,6 +121,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese subject: .url("https://t.me/addstickers/\(info.shortName)"), forceExternal: true, shareStory: nil, + enqueued: nil, actionCompleted: { [weak parentNavigationController] in if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index e98d2366d8..475962b539 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -13,7 +13,6 @@ import TelegramPresentationData import ShimmerEffect import StickerPeekUI import TextFormat -import Accelerate final class StickerPackPreviewInteraction { var previewedItem: StickerPreviewPeekItem? @@ -536,100 +535,3 @@ final class StickerPackPreviewGridItemNode: GridItemNode { } } } - -private func getAverageColor(image: UIImage) -> UIColor? { - let blurredWidth = 16 - let blurredHeight = 16 - let blurredBytesPerRow = blurredWidth * 4 - guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: blurredBytesPerRow) else { - return nil - } - - let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) - - if let cgImage = image.cgImage { - context.withFlippedContext { c in - c.setFillColor(UIColor.white.cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - c.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8))) - } - } - - var destinationBuffer = vImage_Buffer() - destinationBuffer.width = UInt(blurredWidth) - destinationBuffer.height = UInt(blurredHeight) - destinationBuffer.data = context.bytes - destinationBuffer.rowBytes = context.bytesPerRow - - vImageBoxConvolve_ARGB8888(&destinationBuffer, - &destinationBuffer, - nil, - 0, 0, - UInt32(15), - UInt32(15), - nil, - vImage_Flags(kvImageTruncateKernel)) - - let divisor: Int32 = 0x1000 - - let rwgt: CGFloat = 0.3086 - let gwgt: CGFloat = 0.6094 - let bwgt: CGFloat = 0.0820 - - let adjustSaturation: CGFloat = 1.7 - - let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation - let b = (1.0 - adjustSaturation) * rwgt - let c = (1.0 - adjustSaturation) * rwgt - let d = (1.0 - adjustSaturation) * gwgt - let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation - let f = (1.0 - adjustSaturation) * gwgt - let g = (1.0 - adjustSaturation) * bwgt - let h = (1.0 - adjustSaturation) * bwgt - let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation - - let satMatrix: [CGFloat] = [ - a, b, c, 0, - d, e, f, 0, - g, h, i, 0, - 0, 0, 0, 1 - ] - - var matrix: [Int16] = satMatrix.map { value in - return Int16(value * CGFloat(divisor)) - } - - vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) - - context.withFlippedContext { c in - c.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - } - - var sumR: UInt64 = 0 - var sumG: UInt64 = 0 - var sumB: UInt64 = 0 - var sumA: UInt64 = 0 - - for y in 0 ..< blurredHeight { - let row = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredBytesPerRow) - for x in 0 ..< blurredWidth { - let pixel = row.advanced(by: x * 4) - sumB += UInt64(pixel.advanced(by: 0).pointee) - sumG += UInt64(pixel.advanced(by: 1).pointee) - sumR += UInt64(pixel.advanced(by: 2).pointee) - sumA += UInt64(pixel.advanced(by: 3).pointee) - } - } - sumR /= UInt64(blurredWidth * blurredHeight) - sumG /= UInt64(blurredWidth * blurredHeight) - sumB /= UInt64(blurredWidth * blurredHeight) - sumA /= UInt64(blurredWidth * blurredHeight) - sumA = 255 - - var color = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0) - if color.lightness > 0.8 { - color = color.withMultipliedBrightnessBy(0.8) - } - return color -} diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index e1689b37bb..ff097a6e60 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -1137,6 +1137,7 @@ private final class StickerPackContainer: ASDisplayNode { subject: shareSubject, forceExternal: false, shareStory: nil, + enqueued: nil, actionCompleted: { [weak parentNavigationController] in if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index b2ed2535d7..572611db40 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -8,6 +8,7 @@ import MediaResources import Tuples import ImageBlur import FastBlur +import Accelerate public func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? { if let (colorData, alphaData) = data.withUnsafeBytes({ bytes -> (Data, Data)? in @@ -660,3 +661,100 @@ public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCol return .single(true) } + +public func getAverageColor(image: UIImage) -> UIColor? { + let blurredWidth = 16 + let blurredHeight = 16 + let blurredBytesPerRow = blurredWidth * 4 + guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: blurredBytesPerRow) else { + return nil + } + + let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) + + if let cgImage = image.cgImage { + context.withFlippedContext { c in + c.setFillColor(UIColor.white.cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + c.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8))) + } + } + + var destinationBuffer = vImage_Buffer() + destinationBuffer.width = UInt(blurredWidth) + destinationBuffer.height = UInt(blurredHeight) + destinationBuffer.data = context.bytes + destinationBuffer.rowBytes = context.bytesPerRow + + vImageBoxConvolve_ARGB8888(&destinationBuffer, + &destinationBuffer, + nil, + 0, 0, + UInt32(15), + UInt32(15), + nil, + vImage_Flags(kvImageTruncateKernel)) + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation: CGFloat = 1.7 + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) + + context.withFlippedContext { c in + c.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + } + + var sumR: UInt64 = 0 + var sumG: UInt64 = 0 + var sumB: UInt64 = 0 + var sumA: UInt64 = 0 + + for y in 0 ..< blurredHeight { + let row = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredBytesPerRow) + for x in 0 ..< blurredWidth { + let pixel = row.advanced(by: x * 4) + sumB += UInt64(pixel.advanced(by: 0).pointee) + sumG += UInt64(pixel.advanced(by: 1).pointee) + sumR += UInt64(pixel.advanced(by: 2).pointee) + sumA += UInt64(pixel.advanced(by: 3).pointee) + } + } + sumR /= UInt64(blurredWidth * blurredHeight) + sumG /= UInt64(blurredWidth * blurredHeight) + sumB /= UInt64(blurredWidth * blurredHeight) + sumA /= UInt64(blurredWidth * blurredHeight) + sumA = 255 + + var color = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0) + if color.lightness > 0.8 { + color = color.withMultipliedBrightnessBy(0.8) + } + return color +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index ba7b2a4dd4..9b26ac2eb2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -138,6 +138,7 @@ public struct Namespaces { public static let starsReactionDefaultToPrivate: Int8 = 41 public static let cachedPremiumGiftCodeOptions: Int8 = 42 public static let cachedProfileGifts: Int8 = 43 + public static let recommendedBots: Int8 = 44 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/BotRecomendation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/BotRecomendation.swift new file mode 100644 index 0000000000..2f7d3681cb --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/BotRecomendation.swift @@ -0,0 +1,130 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +final class CachedRecommendedBots: Codable { + public let peerIds: [EnginePeer.Id] + public let count: Int32 + public let timestamp: Int32? + + public init(peerIds: [EnginePeer.Id], count: Int32, timestamp: Int32?) { + self.peerIds = peerIds + self.count = count + self.timestamp = timestamp + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.peerIds = try container.decode([Int64].self, forKey: "l").map(EnginePeer.Id.init) + self.count = try container.decodeIfPresent(Int32.self, forKey: "c") ?? 0 + self.timestamp = try container.decodeIfPresent(Int32.self, forKey: "ts") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "l") + try container.encode(self.count, forKey: "c") + try container.encodeIfPresent(self.timestamp, forKey: "ts") + } +} + +private func entryId(peerId: EnginePeer.Id?) -> ItemCacheEntryId { + let cacheKey = ValueBoxKey(length: 8) + if let peerId { + cacheKey.setInt64(0, value: peerId.toInt64()) + } else { + cacheKey.setInt64(0, value: 0) + } + return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.recommendedBots, key: cacheKey) +} + +func _internal_requestRecommendedBots(account: Account, peerId: EnginePeer.Id, forceUpdate: Bool) -> Signal { + return account.postbox.transaction { transaction -> (Peer?, Bool) in + guard let user = transaction.getPeer(peerId) as? TelegramUser, let _ = user.botInfo else { + return (nil, false) + } + if let entry = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedRecommendedBots.self), !entry.peerIds.isEmpty && !forceUpdate { + return (nil, false) + } else { + return (user, true) + } + } + |> mapToSignal { user, shouldUpdate in + guard shouldUpdate, let user, let inputUser = apiInputUser(user) else { + return .complete() + } + return account.network.request(Api.functions.bots.getBotRecommendations(bot: inputUser)) + |> retryRequest + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> [EnginePeer] in + let users: [Api.User] + let parsedPeers: AccumulatedPeers + var count: Int32 + switch result { + case let .users(apiUsers): + users = apiUsers + count = Int32(apiUsers.count) + case let .usersSlice(apiCount, apiUsers): + users = apiUsers + count = apiCount + } + parsedPeers = AccumulatedPeers(users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + + let peers = users.map { EnginePeer(TelegramUser(user: $0)) } + if let entry = CodableEntry(CachedRecommendedBots(peerIds: peers.map(\.id), count: count, timestamp: Int32(Date().timeIntervalSince1970))) { + transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry) + } + return peers + } + |> ignoreValues + } + } +} + +public struct RecommendedBots: Equatable { + public var bots: [EnginePeer] + public var count: Int32 + + public init(bots: [EnginePeer], count: Int32) { + self.bots = bots + self.count = count + } +} + +func _internal_recommendedBotPeerIds(account: Account, peerId: EnginePeer.Id) -> Signal<[EnginePeer.Id]?, NoError> { + let key = PostboxViewKey.cachedItem(entryId(peerId: peerId)) + return account.postbox.combinedView(keys: [key]) + |> mapToSignal { views -> Signal<[EnginePeer.Id]?, NoError> in + guard let cachedBots = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedBots.self), !cachedBots.peerIds.isEmpty else { + return .single(nil) + } + return .single(cachedBots.peerIds) + } +} + +func _internal_recommendedBots(account: Account, peerId: EnginePeer.Id) -> Signal { + let key = PostboxViewKey.cachedItem(entryId(peerId: peerId)) + return account.postbox.combinedView(keys: [key]) + |> mapToSignal { views -> Signal in + guard let cachedBots = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedBots.self) else { + return .single(nil) + } + if cachedBots.peerIds.isEmpty { + return .single(nil) + } + return account.postbox.transaction { transaction -> RecommendedBots? in + var bots: [EnginePeer] = [] + for peerId in cachedBots.peerIds { + if let peer = transaction.getPeer(peerId) { + bots.append(EnginePeer(peer)) + } + } + return RecommendedBots(bots: bots, count: cachedBots.count) + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index b716f47008..986ec158e2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1454,7 +1454,15 @@ public extension TelegramEngine { public func requestRecommendedAppsIfNeeded() -> Signal { return _internal_requestRecommendedApps(account: self.account, forceUpdate: false) } + + public func recommendedBots(peerId: EnginePeer.Id) -> Signal { + return _internal_recommendedBots(account: self.account, peerId: peerId) + } + public func requestRecommendedBots(peerId: EnginePeer.Id, forceUpdate: Bool = false) -> Signal { + return _internal_requestRecommendedBots(account: self.account, peerId: peerId, forceUpdate: forceUpdate) + } + public func isPremiumRequiredToContact(_ peerIds: [EnginePeer.Id]) -> Signal<[EnginePeer.Id], NoError> { return _internal_updateIsPremiumRequiredToContact(account: self.account, peerIds: peerIds) } diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 606d218025..4ae316e7b1 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -357,6 +357,16 @@ public final class ButtonComponent: Component { self.cornerRadius = cornerRadius self.isShimmering = isShimmering } + + public func withIsShimmering(_ isShimmering: Bool) -> Background { + return Background( + color: self.color, + foreground: self.foreground, + pressedColor: self.pressedColor, + cornerRadius: self.cornerRadius, + isShimmering: isShimmering + ) + } } public let background: Background diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index b6fb2a1f74..2662ca11d3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1847,6 +1847,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { var transferGiftImpl: (() -> Void)? var showAttributeInfoImpl: ((Any, Float) -> Void)? var upgradeGiftImpl: ((Int64?, Bool) -> Signal)? + var shareGiftImpl: (() -> Void)? var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)? var viewUpgradedImpl: ((EngineMessage.Id) -> Void)? @@ -2140,6 +2141,74 @@ public class GiftViewScreen: ViewControllerComponentContainer { } } + shareGiftImpl = { [weak self] in + guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else { + return + } + let link = "https://t.me/nft/\(gift.slug)" + let shareController = context.sharedContext.makeShareController( + context: context, + subject: .url(link), + forceExternal: false, + shareStory: shareStory, + enqueued: { peerIds, _ in + let _ = (context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + let peers = peerList.compactMap { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + self?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + openPeerImpl?(peer) + Queue.mainQueue().after(1.0) { + self?.dismiss(animated: false, completion: nil) + } + }) + } + return false + }, additionalView: nil), in: .current) + }) + }, + actionCompleted: { [weak self] in + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + ) + self.present(shareController, in: .window(.root)) + } + viewUpgradedImpl = { [weak self] messageId in guard let self, let navigationController = self.navigationController as? NavigationController else { return @@ -2178,13 +2247,10 @@ public class GiftViewScreen: ViewControllerComponentContainer { } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - //TODO:localize let link = "https://t.me/nft/\(gift.slug)" var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: nil) @@ -2198,28 +2264,16 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) - items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, _ in + }, action: { c, _ in c?.dismiss(completion: nil) - guard let self else { - return - } - let shareController = context.sharedContext.makeShareController( - context: context, - subject: .url(link), - forceExternal: false, - shareStory: shareStory, - actionCompleted: { [weak self] in - self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - ) - self.present(shareController, in: .window(.root)) + shareGiftImpl?() }))) if let _ = arguments.transferStars { - items.append(.action(ContextMenuActionItem(text: "Transfer", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_Transfer, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { c, _ in c?.dismiss(completion: nil) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index ca5592222b..05f3c0c9dc 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -88,6 +88,7 @@ public final class DrawingMessageRenderer { private let isOverlay: Bool private let isLink: Bool private let isGift: Bool + private let wallpaperColor: UIColor? private let messagesContainerNode: ASDisplayNode private var avatarHeaderNode: ListViewItemHeaderNode? @@ -99,7 +100,8 @@ public final class DrawingMessageRenderer { isNight: Bool = false, isOverlay: Bool = false, isLink: Bool = false, - isGift: Bool = false + isGift: Bool = false, + wallpaperColor: UIColor? = nil ) { self.context = context self.messages = messages @@ -107,6 +109,7 @@ public final class DrawingMessageRenderer { self.isOverlay = isOverlay self.isLink = isLink self.isGift = isGift + self.wallpaperColor = wallpaperColor self.messagesContainerNode = ASDisplayNode() self.messagesContainerNode.clipsToBounds = true @@ -169,14 +172,19 @@ public final class DrawingMessageRenderer { } } } + + var borderColor: UIColor? + if self.isGift && !self.isOverlay, let wallpaperColor = self.wallpaperColor { + borderColor = wallpaperColor.withMultiplied(hue: 1.0, saturation: 1.5, brightness: self.isNight ? 1.6 : 0.7).withAlphaComponent(0.6) + } - self.generate(size: size) { image in + self.generate(size: size, borderColor: borderColor) { image in completion(size, image, mediaRect) } }) } - private func generate(size: CGSize, completion: @escaping (UIImage) -> Void) { + private func generate(size: CGSize, borderColor: UIColor? = nil, completion: @escaping (UIImage) -> Void) { UIGraphicsBeginImageContextWithOptions(size, false, 3.0) self.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true) let img = UIGraphicsGetImageFromCurrentImageContext() @@ -184,6 +192,11 @@ public final class DrawingMessageRenderer { let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) + if let borderColor { + context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: 6.0, y: 12.0), size: CGSize(width: size.width - 6.0, height: size.height - 13.0)), cornerWidth: 70.0, cornerHeight: 70.0, transform: nil)) + context.setFillColor(borderColor.cgColor) + context.fillPath() + } if let cgImage = img?.cgImage { context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) } @@ -351,14 +364,16 @@ public final class DrawingMessageRenderer { messages: [Message], parentView: UIView, isLink: Bool = false, - isGift: Bool = false + isGift: Bool = false, + wallpaperDayColor: UIColor? = nil, + wallpaperNightColor: UIColor? = nil ) { self.context = context self.messages = messages - self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink, isGift: isGift) - self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink, isGift: isGift) - self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink, isGift: isGift) + self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink, isGift: isGift, wallpaperColor: wallpaperDayColor) + self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink, isGift: isGift, wallpaperColor: wallpaperNightColor) + self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink, isGift: isGift, wallpaperColor: nil) parentView.addSubview(self.dayContainerNode.view) parentView.addSubview(self.nightContainerNode.view) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 11bb771608..e4dc09a8c1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -308,7 +308,15 @@ public final class MediaEditor { return self.renderer.finalRenderedImage(mirror: mirror) } - private var wallpapers: ((day: UIImage, night: UIImage?))? + private var wallpapersValue: ((day: UIImage, night: UIImage?))? { + didSet { + self.wallpapersPromise.set(.single(self.wallpapersValue)) + } + } + private let wallpapersPromise = Promise<(day: UIImage, night: UIImage?)?>() + public var wallpapers: Signal<((day: UIImage, night: UIImage?))?, NoError> { + return self.wallpapersPromise.get() + } private struct PlaybackState: Equatable { let duration: Double @@ -871,10 +879,13 @@ public final class MediaEditor { let textureSource = UniversalTextureSource(renderTarget: renderTarget) - if case .message = self.self.subject { + switch self.subject { + case .message, .gift: if let image = textureSourceResult.image { - self.wallpapers = (image, textureSourceResult.nightImage ?? image) + self.wallpapersValue = (image, textureSourceResult.nightImage ?? image) } + default: + break } self.player = textureSourceResult.player @@ -1240,7 +1251,7 @@ public final class MediaEditor { return values.withUpdatedNightTheme(nightTheme) } - guard let (dayImage, nightImage) = self.wallpapers, let nightImage else { + guard let (dayImage, nightImage) = self.wallpapersValue, let nightImage else { return } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 6aab728739..cff7729ebe 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3388,53 +3388,83 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID messageFile = nil } - let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift) - renderer.render(completion: { result in - if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in - if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { - return true - } else { - return false - } - }) as? DrawingStickerEntityView { - existingEntityView.isNightTheme = isNightTheme - let messageEntity = existingEntityView.entity as! DrawingStickerEntity - messageEntity.renderImage = result.dayImage - messageEntity.secondaryRenderImage = result.nightImage - messageEntity.overlayRenderImage = result.overlayImage - existingEntityView.update(animated: false) - } else { - var content: DrawingStickerEntity.Content - var position: CGPoint - switch effectiveSubject { - case let .message(messageIds): - content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius) - position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0) - case let .gift(gift): - content = .gift(gift, result.size) - position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) - default: - fatalError() - } - - let messageEntity = DrawingStickerEntity(content: content) - messageEntity.renderImage = result.dayImage - messageEntity.secondaryRenderImage = result.nightImage - messageEntity.overlayRenderImage = result.overlayImage - messageEntity.referenceDrawingSize = storyDimensions - messageEntity.position = position - - let fraction = max(result.size.width, result.size.height) / 353.0 - messageEntity.scale = min(6.0, 3.3 * fraction) - - if let entityView = self.entitiesView.add(messageEntity, announce: false) as? DrawingStickerEntityView { - if isNightTheme { - entityView.isNightTheme = true + let wallpaperColors: Signal<(UIColor?, UIColor?), NoError> + if let subject = self.subject, case .gift = subject { + wallpaperColors = self.mediaEditorPromise.get() + |> mapToSignal { mediaEditor in + if let mediaEditor { + return mediaEditor.wallpapers + |> filter { + $0 != nil + } + |> take(1) + |> map { result in + if let (dayImage, nightImage) = result { + return (getAverageColor(image: dayImage), nightImage.flatMap { getAverageColor(image: $0) }) + } + return (nil, nil) } } + return .complete() } - - self.readyValue.set(.single(true)) + + } else { + wallpaperColors = .single((nil, nil)) + } + + let _ = (wallpaperColors + |> deliverOnMainQueue).start(next: { [weak self] wallpaperColors in + guard let self else { + return + } + let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift, wallpaperDayColor: wallpaperColors.0, wallpaperNightColor: wallpaperColors.1) + renderer.render(completion: { result in + if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in + if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { + return true + } else { + return false + } + }) as? DrawingStickerEntityView { + existingEntityView.isNightTheme = isNightTheme + let messageEntity = existingEntityView.entity as! DrawingStickerEntity + messageEntity.renderImage = result.dayImage + messageEntity.secondaryRenderImage = result.nightImage + messageEntity.overlayRenderImage = result.overlayImage + existingEntityView.update(animated: false) + } else { + var content: DrawingStickerEntity.Content + var position: CGPoint + switch effectiveSubject { + case let .message(messageIds): + content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius) + position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0) + case let .gift(gift): + content = .gift(gift, result.size) + position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) + default: + fatalError() + } + + let messageEntity = DrawingStickerEntity(content: content) + messageEntity.renderImage = result.dayImage + messageEntity.secondaryRenderImage = result.nightImage + messageEntity.overlayRenderImage = result.overlayImage + messageEntity.referenceDrawingSize = storyDimensions + messageEntity.position = position + + let fraction = max(result.size.width, result.size.height) / 353.0 + messageEntity.scale = min(6.0, 3.3 * fraction) + + if let entityView = self.entitiesView.add(messageEntity, announce: false) as? DrawingStickerEntityView { + if isNightTheme { + entityView.isNightTheme = true + } + } + } + + self.readyValue.set(.single(true)) + }) }) }) default: diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift index 785ce6231f..d07d0edd5d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -20,7 +20,8 @@ public enum PeerInfoPaneKey: Int32 { case links case gifs case groupsInCommon - case recommended + case similarChannels + case similarBots } public struct PeerInfoStatusData: Equatable { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift similarity index 73% rename from submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift rename to submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift index 2dd9c7b988..716cb16119 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift @@ -20,29 +20,29 @@ import Markdown import SolidRoundedButtonNode import PeerInfoPaneNode -private struct RecommendedChannelsListTransaction { +private struct RecommendedPeersListTransaction { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let animated: Bool } -private enum RecommendedChannelsListEntryStableId: Hashable { +private enum RecommendedPeersListEntryStableId: Hashable { case addMember case peer(PeerId) } -private enum RecommendedChannelsListEntry: Comparable, Identifiable { +private enum RecommendedPeersListEntry: Comparable, Identifiable { case peer(theme: PresentationTheme, index: Int, peer: EnginePeer, subscribers: Int32) - var stableId: RecommendedChannelsListEntryStableId { + var stableId: RecommendedPeersListEntryStableId { switch self { case let .peer(_, _, peer, _): return .peer(peer.id) } } - static func ==(lhs: RecommendedChannelsListEntry, rhs: RecommendedChannelsListEntry) -> Bool { + static func ==(lhs: RecommendedPeersListEntry, rhs: RecommendedPeersListEntry) -> Bool { switch lhs { case let .peer(lhsTheme, lhsIndex, lhsPeer, lhsSubscribers): if case let .peer(rhsTheme, rhsIndex, rhsPeer, rhsSubscribers) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsPeer == rhsPeer, lhsSubscribers == rhsSubscribers { @@ -53,7 +53,7 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable { } } - static func <(lhs: RecommendedChannelsListEntry, rhs: RecommendedChannelsListEntry) -> Bool { + static func <(lhs: RecommendedPeersListEntry, rhs: RecommendedPeersListEntry) -> Bool { switch lhs { case let .peer(_, lhsIndex, _, _): switch rhs { @@ -66,8 +66,15 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable { func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem { switch self { case let .peer(_, _, peer, subscribers): - let subtitle = presentationData.strings.Conversation_StatusSubscribers(subscribers) - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: nil, text: .text(subtitle, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { + let text: ItemListPeerItemText + if subscribers > 0 { + text = .text(presentationData.strings.Conversation_StatusSubscribers(subscribers), .secondary) + } else if let addressName = peer.addressName { + text = .text("@\(addressName)", .secondary) + } else { + text = .none + } + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { action(peer) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in @@ -78,19 +85,29 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable { } } -private func preparedTransition(from fromEntries: [RecommendedChannelsListEntry], to toEntries: [RecommendedChannelsListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedChannelsListTransaction { +private func preparedTransition(from fromEntries: [RecommendedPeersListEntry], to toEntries: [RecommendedPeersListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedPeersListTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) } - return RecommendedChannelsListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count) + return RecommendedPeersListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count) } -private let channelsLimit: Int32 = 8 +private protocol RecommendedPeers { + +} -final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode { +extension RecommendedChannels: RecommendedPeers { + +} + +extension RecommendedBots: RecommendedPeers { + +} + +final class PeerInfoRecommendedPeersPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext private let chatControllerInteraction: ChatControllerInteraction private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void @@ -98,9 +115,9 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode weak var parentController: ViewController? private let listNode: ListView - private var currentEntries: [RecommendedChannelsListEntry] = [] - private var currentState: (RecommendedChannels?, Bool)? - private var enqueuedTransactions: [RecommendedChannelsListTransaction] = [] + private var currentEntries: [RecommendedPeersListEntry] = [] + private var enqueuedTransactions: [RecommendedPeersListTransaction] = [] + private var currentState: (RecommendedPeers?, Bool)? private var unlockBackground: UIImageView? private var unlockText: ComponentView? @@ -145,32 +162,35 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.listNode.preloadPages = true self.addSubnode(self.listNode) + let signal: Signal + if peerId.namespace == Namespaces.Peer.CloudUser { + signal = context.engine.peers.recommendedBots(peerId: peerId) + |> map { + $0 as RecommendedPeers? + } + } else { + signal = context.engine.peers.recommendedChannels(peerId: peerId) + |> map { + $0 as RecommendedPeers? + } + } + self.disposable = (combineLatest(queue: .mainQueue(), self.presentationDataPromise.get(), - context.engine.peers.recommendedChannels(peerId: peerId), + signal, context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in return peer?.isPremium ?? false } ) - |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recommendedChannels, isPremium in - guard let strongSelf = self else { + |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recommendedPeers, isPremium in + guard let self else { return } - strongSelf.currentState = (recommendedChannels, isPremium) - strongSelf.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) + self.currentState = (recommendedPeers, isPremium) + self.updateState(recommendedPeers: recommendedPeers, isPremium: isPremium, presentationData: presentationData) }) - - self.statusPromise.set(context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId) - ) - |> map { count -> PeerInfoStatusData? in - if let count { - return PeerInfoStatusData(text: presentationData.strings.Conversation_StatusSubscribers(Int32(count)), isActivity: true, key: .recommended) - } - return nil - }) - + self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in if let self { self.layoutUnlockPanel(transition: .animated(duration: 0.4, curve: .spring)) @@ -215,8 +235,8 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.listNode.scrollEnabled = !isScrollingLockedAtTop - if isFirstLayout, let (recommendedChannels, isPremium) = self.currentState { - self.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) + if isFirstLayout, let (recommendedPeers, isPremium) = self.currentState { + self.updateState(recommendedPeers: recommendedPeers, isPremium: isPremium, presentationData: presentationData) } } @@ -225,8 +245,16 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.chatControllerInteraction.navigationController()?.pushViewController(controller) } + private func updateState(recommendedPeers: RecommendedPeers?, isPremium: Bool, presentationData: PresentationData) { + if let recommendedChannels = recommendedPeers as? RecommendedChannels { + self.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) + } else if let recommendedBots = recommendedPeers as? RecommendedBots { + self.updateState(recommendedBots: recommendedBots, isPremium: isPremium, presentationData: presentationData) + } + } + private func updateState(recommendedChannels: RecommendedChannels?, isPremium: Bool, presentationData: PresentationData) { - var entries: [RecommendedChannelsListEntry] = [] + var entries: [RecommendedPeersListEntry] = [] if let channels = recommendedChannels?.channels { for channel in channels { @@ -243,6 +271,42 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.currentEntries = entries self.enqueuedTransactions.append(transaction) self.dequeueTransaction() + + if let recommendedChannels { + self.statusPromise.set(.single( + PeerInfoStatusData(text: presentationData.strings.SharedMedia_SimilarChannelCount(recommendedChannels.count), isActivity: true, key: .similarChannels) + )) + } + } + + private func updateState(recommendedBots: RecommendedBots?, isPremium: Bool, presentationData: PresentationData) { + var entries: [RecommendedPeersListEntry] = [] + + if let bots = recommendedBots?.bots { + for bot in bots { + var subscriberCount: Int32 = 0 + if case let .user(user) = bot { + subscriberCount = user.subscriberCount ?? 0 + } + entries.append(.peer(theme: presentationData.theme, index: entries.count, peer: bot, subscribers: subscriberCount)) + } + } + + let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, action: { [weak self] peer in + self?.chatControllerInteraction.openPeer(peer, .info(nil), nil, .default) + }, openPeerContextAction: { [weak self] peer, node, gesture in + self?.openPeerContextAction(true, peer, node, gesture) + }) + + self.currentEntries = entries + self.enqueuedTransactions.append(transaction) + self.dequeueTransaction() + + if let recommendedBots { + self.statusPromise.set(.single( + PeerInfoStatusData(text: presentationData.strings.SharedMedia_SimilarBotCount(recommendedBots.count), isActivity: true, key: .similarBots) + )) + } } private func layoutUnlockPanel(transition: ContainedViewLayoutTransition) { @@ -278,6 +342,11 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.view.addSubview(unlockBackground) self.unlockBackground = unlockBackground } + + var isBots = false + if let (state, _) = self.currentState, state is RecommendedBots { + isBots = true + } if let current = self.unlockButton { unlockButton = current @@ -289,7 +358,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode unlockButton.animationLoopTime = 2.5 unlockButton.animation = "premium_unlock" unlockButton.iconPosition = .right - unlockButton.title = presentationData.strings.Channel_SimilarChannels_ShowMore + unlockButton.title = isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMore : presentationData.strings.Channel_SimilarChannels_ShowMore unlockButton.pressed = { [weak self] in self?.unlockPressed() @@ -320,7 +389,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .markdown(text: presentationData.strings.Channel_SimilarChannels_ShowMoreInfo, attributes: markdownAttributes), + text: .markdown(text: isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMoreInfo : presentationData.strings.Channel_SimilarChannels_ShowMoreInfo, attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 62840ba730..0e68c22929 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1008,6 +1008,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen groupsInCommon = nil } + let recommendedBots: Signal + if case .bot = kind { + recommendedBots = context.engine.peers.recommendedBots(peerId: userPeerId) + } else { + recommendedBots = .single(nil) + } + let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError> let profileGiftsContext: ProfileGiftsContext? if case .user = kind { @@ -1309,6 +1316,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen status, hasStories, hasStoryArchive, + recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, @@ -1322,7 +1330,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen premiumGiftOptions, webAppPermissions ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1373,6 +1381,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes?.insert(.botPreview, at: 0) } } + + if let recommendedBots, recommendedBots.count > 0 { + availablePanes?.append(.similarBots) + } } else { availablePanes = nil } @@ -1574,7 +1586,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes?.insert(.stories, at: 0) } if let recommendedChannels, !recommendedChannels.channels.isEmpty { - availablePanes?.append(.recommended) + availablePanes?.append(.similarChannels) } if case .peer = chatLocation { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 940bc2c5b9..bbc655fc54 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -545,8 +545,8 @@ private final class PeerInfoPendingPane { } else { preconditionFailure() } - case .recommended: - paneNode = PeerInfoRecommendedChannelsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction) + case .similarChannels, .similarBots: + paneNode = PeerInfoRecommendedPeersPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction) case .savedMessagesChats: paneNode = PeerInfoChatListPaneNode(context: context, navigationController: chatControllerInteraction.navigationController) case .savedMessages: @@ -1201,8 +1201,10 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat title = presentationData.strings.PeerInfo_PaneGroups case .members: title = presentationData.strings.PeerInfo_PaneMembers - case .recommended: + case .similarChannels: title = presentationData.strings.PeerInfo_PaneRecommended + case .similarBots: + title = presentationData.strings.PeerInfo_PaneRecommendedBots case .savedMessagesChats: title = presentationData.strings.DialogList_TabTitle case .savedMessages: diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 8d5aa05bef..33d0fe32c0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -4824,6 +4824,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) } + if peerId.namespace == Namespaces.Peer.CloudUser { + let _ = context.engine.peers.requestRecommendedBots(peerId: peerId, forceUpdate: true).startStandalone() + } + if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudUser { self.storiesReady.set(false) let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId) @@ -12641,7 +12645,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc override public func loadDisplayNode() { var initialPaneKey: PeerInfoPaneKey? if self.switchToRecommendedChannels { - initialPaneKey = .recommended + initialPaneKey = .similarChannels } else if self.switchToGifts { initialPaneKey = .gifts } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 735ab2686a..1a9ba0f76a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -249,6 +249,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return .never() } return self.profileGifts.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo) + }, + shareStory: { [weak self] in + guard let self, case let .unique(uniqueGift) = product.gift, let parentController = self.parentController else { + return + } + Queue.mainQueue().after(0.15) { + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: parentController) + parentController.push(controller) + } } ) self.parentController?.push(controller) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift deleted file mode 100644 index 245bab70ab..0000000000 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Foundation -import UIKit -import Postbox -import SwiftSignalKit -import Display -import AsyncDisplayKit -import TelegramCore -import SafariServices -import MobileCoreServices -import Intents -import LegacyComponents -import TelegramPresentationData -import TelegramUIPreferences -import DeviceAccess -import TextFormat -import TelegramBaseController -import AccountContext -import TelegramStringFormatting -import PresentationDataUtils -import UndoUI -import PeerInfoUI -import AppBundle -import LocalizedPeerData -import ChatInterfaceState -import ChatControllerInteraction -import StoryContainerScreen -import SaveToCameraRoll -import MediaEditorScreen - -enum StorySharingSubject { - case messages([Message]) - case gift(StarGift.UniqueGift) -} - -extension ChatControllerImpl { - func openStorySharing(subject: StorySharingSubject) { - let context = self.context - let editorSubject: Signal - switch subject { - case let .messages(messages): - editorSubject = .single(.message(messages.map { $0.id })) - case let .gift(gift): - editorSubject = .single(.gift(gift)) - } - - let externalState = MediaEditorTransitionOutExternalState( - storyTarget: nil, - isForcedTarget: false, - isPeerArchived: false, - transitionOut: nil - ) - - let controller = MediaEditorScreenImpl( - context: context, - mode: .storyEditor, - subject: editorSubject, - transitionIn: nil, - transitionOut: { _, _ in - return nil - }, - completion: { [weak self] result, commit in - guard let self else { - return - } - 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 - } - externalState.storyTarget = target - - if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) - } - - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - let text: String - if case .channel = peer { - text = self.presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string - } else { - text = self.presentationData.strings.Story_MessageReposted_Personal - } - Queue.mainQueue().after(0.25) { - self.present(UndoOverlayController( - presentationData: self.presentationData, - content: .forward(savedMessages: false, text: text), - elevatedLayout: false, - action: { _ in return false } - ), in: .current) - - Queue.mainQueue().after(0.1) { - self.chatDisplayNode.hapticFeedback.success() - } - } - }) - } - ) - self.push(controller) - } -} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 74bd08d89b..43ada87e50 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1201,7 +1201,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = strongSelf.context.sharedContext.makeGiftViewScreen(context: strongSelf.context, message: EngineMessage(message), shareStory: { [weak self] in if let self, case let .starGiftUnique(gift, _, _, _, _, _, _) = action.action, case let .unique(uniqueGift) = gift { Queue.mainQueue().after(0.15) { - self.openStorySharing(subject: .gift(uniqueGift)) + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: self) + self.push(controller) } } }) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift index 6954dd1633..e284e26d67 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift @@ -134,7 +134,8 @@ extension ChatControllerImpl { return } Queue.mainQueue().after(0.15) { - self.openStorySharing(subject: .messages(messages)) + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .messages(messages), parentController: self) + self.push(controller) } } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 67b5531abc..363ce4a91d 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2821,12 +2821,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto for entry in historyView.filteredEntries { switch entry { case let .MessageEntry(message, _, _, _, _, _): - var hasAction = false - for media in message.media { - if let _ = media as? TelegramMediaAction { - hasAction = true - } - } if let _ = message.inlineBotAttribute { if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId { if visibleBusinessBotMessageIdValue < message.id { @@ -2836,22 +2830,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto visibleBusinessBotMessageId = message.id } } - if !hasAction { - switch message.id.peerId.namespace { - case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: - messageIdsWithPossibleReactions.append(message.id) - default: - break - } + switch message.id.peerId.namespace { + case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: + messageIdsWithPossibleReactions.append(message.id) + default: + break } case let .MessageGroupEntry(_, messages, _): for (message, _, _, _, _) in messages { - var hasAction = false - for media in message.media { - if let _ = media as? TelegramMediaAction { - hasAction = true - } - } if let _ = message.inlineBotAttribute { if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId { if visibleBusinessBotMessageIdValue < message.id { @@ -2861,13 +2847,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto visibleBusinessBotMessageId = message.id } } - if !hasAction { - switch message.id.peerId.namespace { - case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: - messageIdsWithPossibleReactions.append(message.id) - default: - break - } + switch message.id.peerId.namespace { + case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: + messageIdsWithPossibleReactions.append(message.id) + default: + break } } default: diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 5f758642c5..615db1d433 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -17,7 +17,7 @@ import AccountContext private enum ChatReportPeerTitleButton: Equatable { case block - case addContact(String?) + case addContact(String?, Bool) case shareMyPhoneNumber case reportSpam case reportUserSpam @@ -30,11 +30,15 @@ private enum ChatReportPeerTitleButton: Equatable { switch self { case .block: return strings.Conversation_BlockUser - case let .addContact(name): + case let .addContact(name, long): if let name = name { return strings.Conversation_AddNameToContacts(name).string } else { - return strings.Conversation_AddToContacts + if long { + return strings.Conversation_AddToContactsLong + } else { + return strings.Conversation_AddToContacts + } } case .shareMyPhoneNumber: return strings.Conversation_ShareMyPhoneNumber @@ -76,9 +80,9 @@ private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReport } } if buttons.isEmpty, let phone = peer.phone, !phone.isEmpty { - buttons.append(.addContact(EnginePeer(peer).compactDisplayTitle)) + buttons.append(.addContact(EnginePeer(peer).compactDisplayTitle, buttons.isEmpty)) } else { - buttons.append(.addContact(nil)) + buttons.append(.addContact(nil, buttons.isEmpty)) } } else { if peerStatusSettings.contains(.canBlock) || peerStatusSettings.contains(.canReport) { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index f5f17f6630..b16eeaf750 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2931,6 +2931,75 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftViewScreen(context: context, subject: .uniqueGift(gift), shareStory: shareStory) } + public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController { + let editorSubject: Signal + switch subject { + case let .messages(messages): + editorSubject = .single(.message(messages.map { $0.id })) + case let .gift(gift): + editorSubject = .single(.gift(gift)) + } + + let externalState = MediaEditorTransitionOutExternalState( + storyTarget: nil, + isForcedTarget: false, + isPeerArchived: false, + transitionOut: nil + ) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = MediaEditorScreenImpl( + context: context, + mode: .storyEditor, + subject: editorSubject, + transitionIn: nil, + transitionOut: { _, _ in + return nil + }, + completion: { [weak parentController] 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 = context.account.peerId + } + externalState.storyTarget = target + + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + } + + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + let text: String + if case .channel = peer { + text = presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string + } else { + text = presentationData.strings.Story_MessageReposted_Personal + } + Queue.mainQueue().after(0.25) { + parentController?.present(UndoOverlayController( + presentationData: presentationData, + content: .forward(savedMessages: false, text: text), + elevatedLayout: false, + action: { _ in return false } + ), in: .current) + + Queue.mainQueue().after(0.1) { + HapticFeedback().success() + } + } + }) + } + ) + return controller + } + public func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) { let _ = (context.engine.messages.reportContent(subject: subject, option: nil, message: nil) |> deliverOnMainQueue).startStandalone(next: { result in @@ -2940,9 +3009,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } - public func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, actionCompleted: (() -> Void)?) -> ViewController { + public func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController { let controller = ShareController(context: context, subject: subject, externalShare: forceExternal) controller.shareStory = shareStory + controller.enqueued = enqueued controller.actionCompleted = actionCompleted return controller }