diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 32917f52d8..ed93228fe3 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -795,7 +795,7 @@ public struct StoryCameraTransitionInCoordinator { public protocol TelegramRootControllerInterface: NavigationController { @discardableResult - func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? + func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? func getContactsController() -> ViewController? func getChatsController() -> ViewController? diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 4d7e2e1cad..3bdc2641ce 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1366,9 +1366,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } + if let storyPeerListView = self.chatListHeaderView()?.storyPeerListView() { + storyPeerListView.cancelLoadingItem() + } + switch subject { case .archive: - StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode) + StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode, sharedProgressDisposable: self.sharedOpenStoryProgressDisposable) case let .peer(peerId): StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode, sharedProgressDisposable: self.sharedOpenStoryProgressDisposable) } @@ -2487,6 +2491,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.fullScreenEffectView = nil fullScreenEffectView.removeFromSuperview() } + + self.sharedOpenStoryProgressDisposable.set(nil) + + if let storyPeerListView = self.chatListHeaderView()?.storyPeerListView() { + storyPeerListView.cancelLoadingItem() + } } func updateHeaderContent() -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) { @@ -2692,39 +2702,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - let coordinator = rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: { [weak self] target in - guard let self, let target else { - return nil - } - if let componentView = self.chatListHeaderView() { - let peerId: EnginePeer.Id - switch target { - case .myStories: - peerId = self.context.account.peerId - case let .peer(id): - peerId = id - } - - if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { - return StoryCameraTransitionOut( - destinationView: transitionView, - destinationRect: transitionView.bounds, - destinationCornerRadius: transitionView.bounds.height * 0.5 - ) - } else if let rightButtonView = componentView.rightButtonViews["story"] { - return StoryCameraTransitionOut( - destinationView: rightButtonView, - destinationRect: rightButtonView.bounds, - destinationCornerRadius: rightButtonView.bounds.height * 0.5 - ) - } - } - return nil - }) + let coordinator = rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut()) coordinator?.animateIn() } } + public func storyCameraTransitionOut() -> (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut? { + return { [weak self] target, isArchived in + guard let self, let target else { + return nil + } + if let componentView = self.chatListHeaderView() { + let peerId: EnginePeer.Id + switch target { + case .myStories: + peerId = self.context.account.peerId + case let .peer(id): + peerId = id + } + + if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { + return StoryCameraTransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: transitionView.bounds.height * 0.5 + ) + } + } + return nil + } + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -3947,6 +3955,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } if let componentView = self.chatListHeaderView() { + self.sharedOpenStoryProgressDisposable.set(nil) componentView.storyPeerListView()?.setLoadingItem(peerId: peerId, signal: signal) } } @@ -5667,7 +5676,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let current = self.storyCameraTransitionInCoordinator { coordinator = current } else { - coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] target in + coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] target, _ in guard let self, let target else { return nil } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index cbca8f1345..328280f9d4 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -260,6 +260,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var isLeftAligned: Bool = true private var itemLayout: ItemLayout? + public var centerAligned: Bool { + return self.validLayout?.4 ?? false + } + private var customReactionSource: (view: UIView, rect: CGRect, layer: CALayer, item: ReactionItem)? public var reactionSelected: ((UpdateMessageReaction, Bool) -> Void)? diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 5ca53dcafe..1edd7838d1 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4579,7 +4579,7 @@ func replayFinalState( mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: _internal_updateStoryViewsForMyReaction(views: item.views, previousReaction: item.myReaction, reaction: updatedReaction), + views: _internal_updateStoryViewsForMyReaction(isChannel: peerId.namespace == Namespaces.Peer.CloudChannel, views: item.views, previousReaction: item.myReaction, reaction: updatedReaction), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, @@ -4610,7 +4610,7 @@ func replayFinalState( mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: _internal_updateStoryViewsForMyReaction(views: item.views, previousReaction: item.myReaction, reaction: updatedReaction), + views: _internal_updateStoryViewsForMyReaction(isChannel: peerId.namespace == Namespaces.Peer.CloudChannel, views: item.views, previousReaction: item.myReaction, reaction: updatedReaction), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index 345e5aea37..fb72b88321 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -23,10 +23,10 @@ public struct CachedChannelFlags: OptionSet { } public struct CachedChannelParticipantsSummary: PostboxCoding, Equatable { - public let memberCount: Int32? - public let adminCount: Int32? - public let bannedCount: Int32? - public let kickedCount: Int32? + public var memberCount: Int32? + public var adminCount: Int32? + public var bannedCount: Int32? + public var kickedCount: Int32? public init(memberCount: Int32?, adminCount: Int32?, bannedCount: Int32?, kickedCount: Int32?) { self.memberCount = memberCount diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedLocalizationInfos.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedLocalizationInfos.swift index c4e84b4ce9..bf10261a93 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedLocalizationInfos.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedLocalizationInfos.swift @@ -16,6 +16,6 @@ public final class CachedLocalizationInfos: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - try container.encode(self.list, forKey: "l") + try container.encode(self.list, forKey: "t") } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index c1b0f32c6b..3269fd80ce 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -109,6 +109,7 @@ public struct Namespaces { public static let cachedEmojiQueryResults: Int8 = 26 public static let cachedPeerStoryListHeads: Int8 = 27 public static let displayedStoryNotifications: Int8 = 28 + public static let storySendAsPeerIds: Int8 = 29 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 670c7ebd37..a75270939c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1047,7 +1047,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId if !peerIds.contains(toPeerId) { peerIds.append(toPeerId) } - transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + transaction.replaceAllStorySubscriptions(key: subscriptionsKey, state: state, peerIds: peerIds) } } @@ -1976,7 +1976,11 @@ public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction, transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data())) } -func _internal_updateStoryViewsForMyReaction(views: Stories.Item.Views?, previousReaction: MessageReaction.Reaction?, reaction: MessageReaction.Reaction?) -> Stories.Item.Views? { +func _internal_updateStoryViewsForMyReaction(isChannel: Bool, views: Stories.Item.Views?, previousReaction: MessageReaction.Reaction?, reaction: MessageReaction.Reaction?) -> Stories.Item.Views? { + if !isChannel { + return views + } + var views = views ?? Stories.Item.Views(seenCount: 0, reactedCount: 0, forwardCount: 0, seenPeerIds: [], reactions: [], hasList: false) if let reaction { @@ -2040,7 +2044,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int var updatedItemValue: Stories.StoredItem? let updateViews: (Stories.Item.Views?, MessageReaction.Reaction?) -> Stories.Item.Views? = { views, previousReaction in - return _internal_updateStoryViewsForMyReaction(views: views, previousReaction: previousReaction, reaction: reaction) + return _internal_updateStoryViewsForMyReaction(isChannel: peerId.namespace == Namespaces.Peer.CloudChannel, views: views, previousReaction: previousReaction, reaction: reaction) } var currentItems = transaction.getStoryItems(peerId: peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index e0ba967b1b..f595fe87a0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -839,6 +839,17 @@ public extension TelegramEngine { guard let peer = peerView.peer else { continue } + + var isPeerHidden = false + if let user = peer as? TelegramUser { + isPeerHidden = user.storiesHidden ?? false + } else if let channel = peer as? TelegramChannel { + isPeerHidden = channel.storiesHidden ?? false + } + if isPeerHidden != isHidden { + continue + } + guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else { continue } @@ -912,6 +923,16 @@ public extension TelegramEngine { continue } + var isPeerHidden = false + if let user = peer as? TelegramUser { + isPeerHidden = user.storiesHidden ?? false + } else if let channel = peer as? TelegramChannel { + isPeerHidden = channel.storiesHidden ?? false + } + if isPeerHidden != isHidden { + continue + } + let item = EngineStorySubscriptions.Item( peer: EnginePeer(peer), hasUnseen: false, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift index 60633975b5..75b15431e4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift @@ -546,29 +546,81 @@ func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChann } } +final class CachedStorySendAsPeers: Codable { + public let peerIds: [PeerId] + + public init(peerIds: [PeerId]) { + self.peerIds = peerIds + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.peerIds = try container.decode([Int64].self, forKey: "l").map(PeerId.init) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "l") + } +} + func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> { let accountPeerId = account.peerId - return account.network.request(Api.functions.stories.getChatsToSend()) - |> retryRequest - |> mapToSignal { result -> Signal<[Peer], NoError> in - return account.postbox.transaction { transaction -> [Peer] in - let chats: [Api.Chat] - let parsedPeers: AccumulatedPeers - switch result { - case let .chats(apiChats): - chats = apiChats - case let .chatsSlice(_, apiChats): - chats = apiChats - } - parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: []) - updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) - var peers: [Peer] = [] - for chat in chats { - if let peer = transaction.getPeer(chat.peerId) { - peers.append(peer) + return account.postbox.transaction { transaction -> [Peer]? in + if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.storySendAsPeerIds, key: ValueBoxKey(length: 0)))?.get(CachedStorySendAsPeers.self) { + return entry.peerIds.compactMap(transaction.getPeer) + } else { + return nil + } + } + |> mapToSignal { cachedPeers in + let remote: Signal<[Peer], NoError> = account.network.request(Api.functions.stories.getChatsToSend()) + |> retryRequest + |> mapToSignal { result -> Signal<[Peer], NoError> in + return account.postbox.transaction { transaction -> [Peer] in + let chats: [Api.Chat] + let parsedPeers: AccumulatedPeers + switch result { + case let .chats(apiChats): + chats = apiChats + case let .chatsSlice(_, apiChats): + chats = apiChats } + parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: []) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + var peers: [Peer] = [] + for chat in chats { + if let peer = transaction.getPeer(chat.peerId) { + peers.append(peer) + + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _) = chat, let participantsCount = participantsCount { + transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in + var current = current as? CachedChannelData ?? CachedChannelData() + var participantsSummary = current.participantsSummary + + participantsSummary.memberCount = participantsCount + + current = current.withUpdatedParticipantsSummary(participantsSummary) + return current + }) + } + } + } + + if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id))) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.storySendAsPeerIds, key: ValueBoxKey(length: 0)), entry: entry) + } + + return peers } - return peers + } + + if let cachedPeers { + return .single(cachedPeers) |> then(remote) + } else { + return remote } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index c0efa3e5a4..f0df441cb2 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -809,6 +809,7 @@ private final class GroupHeaderLayer: UIView { func update( context: AccountContext, theme: PresentationTheme, + forceNeedsVibrancy: Bool, layoutType: EmojiPagerContentComponent.ItemLayoutType, hasTopSeparator: Bool, actionButtonTitle: String?, @@ -830,7 +831,7 @@ private final class GroupHeaderLayer: UIView { themeUpdated = true } - let needsVibrancy = !theme.overallDarkAppearance + let needsVibrancy = !theme.overallDarkAppearance || forceNeedsVibrancy let textOffsetY: CGFloat if hasTopSeparator { @@ -839,16 +840,22 @@ private final class GroupHeaderLayer: UIView { textOffsetY = 0.0 } + let subtitleColor: UIColor + if theme.overallDarkAppearance && forceNeedsVibrancy { + subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.withMultipliedAlpha(0.2) + } else { + subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + } + let color: UIColor let needsTintText: Bool if subtitle != nil { color = theme.chat.inputPanel.primaryTextColor needsTintText = false } else { - color = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + color = subtitleColor needsTintText = true } - let subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor let titleHorizontalOffset: CGFloat if isPremiumLocked { @@ -903,7 +910,7 @@ private final class GroupHeaderLayer: UIView { tintClearIconLayer.isHidden = !needsVibrancy clearSize = clearIconLayer.bounds.size - if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) { + if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: subtitleColor) { clearSize = image.size clearIconLayer.contents = image.cgImage } @@ -1144,7 +1151,7 @@ private final class GroupHeaderLayer: UIView { self.separatorLayer = separatorLayer self.layer.addSublayer(separatorLayer) } - separatorLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.cgColor + separatorLayer.backgroundColor = subtitleColor.cgColor separatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel)) let tintSeparatorLayer: SimpleLayer @@ -1517,6 +1524,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { private struct Params: Equatable { var context: AccountContext var theme: PresentationTheme + var forceNeedsVibrancy: Bool var strings: PresentationStrings var text: String var useOpaqueTheme: Bool @@ -1535,6 +1543,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { if lhs.theme !== rhs.theme { return false } + if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy { + return false + } if lhs.strings !== rhs.strings { return false } @@ -1823,10 +1834,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { return } self.params = nil - self.update(context: params.context, theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, searchState: params.searchState, transition: transition) + self.update(context: params.context, theme: params.theme, forceNeedsVibrancy: params.forceNeedsVibrancy, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, searchState: params.searchState, transition: transition) } - public func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: Transition) { + public func update(context: AccountContext, theme: PresentationTheme, forceNeedsVibrancy: Bool, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: Transition) { let textInputState: EmojiSearchSearchBarComponent.TextInputState if let textField = self.textField { textInputState = .active(hasText: !(textField.text ?? "").isEmpty) @@ -1837,6 +1848,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { let params = Params( context: context, theme: theme, + forceNeedsVibrancy: forceNeedsVibrancy, strings: strings, text: text, useOpaqueTheme: useOpaqueTheme, @@ -1880,7 +1892,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { let sideTextInset: CGFloat = sideInset + 4.0 + 24.0 - if useOpaqueTheme { + if theme.overallDarkAppearance && forceNeedsVibrancy { + self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.withMultipliedAlpha(0.3).cgColor + self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor + } else if useOpaqueTheme { self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor } else { @@ -1891,12 +1906,19 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.backgroundLayer.cornerRadius = inputHeight * 0.5 self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5 + let cancelColor: UIColor + if theme.overallDarkAppearance && forceNeedsVibrancy { + cancelColor = theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3) + } else { + cancelColor = useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + } + let cancelTextSize = self.cancelButtonTitle.update( transition: .immediate, component: AnyComponent(Text( text: strings.Common_Cancel, font: Font.regular(17.0), - color: useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + color: cancelColor )), environment: {}, containerSize: CGSize(width: size.width - 32.0, height: 100.0) @@ -1942,6 +1964,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { transition: transition, component: AnyComponent(EmojiSearchStatusComponent( theme: theme, + forceNeedsVibrancy: forceNeedsVibrancy, strings: strings, useOpaqueTheme: useOpaqueTheme, content: statusContent @@ -1990,6 +2013,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { component: AnyComponent(EmojiSearchSearchBarComponent( context: context, theme: theme, + forceNeedsVibrancy: forceNeedsVibrancy, strings: strings, useOpaqueTheme: useOpaqueTheme, textInputState: textInputState, @@ -5426,6 +5450,7 @@ public final class EmojiPagerContentComponent: Component { let (groupHeaderSize, centralContentWidth) = groupHeaderView.update( context: component.context, theme: keyboardChildEnvironment.theme, + forceNeedsVibrancy: component.inputInteractionHolder.inputInteraction?.externalBackground != nil, layoutType: itemLayout.layoutType, hasTopSeparator: hasTopSeparator, actionButtonTitle: actionButtonTitle, @@ -5467,7 +5492,14 @@ public final class EmojiPagerContentComponent: Component { self.scrollView.layer.insertSublayer(groupBorderLayer, at: 0) self.mirrorContentScrollView.layer.addSublayer(groupBorderLayer.tintContainerLayer) - groupBorderLayer.strokeColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.cgColor + let borderColor: UIColor + if keyboardChildEnvironment.theme.overallDarkAppearance && component.inputInteractionHolder.inputInteraction?.externalBackground != nil { + borderColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.withMultipliedAlpha(0.2) + } else { + borderColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + } + + groupBorderLayer.strokeColor = borderColor.cgColor groupBorderLayer.tintContainerLayer.strokeColor = UIColor.white.cgColor groupBorderLayer.lineWidth = 1.6 groupBorderLayer.lineCap = .round @@ -6854,7 +6886,7 @@ public final class EmojiPagerContentComponent: Component { } let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) - visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition) + visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, forceNeedsVibrancy: component.inputInteractionHolder.inputInteraction?.externalBackground != nil, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition) transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame) // Temporary workaround for status selection; use a separate search container (see GIF) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index 6ce874a3a1..d3b6f06900 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -99,6 +99,7 @@ final class EmojiSearchSearchBarComponent: Component { let context: AccountContext let theme: PresentationTheme + let forceNeedsVibrancy: Bool let strings: PresentationStrings let useOpaqueTheme: Bool let textInputState: TextInputState @@ -109,6 +110,7 @@ final class EmojiSearchSearchBarComponent: Component { init( context: AccountContext, theme: PresentationTheme, + forceNeedsVibrancy: Bool, strings: PresentationStrings, useOpaqueTheme: Bool, textInputState: TextInputState, @@ -118,6 +120,7 @@ final class EmojiSearchSearchBarComponent: Component { ) { self.context = context self.theme = theme + self.forceNeedsVibrancy = forceNeedsVibrancy self.strings = strings self.useOpaqueTheme = useOpaqueTheme self.textInputState = textInputState @@ -130,6 +133,9 @@ final class EmojiSearchSearchBarComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy { + return false + } if lhs.strings !== rhs.strings { return false } @@ -456,7 +462,10 @@ final class EmojiSearchSearchBarComponent: Component { } let color: UIColor - if component.useOpaqueTheme { + if component.theme.overallDarkAppearance && component.forceNeedsVibrancy { + let tempColor = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + color = tempColor.withMultipliedAlpha(0.3) + } else if component.useOpaqueTheme { color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor } else { color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor @@ -541,10 +550,18 @@ final class EmojiSearchSearchBarComponent: Component { self.visibleItemViews.removeValue(forKey: id) } + let selectedColor: UIColor + if component.theme.overallDarkAppearance && component.forceNeedsVibrancy { + let tempColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor + selectedColor = tempColor.withMultipliedAlpha(0.3) + } else { + selectedColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor + } + if let selectedItem = self.selectedItem, let index = items.firstIndex(where: { AnyHashable($0.id) == selectedItem }) { let selectedItemCenter = itemLayout.frame(at: index).center let selectionSize = CGSize(width: 28.0, height: 28.0) - self.selectedItemBackground.backgroundColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor.cgColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor.cgColor + self.selectedItemBackground.backgroundColor = selectedColor.cgColor self.selectedItemTintBackground.backgroundColor = UIColor(white: 1.0, alpha: 0.15).cgColor self.selectedItemBackground.cornerRadius = selectionSize.height * 0.5 self.selectedItemTintBackground.cornerRadius = selectionSize.height * 0.5 @@ -609,12 +626,19 @@ final class EmojiSearchSearchBarComponent: Component { self.component = component self.componentState = state + let textColor: UIColor + if component.theme.overallDarkAppearance && component.forceNeedsVibrancy { + textColor = component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3) + } else { + textColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + } + let textSize = self.textView.update( transition: .immediate, component: AnyComponent(Text( text: component.strings.Common_Search, font: Font.regular(17.0), - color: component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + color: textColor )), environment: {}, containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift index aa90396271..4ca1c3f602 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift @@ -70,17 +70,20 @@ final class EmojiSearchStatusComponent: Component { } let theme: PresentationTheme + let forceNeedsVibrancy: Bool let strings: PresentationStrings let useOpaqueTheme: Bool let content: Content init( theme: PresentationTheme, + forceNeedsVibrancy: Bool, strings: PresentationStrings, useOpaqueTheme: Bool, content: Content ) { self.theme = theme + self.forceNeedsVibrancy = forceNeedsVibrancy self.strings = strings self.useOpaqueTheme = useOpaqueTheme self.content = content @@ -90,6 +93,9 @@ final class EmojiSearchStatusComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy { + return false + } if lhs.strings !== rhs.strings { return false } @@ -430,7 +436,13 @@ final class EmojiSearchStatusComponent: Component { let displaySize = CGSize(width: availableSize.width * UIScreenScale, height: availableSize.height * UIScreenScale) self.displaySize = displaySize - let overlayColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + let overlayColor: UIColor + if component.theme.overallDarkAppearance && component.forceNeedsVibrancy { + overlayColor = component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3) + } else { + overlayColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + } + let baseColor: UIColor = .white if self.contentView.tintColor != overlayColor { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 55d0729bbf..c601dbf308 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -1090,7 +1090,7 @@ public final class GifPagerContentComponent: Component { } let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) - visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition) + visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, forceNeedsVibrancy: false, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition) transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in let _ = self let _ = completed diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift index 850a203b14..5bfc68e1e4 100644 --- a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -291,7 +291,7 @@ public final class LottieComponent: Component { } } - if self.scheduledPlayOnce { + if self.scheduledPlayOnce && self.isEffectivelyVisible { self.scheduledPlayOnce = false self.playOnce() } else { diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index d8fde9c552..a8ae689f3f 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -718,7 +718,6 @@ final class ShareWithPeersScreenComponent: Component { return result } } else { - let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() return .complete() } } @@ -2693,12 +2692,6 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { ) self.stateValue = state self.stateSubject.set(.single(state)) - - for peer in peers { - if case let .channel(channel) = peer, participants[channel.id] == nil { - let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start() - } - } self.readySubject.set(true) }) @@ -2811,7 +2804,9 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } for channel in adminedChannels { if case let .channel(channel) = channel, channel.hasPermission(.postStories) { - sendAsPeers.append(contentsOf: adminedChannels) + if !sendAsPeers.contains(where: { $0.id == channel.id }) { + sendAsPeers.append(contentsOf: adminedChannels) + } } } @@ -2832,12 +2827,6 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { ) self.stateValue = state self.stateSubject.set(.single(state)) - - for peer in adminedChannels { - if case let .channel(channel) = peer, participants[channel.id] == nil { - let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start() - } - } self.readySubject.set(true) }) @@ -2958,12 +2947,6 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { self.stateValue = state self.stateSubject.set(.single(state)) - for peer in state.peers { - if case let .channel(channel) = peer, participants[channel.id] == nil { - let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start() - } - } - self.readySubject.set(true) }) case let .contacts(base): @@ -3107,12 +3090,6 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { self.stateValue = state self.stateSubject.set(.single(state)) - for peer in state.peers { - if case let .channel(channel) = peer, participants[channel.id] == nil { - let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start() - } - } - self.readySubject.set(true) }) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift index 386c949236..f32358946b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift @@ -8,7 +8,7 @@ import Postbox import AvatarNode public extension StoryContainerScreen { - static func openArchivedStories(context: AccountContext, parentController: ViewController, avatarNode: AvatarNode) { + static func openArchivedStories(context: AccountContext, parentController: ViewController, avatarNode: AvatarNode, sharedProgressDisposable: MetaDisposable?) { let storyContent = StoryContentContextImpl(context: context, isHidden: true, focusedPeerId: nil, singlePeer: false) let signal = storyContent.state |> take(1) @@ -82,7 +82,10 @@ public extension StoryContainerScreen { } |> ignoreValues - let _ = avatarNode.pushLoadingStatus(signal: signal) + let disposable = avatarNode.pushLoadingStatus(signal: signal) + if let sharedProgressDisposable { + sharedProgressDisposable.set(disposable) + } } static func openPeerStories(context: AccountContext, peerId: EnginePeer.Id, parentController: ViewController, avatarNode: AvatarNode?, sharedProgressDisposable: MetaDisposable? = nil) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 4c7e60ee2e..dbc9671013 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -91,7 +91,7 @@ private final class MuteMonitor { private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { var shouldBegin: ((UITouch) -> Bool)? - var updateIsTracking: ((Bool) -> Void)? + var updateIsTracking: ((CGPoint?) -> Void)? override var state: UIGestureRecognizer.State { didSet { @@ -116,7 +116,7 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { self.isValidated = false if self.isTracking { self.isTracking = false - self.updateIsTracking?(false) + self.updateIsTracking?(nil) } } @@ -134,7 +134,7 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { if !self.isTracking { self.isTracking = true - self.updateIsTracking?(true) + self.updateIsTracking?(touches.first?.location(in: self.view)) } } } @@ -450,12 +450,38 @@ private final class StoryContainerScreenComponent: Component { let longPressRecognizer = StoryLongPressRecognizer(target: self, action: #selector(self.longPressGesture(_:))) longPressRecognizer.delegate = self - longPressRecognizer.updateIsTracking = { [weak self] isTracking in + longPressRecognizer.updateIsTracking = { [weak self] point in guard let self else { return } - self.isHoldingTouch = isTracking - self.state?.updated(transition: .immediate) + guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { + return + } + + var point = point + if let pointValue = point { + if !itemSetComponentView.allowsInstantPauseOnTouch(point: self.convert(pointValue, to: itemSetComponentView)) { + point = nil + } + } + + if point != nil { + if !self.isHoldingTouch { + self.isHoldingTouch = true + self.state?.updated(transition: .immediate) + } + } else { + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + + if self.isHoldingTouch { + self.isHoldingTouch = false + self.state?.updated(transition: .immediate) + } + } + } } longPressRecognizer.shouldBegin = { [weak self] touch in guard let self else { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index c851a0ab28..07c5fe5170 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -162,6 +162,13 @@ final class StoryItemContentComponent: Component { self.currentFetchPriority?.disposable.dispose() } + func allowsInstantPauseOnTouch(point: CGPoint) -> Bool { + if let _ = self.overlaysView.hitTest(self.convert(self.convert(point, to: self.overlaysView), to: self.overlaysView), with: nil) { + return false + } + return true + } + private func performActionAfterImageContentLoaded(update: Bool) { self.initializeVideoIfReady(update: update) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index d7c10d0cdc..727655621c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -449,6 +449,7 @@ public final class StoryItemSetContainerComponent: Component { var reactionContextNode: ReactionContextNode? weak var disappearingReactionContextNode: ReactionContextNode? + weak var willDismissReactionContextNode: ReactionContextNode? var displayLikeReactions: Bool = false var tempReactionsGesture: ContextGesture? var waitingForReactionAnimateOutToLike: MessageReaction.Reaction? @@ -711,7 +712,32 @@ public final class StoryItemSetContainerComponent: Component { return true } + func allowsInstantPauseOnTouch(point: CGPoint) -> Bool { + guard let component = self.component else { + return false + } + guard let visibleItem = self.visibleItems[component.slice.item.storyItem.id] else { + return false + } + guard let itemView = visibleItem.view.view as? StoryItemContentComponent.View else { + return false + } + + let localPoint = self.convert(point, to: itemView) + if itemView.bounds.contains(localPoint) { + if !itemView.allowsInstantPauseOnTouch(point: localPoint) { + return false + } + } + + return true + } + func isPointInsideContentArea(point: CGPoint) -> Bool { + if self.reactionContextNode != nil { + return false + } + if let inputPanelView = self.inputPanel.view, inputPanelView.alpha != 0.0 { if inputPanelView.frame.contains(point) { return false @@ -874,6 +900,13 @@ public final class StoryItemSetContainerComponent: Component { } if self.displayLikeReactions { self.displayLikeReactions = false + self.sendMessageContext.currentInputMode = .text + self.willDismissReactionContextNode = self.reactionContextNode + + if hasFirstResponder(self) { + self.endEditing(true) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) self.updateIsProgressPaused() } else if self.hasActiveDeactivateableInput() { @@ -1240,6 +1273,8 @@ public final class StoryItemSetContainerComponent: Component { return self } return self.itemsContainerView + } else if self.viewListDisplayState == .half && result.isDescendant(of: self.itemsContainerView) { + return self.itemsContainerView } return result @@ -4077,18 +4112,17 @@ public final class StoryItemSetContainerComponent: Component { } let reactionsAnchorRect: CGRect - if self.displayLikeReactions, let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView { + + if self.inputPanelExternalState.isEditing, let inputPanelFrameValue { + reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrameValue.maxX - 40.0, y: inputPanelFrameValue.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) + } else if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView { var likeRect = likeButtonView.convert(likeButtonView.bounds, to: self) likeRect.origin.y -= 15.0 likeRect.size.height += 15.0 likeRect.origin.x -= 30.0 reactionsAnchorRect = likeRect } else { - if let inputPanelFrameValue { - reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrameValue.maxX - 40.0, y: inputPanelFrameValue.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) - } else { - reactionsAnchorRect = CGRect() - } + reactionsAnchorRect = CGRect() } var effectiveDisplayReactions = false @@ -4114,7 +4148,7 @@ public final class StoryItemSetContainerComponent: Component { effectiveDisplayReactions = false } - if let reactionContextNode = self.reactionContextNode, (reactionContextNode.isReactionSearchActive && !reactionContextNode.isAnimatingOutToReaction && !reactionContextNode.isAnimatingOut) { + if let reactionContextNode = self.reactionContextNode, self.willDismissReactionContextNode !== reactionContextNode, (reactionContextNode.isReactionSearchActive && !reactionContextNode.isAnimatingOutToReaction && !reactionContextNode.isAnimatingOut) { effectiveDisplayReactions = true } @@ -4203,11 +4237,11 @@ public final class StoryItemSetContainerComponent: Component { } } - reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in - guard let self else { + reactionContextNode.reactionSelected = { [weak self, weak reactionContextNode] updateReaction, _ in + guard let self, let reactionContextNode else { return } - let action: () -> Void = { [weak self] in + let action: () -> Void = { [weak self, weak reactionContextNode] in guard let self, let component = self.component else { return } @@ -4265,7 +4299,6 @@ public final class StoryItemSetContainerComponent: Component { }, completion: { [weak targetView, weak reactionContextNode] in targetView?.removeFromSuperview() if let reactionContextNode { - reactionContextNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false) reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in reactionContextNode?.view.removeFromSuperview() }) @@ -4486,7 +4519,7 @@ public final class StoryItemSetContainerComponent: Component { if let reactionContextNode = self.disappearingReactionContextNode { if !reactionContextNode.isAnimatingOutToReaction { transition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) - reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition.containedViewLayoutTransition) + reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: reactionContextNode.centerAligned, isCoveredByInput: false, isAnimatingOut: false, transition: transition.containedViewLayoutTransition) } } @@ -5852,6 +5885,51 @@ public final class StoryItemSetContainerComponent: Component { }))) } + var isHidden = false + if case let .channel(channel) = component.slice.peer, let storiesHidden = channel.storiesHidden { + isHidden = storiesHidden + } + items.append(.action(ContextMenuActionItem(text: isHidden ? component.strings.StoryFeed_ContextUnarchive : component.strings.StoryFeed_ContextArchive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: !isHidden) + + let text = !isHidden ? component.strings.StoryFeed_TooltipArchive(component.slice.peer.compactDisplayTitle).string : component.strings.StoryFeed_TooltipUnarchive(component.slice.peer.compactDisplayTitle).string + let tooltipScreen = TooltipScreen( + context: component.context, + account: component.context.account, + sharedContext: component.context.sharedContext, + text: .markdown(text: text), + style: .customBlur(UIColor(rgb: 0x1c1c1c), 0.0), + icon: .peer(peer: component.slice.peer, isStory: true), + action: TooltipScreen.Action( + title: component.strings.Undo_Undo, + action: { + component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: isHidden) + } + ), + location: .bottom, + shouldDismissOnTouch: { _, _ in return .dismiss(consume: false) } + ) + tooltipScreen.willBecomeDismissed = { [weak self] _ in + guard let self else { + return + } + self.sendMessageContext.tooltipScreen = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.tooltipScreen?.dismiss() + self.sendMessageContext.tooltipScreen = tooltipScreen + self.updateIsProgressPaused() + component.controller()?.present(tooltipScreen, in: .current) + }))) + if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.deleteStories) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteStory, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) @@ -6004,14 +6082,27 @@ public final class StoryItemSetContainerComponent: Component { ), nil) } }))) - - var isHidden = false - if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden { - isHidden = storiesHidden - } else if case let .channel(channel) = component.slice.peer, let storiesHidden = channel.storiesHidden { - isHidden = storiesHidden + } + + var isHidden = false + if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden { + isHidden = storiesHidden + } else if case let .channel(channel) = component.slice.peer, let storiesHidden = channel.storiesHidden { + isHidden = storiesHidden + } + + var canArchive = false + if isHidden { + canArchive = true + } else { + if case .user = component.slice.peer, !component.slice.peer.isService { + canArchive = true + } else if case .channel = component.slice.peer { + canArchive = true } - + } + + if canArchive { items.append(.action(ContextMenuActionItem(text: isHidden ? component.strings.StoryFeed_ContextUnarchive : component.strings.StoryFeed_ContextArchive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index bb5ef10213..09ff3c657b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -472,10 +472,21 @@ public final class StoryPeerListComponent: Component { } } + public func cancelLoadingItem() { + self.loadingItemDisposable?.dispose() + self.loadingItemDisposable = nil + + if self.loadingItemId != nil { + self.loadingItemId = nil + self.state?.updated(transition: .immediate) + } + } + public func setLoadingItem(peerId: EnginePeer.Id, signal: Signal) { var applyLoadingItem = true + self.loadingItemDisposable?.dispose() - self.loadingItemDisposable = (signal |> deliverOnMainQueue).start(completed: { [weak self] in + let loadingItemDisposable = (signal |> deliverOnMainQueue).start(completed: { [weak self] in guard let self else { return } @@ -483,6 +494,7 @@ public final class StoryPeerListComponent: Component { applyLoadingItem = false self.state?.updated(transition: .immediate) }) + self.loadingItemDisposable = loadingItemDisposable DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in guard let self else { @@ -1285,10 +1297,9 @@ public final class StoryPeerListComponent: Component { titleContentOffset = collapsedTitleOffset } else { titleContentOffset = titleMinContentOffset.interpolate(to: ((itemLayout.containerSize.width - collapsedState.titleWidth) * 0.5) as CGFloat, amount: min(1.0, collapsedState.maxFraction) * (1.0 - collapsedState.activityFraction)) + titleContentOffset += -expandBoundsFraction * 4.0 } - titleContentOffset += -expandBoundsFraction * 4.0 - var titleIndicatorSize: CGSize? if collapsedState.activityFraction != 0.0 { let collapsedItemMinX = collapsedContentOrigin - collapsedItemWidth * 0.5 diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 7aede0fcdc..cb76952e84 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -266,7 +266,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } @discardableResult - public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { + public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { guard let controller = self.viewControllers.last as? ViewController else { return nil } @@ -275,6 +275,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon let context = self.context var storyTarget: Stories.PendingTarget? + var isPeerArchived = false + var updatedTransitionOut: ((Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?)? var presentImpl: ((ViewController) -> Void)? var returnToCameraImpl: (() -> Void)? @@ -295,7 +297,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } }, transitionOut: { finished in - if let transitionOut = transitionOut(finished ? storyTarget : nil), let destinationView = transitionOut.destinationView { + if let transitionOut = (updatedTransitionOut ?? transitionOut)(finished ? storyTarget : nil, isPeerArchived), let destinationView = transitionOut.destinationView { return CameraScreen.TransitionOut( destinationView: destinationView, destinationRect: transitionOut.destinationRect, @@ -353,7 +355,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon isEditing: false, transitionIn: transitionIn, transitionOut: { finished, isNew in - if finished, let transitionOut = transitionOut(storyTarget), let destinationView = transitionOut.destinationView { + if finished, let transitionOut = (updatedTransitionOut ?? transitionOut)(storyTarget, false), let destinationView = transitionOut.destinationView { return MediaEditorScreen.TransitionOut( destinationView: destinationView, destinationRect: transitionOut.destinationRect, @@ -375,13 +377,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return } - let context = self.context - if let rootTabController = self.rootTabController { - if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) { - rootTabController.selectedIndex = index - } - } - let target: Stories.PendingTarget let targetPeerId: EnginePeer.Id if let sendAsPeerId = options.sendAsPeerId { @@ -393,84 +388,118 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } storyTarget = target - let completionImpl: () -> Void = { [weak self] in - guard let self else { + 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 } - if let chatListController = self.chatListController as? ChatListControllerImpl { - let _ = (chatListController.hasPendingStories - |> filter { $0 } - |> take(1) - |> timeout(0.25, queue: .mainQueue(), alternate: .single(true)) - |> deliverOnMainQueue).start(completed: { [weak chatListController] in - guard let chatListController else { - return - } + if case let .user(user) = peer { + isPeerArchived = user.storiesHidden ?? false + } else if case let .channel(channel) = peer { + isPeerArchived = channel.storiesHidden ?? false + } + + let context = self.context + if let rootTabController = self.rootTabController { + if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) { + rootTabController.selectedIndex = index + } + } + + let completionImpl: () -> Void = { [weak self] in + guard let self else { + return + } + + var chatListController: ChatListControllerImpl? + + if isPeerArchived { + var viewControllers = self.viewControllers - chatListController.scrollToStories(peerId: targetPeerId) + let archiveController = ChatListControllerImpl(context: context, location: .chatList(groupId: .archive), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false) + updatedTransitionOut = archiveController.storyCameraTransitionOut() + chatListController = archiveController + viewControllers.insert(archiveController, at: 1) + self.setViewControllers(viewControllers, animated: false) + } else { + chatListController = self.chatListController as? ChatListControllerImpl + } + + if let chatListController { + let _ = (chatListController.hasPendingStories + |> filter { $0 } + |> take(1) + |> timeout(isPeerArchived ? 0.5 : 0.25, queue: .mainQueue(), alternate: .single(true)) + |> deliverOnMainQueue).start(completed: { [weak chatListController] in + guard let chatListController else { + return + } + + chatListController.scrollToStories(peerId: targetPeerId) + Queue.mainQueue().justDispatch { + commit({}) + } + }) + } else { Queue.mainQueue().justDispatch { commit({}) } - }) - } else { - Queue.mainQueue().justDispatch { - commit({}) } } - } - - if let _ = self.chatListController as? ChatListControllerImpl { - switch mediaResult { - case let .image(image, dimensions): - if let imageData = compressImageToJPEG(image, quality: 0.7) { - let entities = generateChatInputTextEntities(caption) - Logger.shared.log("MediaEditor", "Calling uploadStory for image, randomId \(randomId)") - let _ = (context.engine.messages.uploadStory(target: target, media: .image(dimensions: dimensions, data: imageData, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId) - |> deliverOnMainQueue).start(next: { stableId in - moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId)) - }) - - completionImpl() - } - case let .video(content, firstFrameImage, values, duration, dimensions): - let adjustments: VideoMediaResourceAdjustments - if let valuesData = try? JSONEncoder().encode(values) { - let data = MemoryBuffer(data: valuesData) - let digest = MemoryBuffer(data: data.md5Digest()) - adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true) - - let resource: TelegramMediaResource - switch content { - case let .imageFile(path): - resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) - case let .videoFile(path): - resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) - case let .asset(localIdentifier): - resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) + + if let _ = self.chatListController as? ChatListControllerImpl { + switch mediaResult { + case let .image(image, dimensions): + if let imageData = compressImageToJPEG(image, quality: 0.7) { + let entities = generateChatInputTextEntities(caption) + Logger.shared.log("MediaEditor", "Calling uploadStory for image, randomId \(randomId)") + let _ = (context.engine.messages.uploadStory(target: target, media: .image(dimensions: dimensions, data: imageData, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId) + |> deliverOnMainQueue).start(next: { stableId in + moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId)) + }) + + completionImpl() } - let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } - let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in - let file = TempBox.shared.tempFile(fileName: "image.jpg") - if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { - return file - } else { - return nil + case let .video(content, firstFrameImage, values, duration, dimensions): + let adjustments: VideoMediaResourceAdjustments + if let valuesData = try? JSONEncoder().encode(values) { + let data = MemoryBuffer(data: valuesData) + let digest = MemoryBuffer(data: data.md5Digest()) + adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true) + + let resource: TelegramMediaResource + switch content { + case let .imageFile(path): + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) + case let .videoFile(path): + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) + case let .asset(localIdentifier): + resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } + let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } + let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in + let file = TempBox.shared.tempFile(fileName: "image.jpg") + if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { + return file + } else { + return nil + } + } + Logger.shared.log("MediaEditor", "Calling uploadStory for video, randomId \(randomId)") + let entities = generateChatInputTextEntities(caption) + let _ = (context.engine.messages.uploadStory(target: target, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId) + |> deliverOnMainQueue).start(next: { stableId in + moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId)) + }) + + completionImpl() } - Logger.shared.log("MediaEditor", "Calling uploadStory for video, randomId \(randomId)") - let entities = generateChatInputTextEntities(caption) - let _ = (context.engine.messages.uploadStory(target: target, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId) - |> deliverOnMainQueue).start(next: { stableId in - moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId)) - }) - - completionImpl() } } - } - - dismissCameraImpl?() + + dismissCameraImpl?() + }) } as (Int64, MediaEditorScreen.Result?, [MediaArea], NSAttributedString, MediaEditorResultPrivacy, [TelegramMediaFile], @escaping (@escaping () -> Void) -> Void) -> Void ) controller.cancelled = { showDraftTooltip in