diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 40448d0518..d99c106308 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3365,7 +3365,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate timeout: privacy.timeout, mentions: mentions, stateContext: stateContext, - completion: { [weak self] privacy, allowScreenshots, pin in + completion: { [weak self] privacy, allowScreenshots, pin, _ in guard let self else { return } @@ -3408,12 +3408,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate allowScreenshots: !isForwardingDisabled, pin: pin, stateContext: stateContext, - completion: { [weak self] result, isForwardingDisabled, pin in + completion: { [weak self] result, isForwardingDisabled, pin, peers in guard let self else { return } if case .closeFriends = privacy.base { let _ = self.context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start() + self.closeFriends.set(.single(peers)) completion(EngineStoryPrivacy(base: .closeFriends, additionallyIncludePeers: [])) } else { completion(result) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 60d796f143..2b5efec6ca 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -35,7 +35,7 @@ final class ShareWithPeersScreenComponent: Component { let mentions: [String] let categoryItems: [CategoryItem] let optionItems: [OptionItem] - let completion: (EngineStoryPrivacy, Bool, Bool) -> Void + let completion: (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void let editCategory: (EngineStoryPrivacy, Bool, Bool) -> Void init( @@ -48,7 +48,7 @@ final class ShareWithPeersScreenComponent: Component { mentions: [String], categoryItems: [CategoryItem], optionItems: [OptionItem], - completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, + completion: @escaping (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void, editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void ) { self.context = context @@ -1552,7 +1552,8 @@ final class ShareWithPeersScreenComponent: Component { additionallyIncludePeers: self.selectedPeers ), self.selectedOptions.contains(.screenshot), - self.selectedOptions.contains(.pin) + self.selectedOptions.contains(.pin), + self.component?.stateContext.stateValue?.peers.filter { self.selectedPeers.contains($0.id) } ?? [] ) controller.dismissAllTooltips() @@ -1918,9 +1919,13 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { case let .search(query, onlyContacts): let signal: Signal<[EngineRenderedPeer], NoError> if onlyContacts { - signal = context.engine.contacts.searchContacts(query: query) - |> map { result in - return result.0.map { EngineRenderedPeer(peer: $0) } + signal = combineLatest( + context.engine.contacts.searchLocalPeers(query: query), + context.engine.contacts.searchContacts(query: query) + ) + |> map { peers, contacts in + let contactIds = Set(contacts.0.map { $0.id }) + return peers.filter { contactIds.contains($0.peerId) } } } else { signal = context.engine.contacts.searchLocalPeers(query: query) @@ -1975,7 +1980,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { timeout: Int = 0, mentions: [String] = [], stateContext: StateContext, - completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, + completion: @escaping (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void, editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void ) { self.context = context diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 27a75dd07a..ea8b389c05 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1425,7 +1425,7 @@ private final class StoryContainerScreenComponent: Component { self.state?.updated(transition: .immediate) }, keyboardInputData: self.inputMediaNodeDataPromise.get(), - closeFriends: self.closeFriendsPromise.get(), + closeFriends: self.closeFriendsPromise, sharedViewListsContext: self.sharedViewListsContext )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index d5dab97d6a..db8415139c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -111,7 +111,7 @@ public final class StoryItemSetContainerComponent: Component { public let controller: () -> ViewController? public let toggleAmbientMode: () -> Void public let keyboardInputData: Signal - public let closeFriends: Signal<[EnginePeer], NoError> + public let closeFriends: Promise<[EnginePeer]> let sharedViewListsContext: StoryItemSetViewListComponent.SharedListsContext init( @@ -144,7 +144,7 @@ public final class StoryItemSetContainerComponent: Component { controller: @escaping () -> ViewController?, toggleAmbientMode: @escaping () -> Void, keyboardInputData: Signal, - closeFriends: Signal<[EnginePeer], NoError>, + closeFriends: Promise<[EnginePeer]>, sharedViewListsContext: StoryItemSetViewListComponent.SharedListsContext ) { self.context = context @@ -3202,7 +3202,7 @@ public final class StoryItemSetContainerComponent: Component { context: context, subject: .stories(editing: true), initialPeerIds: Set(privacy.additionallyIncludePeers), - closeFriends: component.closeFriends + closeFriends: component.closeFriends.get() ) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { @@ -3212,7 +3212,7 @@ public final class StoryItemSetContainerComponent: Component { context: context, initialPrivacy: privacy, stateContext: stateContext, - completion: { [weak self] privacy, _, _ in + completion: { [weak self] privacy, _, _, _ in guard let self, let component = self.component else { return } @@ -3262,9 +3262,12 @@ public final class StoryItemSetContainerComponent: Component { context: context, initialPrivacy: privacy, stateContext: stateContext, - completion: { result, _, _ in + completion: { [weak self] result, _, _, peers in if case .closeFriends = privacy.base { let _ = context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start() + if let component = self?.component { + component.closeFriends.set(.single(peers)) + } completion(EngineStoryPrivacy(base: .closeFriends, additionallyIncludePeers: [])) } else { completion(result) @@ -3632,6 +3635,132 @@ public final class StoryItemSetContainerComponent: Component { } } + private func getLinkedStickerPacks() -> (ContextController.Tip?, Signal?) { + guard let component = self.component else { + return (nil, nil) + } + var tip: ContextController.Tip? + var tipSignal: Signal? + + var hasLinkedStickers = false + let media = component.slice.item.storyItem.media._asMedia() + if let image = media as? TelegramMediaImage { + hasLinkedStickers = image.flags.contains(.hasStickers) + } else if let file = media as? TelegramMediaFile { + hasLinkedStickers = file.hasLinkedStickers + } + + var emojiFileIds: [Int64] = [] + for entity in component.slice.item.storyItem.entities { + if case let .CustomEmoji(_, fileId) = entity.type { + emojiFileIds.append(fileId) + } + } + + if !emojiFileIds.isEmpty || hasLinkedStickers, let peerReference = PeerReference(component.slice.peer._asPeer()) { + let context = component.context + + tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil) + + let packsPromise = Promise<[StickerPackReference]>() + if hasLinkedStickers { + packsPromise.set(context.engine.stickers.stickerPacksAttachedToMedia(media: .story(peer: peerReference, id: component.slice.item.storyItem.id, media: media))) + } else { + packsPromise.set(.single([])) + } + + let customEmojiPacksPromise = Promise<[StickerPackReference]>() + if !emojiFileIds.isEmpty { + customEmojiPacksPromise.set( context.engine.stickers.resolveInlineStickers(fileIds: emojiFileIds) + |> map { files -> [StickerPackReference] in + var packReferences: [StickerPackReference] = [] + var existingIds = Set() + for (_, file) in files { + loop: for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { + if case let .id(id, _) = packReference, !existingIds.contains(id) { + packReferences.append(packReference) + existingIds.insert(id) + } + break loop + } + } + } + return packReferences + }) + } else { + customEmojiPacksPromise.set(.single([])) + } + + let action: () -> Void = { [weak self] in + if let self { + let combinedPacks = combineLatest(packsPromise.get(), customEmojiPacksPromise.get()) + |> map { embeddedPackReferences, customEmojiPackReferences in + var packReferences: [StickerPackReference] = [] + for packReference in embeddedPackReferences { + packReferences.append(packReference) + } + for packReference in customEmojiPackReferences { + if !packReferences.contains(packReference) { + packReferences.append(packReference) + } + } + return packReferences + } + self.sendMessageContext.openAttachedStickers(view: self, packs: combinedPacks |> take(1)) + } + } + tipSignal = combineLatest(packsPromise.get(), customEmojiPacksPromise.get()) + |> mapToSignal { embeddedPackReferences, customEmojiPackReferences -> Signal in + var packReferences: [StickerPackReference] = [] + for packReference in embeddedPackReferences { + packReferences.append(packReference) + } + for packReference in customEmojiPackReferences { + if !packReferences.contains(packReference) { + packReferences.append(packReference) + } + } + if packReferences.count > 1 { + let valueText = component.strings.Story_Context_EmbeddedStickersValue(Int32(packReferences.count)) + return .single(.animatedEmoji(text: component.strings.Story_Context_EmbeddedStickers(valueText).string, arguments: nil, file: nil, action: action)) + } else if let reference = packReferences.first { + return context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) + |> filter { result in + if case .result = result { + return true + } else { + return false + } + } + |> mapToSignal { result -> Signal in + if case let .result(info, items, _) = result { + let isEmoji = info.flags.contains(.isEmoji) + let tip: ContextController.Tip = .animatedEmoji( + text: isEmoji ? component.strings.Story_Context_EmbeddedEmojiPack(info.title).string : component.strings.Story_Context_EmbeddedStickerPack(info.title).string, + arguments: TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: .clear, + attemptSynchronous: true + ), + file: items.first?.file, + action: action) + return .single(tip) + } else { + return .complete() + } + } + } else { + return .complete() + } + } + } + + return (tip, tipSignal) + } + private func performMyMoreAction(sourceView: UIView, gesture: ContextGesture?) { guard let component = self.component, let controller = component.controller() else { return @@ -3770,66 +3899,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - var hasLinkedStickers = false - let media = component.slice.item.storyItem.media._asMedia() - if let image = media as? TelegramMediaImage { - hasLinkedStickers = image.flags.contains(.hasStickers) - } else if let file = media as? TelegramMediaFile { - hasLinkedStickers = file.hasLinkedStickers - } - - var tip: ContextController.Tip? - var tipSignal: Signal? - if hasLinkedStickers, let peerReference = PeerReference(component.slice.peer._asPeer()) { - let context = component.context - tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil) - - let packsPromise = Promise<[StickerPackReference]>() - packsPromise.set(context.engine.stickers.stickerPacksAttachedToMedia(media: .story(peer: peerReference, id: component.slice.item.storyItem.id, media: media))) - - let action: () -> Void = { [weak self] in - if let self { - self.sendMessageContext.openAttachedStickers(view: self, packs: packsPromise.get() |> take(1)) - } - } - tipSignal = packsPromise.get() - |> mapToSignal { packReferences -> Signal in - if packReferences.count > 1 { - let valueText = component.strings.Story_Context_EmbeddedStickersValue(Int32(packReferences.count)) - return .single(.animatedEmoji(text: component.strings.Story_Context_EmbeddedStickers(valueText).string, arguments: nil, file: nil, action: action)) - } else if let reference = packReferences.first { - return context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) - |> filter { result in - if case .result = result { - return true - } else { - return false - } - } - |> mapToSignal { result -> Signal in - if case let .result(info, items, _) = result { - let isEmoji = info.flags.contains(.isEmoji) - let tip: ContextController.Tip = .animatedEmoji( - text: isEmoji ? component.strings.Story_Context_EmbeddedEmojiPack(info.title).string : component.strings.Story_Context_EmbeddedStickerPack(info.title).string, - arguments: TextNodeWithEntities.Arguments( - context: context, - cache: context.animationCache, - renderer: context.animationRenderer, - placeholderColor: .clear, - attemptSynchronous: true - ), - file: items.first?.file, - action: action) - return .single(tip) - } else { - return .complete() - } - } - } else { - return .complete() - } - } - } + let (tip, tipSignal) = getLinkedStickerPacks() let contextItems = ContextController.Items(content: .list(items), tip: tip, tipSignal: tipSignal) @@ -4039,66 +4109,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - var hasLinkedStickers = false - let media = component.slice.item.storyItem.media._asMedia() - if let image = media as? TelegramMediaImage { - hasLinkedStickers = image.flags.contains(.hasStickers) - } else if let file = media as? TelegramMediaFile { - hasLinkedStickers = file.hasLinkedStickers - } - - var tip: ContextController.Tip? - var tipSignal: Signal? - if hasLinkedStickers { - let context = component.context - tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil) - - let packsPromise = Promise<[StickerPackReference]>() - packsPromise.set(context.engine.stickers.stickerPacksAttachedToMedia(media: .standalone(media: media))) - - let action: () -> Void = { [weak self] in - if let self { - self.sendMessageContext.openAttachedStickers(view: self, packs: packsPromise.get() |> take(1)) - } - } - tipSignal = packsPromise.get() - |> mapToSignal { packReferences -> Signal in - if packReferences.count > 1 { - let valueText = component.strings.Story_Context_EmbeddedStickersValue(Int32(packReferences.count)) - return .single(.animatedEmoji(text: component.strings.Story_Context_EmbeddedStickers(valueText).string, arguments: nil, file: nil, action: action)) - } else if let reference = packReferences.first { - return context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) - |> filter { result in - if case .result = result { - return true - } else { - return false - } - } - |> mapToSignal { result -> Signal in - if case let .result(info, items, _) = result { - let isEmoji = info.flags.contains(.isEmoji) - let tip: ContextController.Tip = .animatedEmoji( - text: isEmoji ? component.strings.Story_Context_EmbeddedEmojiPack(info.title).string : component.strings.Story_Context_EmbeddedStickerPack(info.title).string, - arguments: TextNodeWithEntities.Arguments( - context: context, - cache: context.animationCache, - renderer: context.animationRenderer, - placeholderColor: .clear, - attemptSynchronous: true - ), - file: items.first?.file, - action: action) - return .single(tip) - } else { - return .complete() - } - } - } else { - return .complete() - } - } - } + let (tip, tipSignal) = getLinkedStickerPacks() let contextItems = ContextController.Items(content: .list(items), tip: tip, tipSignal: tipSignal) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 423398aa34..47bbd32d0b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -2466,7 +2466,7 @@ final class StoryItemSetContainerSendMessage { }) } - func openPeerMention(view: StoryItemSetContainerComponent.View, name: String, navigation: ChatControllerInteractionNavigateToPeer = .info, sourceMessageId: MessageId? = nil) { + func openPeerMention(view: StoryItemSetContainerComponent.View, name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) { guard let component = view.component, let parentController = component.controller() else { return } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryPrivacyIconComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryPrivacyIconComponent.swift new file mode 100644 index 0000000000..a00d413ed4 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryPrivacyIconComponent.swift @@ -0,0 +1,101 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AccountContext +import TelegramCore +import AsyncDisplayKit +import AvatarNode + +final class StoryPrivacyIconComponent: Component { + enum Privacy { + case everyone + case closeFriends + case contacts + case selectedContacts + } + let privacy: Privacy + let isEditable: Bool + + init(privacy: Privacy, isEditable: Bool) { + self.privacy = privacy + self.isEditable = isEditable + } + + static func ==(lhs: StoryPrivacyIconComponent, rhs: StoryPrivacyIconComponent) -> Bool { + if lhs.privacy != rhs.privacy { + return false + } + if lhs.isEditable != rhs.isEditable { + return false + } + return true + } + + final class View: UIImageView { + private var component: StoryPrivacyIconComponent? + private weak var state: EmptyComponentState? + + func update(component: StoryPrivacyIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let size = CGSize(width: component.isEditable ? 40.0 : 24.0, height: 24.0) + self.image = generateImage(size, rotatedContext: { size, context in + let path: CGPath + if size.width == size.height { + path = CGPath(ellipseIn: CGRect(origin: .zero, size: size), transform: nil) + } else { + path = CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: size.height / 2.0, cornerHeight: size.height / 2.0, transform: nil) + } + + context.addPath(path) + context.clip() + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] + let icon: UIImage + + switch component.privacy { + case .everyone: + colors = [UIColor(rgb: 0x4faaff).cgColor, UIColor(rgb: 0x017aff).cgColor] + icon = UIImage() + case .closeFriends: + colors = [UIColor(rgb: 0x87d93a).cgColor, UIColor(rgb: 0x31b73b).cgColor] + icon = UIImage() + case .contacts: + colors = [UIColor(rgb: 0xc36eff).cgColor, UIColor(rgb: 0x8c61fa).cgColor] + icon = UIImage() + case .selectedContacts: + colors = [UIColor(rgb: 0xffb643).cgColor, UIColor(rgb: 0xf69a36).cgColor] + icon = UIImage() + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + if let cgImage = icon.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: icon.size)) + } + + if component.isEditable { + let arrowIcon = UIImage() + if let cgImage = arrowIcon.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: icon.size)) + } + } + }) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +}