diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4f2b1ffa1c..6bf25bd809 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -6911,7 +6911,7 @@ Sorry for the inconvenience."; "SponsoredMessageMenu.Info" = "What are sponsored\nmessages?"; "SponsoredMessageInfoScreen.Title" = "What are sponsored messages?"; -"SponsoredMessageInfoScreen.Text" = "Unlike other apps, Telegram never uses your private data to target ads. You are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored message.\n\nUnline other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together."; +"SponsoredMessageInfoScreen.MarkdownText" = "Unlike other apps, Telegram never uses your private data to target ads. [Learn more in the Privacy Policy](https://telegram.org/privacy#5-6-no-ads-based-on-user-data)\nYou are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored message.\n\nUnline other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together."; "SponsoredMessageInfo.Action" = "Learn More"; "SponsoredMessageInfo.Url" = "https://telegram.org/ads"; diff --git a/submodules/AdUI/BUILD b/submodules/AdUI/BUILD index 3b30811950..d78e91cebc 100644 --- a/submodules/AdUI/BUILD +++ b/submodules/AdUI/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/AccountContext:AccountContext", + "//submodules/Markdown", ], visibility = [ "//visibility:public", diff --git a/submodules/AdUI/Sources/AdInfoScreen.swift b/submodules/AdUI/Sources/AdInfoScreen.swift index 03a57eed51..9c835909d7 100644 --- a/submodules/AdUI/Sources/AdInfoScreen.swift +++ b/submodules/AdUI/Sources/AdInfoScreen.swift @@ -7,6 +7,7 @@ import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext +import Markdown public final class AdInfoScreen: ViewController { private final class Node: ViewControllerTracingNode { @@ -84,9 +85,16 @@ public final class AdInfoScreen: ViewController { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } - var openUrl: (() -> Void)? + var openUrl: ((String) -> Void)? - let rawText = self.presentationData.strings.SponsoredMessageInfoScreen_Text + #if DEBUG && false + let rawText = "First Line\n**Bold Text** [Description](http://google.com) text\n[url]\nabcdee" + #else + let rawText = self.presentationData.strings.SponsoredMessageInfoScreen_MarkdownText + #endif + + let defaultUrl = self.presentationData.strings.SponsoredMessageInfo_Url + var items: [Item] = [] var didAddUrl = false for component in rawText.components(separatedBy: "[url]") { @@ -100,20 +108,40 @@ public final class AdInfoScreen: ViewController { let textNode = ImmediateTextNode() textNode.maximumNumberOfLines = 0 - textNode.attributedText = NSAttributedString(string: itemText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + textNode.attributedText = parseMarkdownIntoAttributedString(itemText, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + )) items.append(.text(textNode)) + textNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + } + textNode.tapAttributeAction = { attributes, _ in + if let value = attributes[NSAttributedString.Key(rawValue: "URL")] as? String { + openUrl?(value) + } + } + textNode.linkHighlightColor = self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5) if !didAddUrl { didAddUrl = true items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: { - openUrl?() + openUrl?(defaultUrl) }))) } } if !didAddUrl { didAddUrl = true items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: { - openUrl?() + openUrl?(defaultUrl) }))) } self.items = items @@ -133,11 +161,11 @@ public final class AdInfoScreen: ViewController { } } - openUrl = { [weak self] in + openUrl = { [weak self] url in guard let strongSelf = self else { return } - strongSelf.context.sharedContext.applicationBindings.openUrl(strongSelf.presentationData.strings.SponsoredMessageInfo_Url) + strongSelf.context.sharedContext.applicationBindings.openUrl(url) } } diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index eb49fab201..98930ed814 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -186,7 +186,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.contentOffset = offset self.contentOffsetChanged(offset: offset) - if self.contactListNode.listNode.isTracking { + /*if self.contactListNode.listNode.isTracking { if case let .known(value) = offset { if !self.storiesUnlocked { if value < -40.0 { @@ -220,7 +220,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { default: break } - } + }*/ } self.contactListNode.contentScrollingEnded = { [weak self] listView in @@ -280,43 +280,18 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } private func contentScrollingEnded(listView: ListView) -> Bool { - if "".isEmpty { - return false - } - - /*if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset { - if navigationBarComponentView.effectiveStoriesInsetHeight > 0.0 { - if clippedScrollOffset > 0.0 && clippedScrollOffset < navigationBarComponentView.effectiveStoriesInsetHeight { - if clippedScrollOffset < navigationBarComponentView.effectiveStoriesInsetHeight * 0.5 { - let _ = listView.scrollToOffsetFromTop(0.0, animated: true) - } else { - let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight, animated: true) - } - return true + if clippedScrollOffset > 0.0 && clippedScrollOffset < ChatListNavigationBar.searchScrollHeight { + if clippedScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 { + let _ = listView.scrollToOffsetFromTop(0.0, animated: true) } else { - let searchScrollOffset = clippedScrollOffset - navigationBarComponentView.effectiveStoriesInsetHeight - if searchScrollOffset > 0.0 && searchScrollOffset < ChatListNavigationBar.searchScrollHeight { - if searchScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 { - let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight, animated: true) - } else { - let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight + ChatListNavigationBar.searchScrollHeight, animated: true) - } - return true - } - } - } else { - if clippedScrollOffset > 0.0 && clippedScrollOffset < ChatListNavigationBar.searchScrollHeight { - if clippedScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 { - let _ = listView.scrollToOffsetFromTop(0.0, animated: true) - } else { - let _ = listView.scrollToOffsetFromTop(ChatListNavigationBar.searchScrollHeight, animated: true) - } - return true + let _ = listView.scrollToOffsetFromTop(ChatListNavigationBar.searchScrollHeight, animated: true) } + return true } } - }*/ + } return false } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 471f020bcb..5daeb253d2 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -1235,7 +1235,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset, topItemFound { if !self.stackFromBottom { - completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset - effectiveInsets.bottom - effectiveInsets.top) + if !keepMinimalScrollHeightWithTopInset.isZero { + completeHeight = max(completeHeight, self.visibleSize.height + effectiveInsets.top + effectiveInsets.bottom) + } + //completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset - effectiveInsets.bottom - effectiveInsets.top) bottomItemEdge = max(bottomItemEdge, topItemEdge + completeHeight) } else { effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - completeHeight) @@ -1647,7 +1650,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset { if !self.stackFromBottom { - completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset) + if !keepMinimalScrollHeightWithTopInset.isZero { + completeHeight = max(completeHeight, self.visibleSize.height + effectiveInsets.top + effectiveInsets.bottom) + } + //completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset) bottomItemEdge = max(bottomItemEdge, topItemEdge + completeHeight) } } diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index ac651cc684..189ed3c9ee 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -580,7 +580,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { public let topShadowNode: ASImageNode public let bottomShadowNode: ASImageNode - public var storyParams: (peer: EnginePeer, items: [EngineStoryItem], count: Int, hasUnseen: Bool)? + public var storyParams: (peer: EnginePeer, items: [EngineStoryItem], count: Int, hasUnseen: Bool, hasUnseenPrivate: Bool)? private var expandedStorySetIndicator: ComponentView? public var expandedStorySetIndicatorTransitionView: (UIView, CGRect)? { if let setView = self.expandedStorySetIndicator?.view as? StorySetIndicatorComponent.View { @@ -1268,6 +1268,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { peer: storyParams.peer, items: storyParams.items, hasUnseen: storyParams.hasUnseen, + hasUnseenPrivate: storyParams.hasUnseenPrivate, totalCount: storyParams.count, theme: defaultDarkPresentationTheme, action: { [weak self] in diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index eff8bfd439..19fd2c372e 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -1074,6 +1074,10 @@ public final class SparseItemGrid: ASDisplayNode { } for id in removeIds { if let item = self.visibleItems.removeValue(forKey: id) { + if let blurLayer = item.blurLayer { + item.blurLayer = nil + blurLayer.removeFromSuperlayer() + } if let layer = item.layer { items.itemBinding.unbindLayer(layer: layer) layer.removeFromSuperlayer() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 3accb87b8a..60bfed09a9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -607,7 +607,11 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput if let firstFrameFile = firstFrameFile { account.postbox.mediaBox.storeCachedResourceRepresentation(resource.id.stringRepresentation, representationId: "first-frame", keepDuration: .general, tempFile: firstFrameFile) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) + if let data = try? Data(contentsOf: URL(fileURLWithPath: firstFrameFile.path), options: .mappedIfSafe) { + let localResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: nil, isSecretRelated: false) + account.postbox.mediaBox.storeResourceData(localResource.id, data: data) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: localResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) + } } let fileMedia = TelegramMediaFile( diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 4fccbf704f..fb2a491900 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -544,9 +544,9 @@ public final class PeerStoryListContext { self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputUser? in return transaction.getPeer(peerId).flatMap(apiInputUser) } - |> mapToSignal { inputUser -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in + |> mapToSignal { inputUser -> Signal<([EngineStoryItem], Int, PeerReference?, Bool), NoError> in guard let inputUser = inputUser else { - return .single(([], 0, nil)) + return .single(([], 0, nil, false)) } let signal: Signal @@ -562,18 +562,20 @@ public final class PeerStoryListContext { |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in + |> mapToSignal { result -> Signal<([EngineStoryItem], Int, PeerReference?, Bool), NoError> in guard let result = result else { - return .single(([], 0, nil)) + return .single(([], 0, nil, false)) } - return account.postbox.transaction { transaction -> ([EngineStoryItem], Int, PeerReference?) in + return account.postbox.transaction { transaction -> ([EngineStoryItem], Int, PeerReference?, Bool) in var storyItems: [EngineStoryItem] = [] var totalCount: Int = 0 + var hasMore: Bool = false switch result { case let .stories(count, stories, users): totalCount = Int(count) + hasMore = stories.count >= 100 updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) @@ -619,11 +621,11 @@ public final class PeerStoryListContext { } } - return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init)) + return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init), hasMore) } } } - |> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, peerReference in + |> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, peerReference, hasMore in guard let `self` = self else { return } @@ -650,7 +652,11 @@ public final class PeerStoryListContext { updatedState.peerReference = peerReference } - updatedState.loadMoreToken = (storyItems.last?.id).flatMap(Int.init) + if hasMore { + updatedState.loadMoreToken = (storyItems.last?.id).flatMap(Int.init) + } else { + updatedState.loadMoreToken = nil + } if updatedState.loadMoreToken != nil { updatedState.totalCount = max(totalCount, updatedState.items.count) } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD index 1abccac0f3..5b58e2600a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD @@ -26,6 +26,8 @@ swift_library( "//submodules/UndoUI", "//submodules/TelegramUI/Components/BottomButtonPanelComponent", "//submodules/TelegramUI/Components/MoreHeaderButton", + "//submodules/TelegramUI/Components/MediaEditorScreen", + "//submodules/SaveToCameraRoll", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index be12de4009..81120c734c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -14,6 +14,8 @@ import ChatTitleView import BottomButtonPanelComponent import UndoUI import MoreHeaderButton +import MediaEditorScreen +import SaveToCameraRoll final class PeerInfoStoryGridScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -93,11 +95,11 @@ final class PeerInfoStoryGridScreenComponent: Component { }, action: { [weak self] _, a in a(.default) - guard let self, let component = self.component else { + guard let self else { return } - let _ = component + self.saveSelected() }))) items.append(.action(ContextMenuActionItem(text: strings.Common_Delete, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) @@ -279,6 +281,72 @@ final class PeerInfoStoryGridScreenComponent: Component { controller.presentInGlobalOverlay(contextController) } + private func saveSelected() { + guard let component = self.component else { + return + } + + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let component = self.component, let peer else { + return + } + guard let peerReference = PeerReference(peer._asPeer()) else { + return + } + + guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else { + return + } + + var signals: [Signal] = [] + let sortedItems = paneNode.selectedItems.sorted(by: { lhs, rhs in return lhs.key < rhs.key }) + if sortedItems.isEmpty { + return + } + + //TODO:localize + let saveScreen = SaveProgressScreen(context: component.context, content: .progress("Saving", 0.0)) + self.environment?.controller()?.present(saveScreen, in: .current) + + let valueNorm: Float = 1.0 / Float(sortedItems.count) + var progressStart: Float = 0.0 + for (_, item) in sortedItems { + let itemOffset = progressStart + progressStart += valueNorm + signals.append(saveToCameraRoll(context: component.context, postbox: component.context.account.postbox, userLocation: .other, mediaReference: .story(peer: peerReference, id: item.id, media: item.media._asMedia())) + |> map { progress -> Float in + return itemOffset + progress * valueNorm + }) + } + + var allSignal: Signal = .single(0.0) + for signal in signals { + allSignal = allSignal |> then(signal) + } + + let disposable = (allSignal + |> deliverOnMainQueue).start(next: { [weak saveScreen] progress in + guard let saveScreen else { + return + } + saveScreen.content = .progress("Saving", progress) + }, completed: { [weak saveScreen] in + guard let saveScreen else { + return + } + saveScreen.content = .completion("Saved") + Queue.mainQueue().after(3.0, { [weak saveScreen] in + saveScreen?.dismiss() + }) + }) + + saveScreen.cancelled = { + disposable.dispose() + } + }) + } + func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.state = state diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 41fbf7a7d3..36fca4059c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1487,29 +1487,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal { - //TODO:load more - /*guard let anchor = anchor as? VisualMediaHoleAnchor else { - return .never() - } - let mappedDirection: SparseMessageList.LoadHoleDirection - switch location { - case .around: - mappedDirection = .around - case .toLower: - mappedDirection = .later - case .toUpper: - mappedDirection = .earlier - } let listSource = self.listSource - return Signal { subscriber in - listSource.loadHole(anchor: anchor.messageId, direction: mappedDirection, completion: { - subscriber.putCompletion() - }) - + return Signal { _ in + listSource.loadMore() + return EmptyDisposable - }*/ - - return .never() + } + |> runOn(.mainQueue()) } public func updateContentType(contentType: ContentType) { @@ -1575,7 +1559,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) var mappedItems: [SparseItemGrid.Item] = [] - let mappedHoles: [SparseItemGrid.HoleAnchor] = [] + var mappedHoles: [SparseItemGrid.HoleAnchor] = [] var totalCount: Int = 0 if let peerReference = state.peerReference { for item in state.items { @@ -1586,6 +1570,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr localMonthTimestamp: Month(localTimestamp: item.timestamp + timezoneOffset).packedValue )) } + if mappedItems.count < state.totalCount, let lastItem = state.items.last { + mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: 1, localMonthTimestamp: Month(localTimestamp: lastItem.timestamp + timezoneOffset).packedValue)) + } } totalCount = state.totalCount totalCount = max(mappedItems.count, totalCount) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 09b760c7ea..7d14441bd1 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1247,6 +1247,10 @@ private final class StoryContainerScreenComponent: Component { } else if slice.previousItemId != nil { component.content.navigate(navigation: .item(.previous)) } else if let environment = self.environment { + if let sourceIsAvatar = component.transitionIn?.sourceIsAvatar, sourceIsAvatar { + } else { + self.dismissWithoutTransitionOut = true + } environment.controller()?.dismiss() } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 384b435b17..1c5aab30a3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1750,7 +1750,7 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.videoRecorderValue?.dismissVideo() self.sendMessageContext.discardMediaRecordingPreview(view: self) }, - attachmentAction: { [weak self] in + attachmentAction: component.slice.peer.isService ? nil : { [weak self] in guard let self else { return } @@ -3635,7 +3635,7 @@ public final class StoryItemSetContainerComponent: Component { self.requestSave() }))) - if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { + if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) && component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) { items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -3772,6 +3772,46 @@ public final class StoryItemSetContainerComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) var items: [ContextMenuItem] = [] + + if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { + items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) + |> deliverOnMainQueue).start(next: { [weak self] link in + guard let self, let component = self.component else { + return + } + if let link { + UIPasteboard.general.string = link + + component.presentController(UndoOverlayController( + presentationData: presentationData, + content: .linkCopied(text: "Link copied."), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), nil) + } + }) + }))) + items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.sendMessageContext.performShareAction(view: self) + }))) + } let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: component.slice.peer._asPeer(), peerSettings: settings._asNotificationSettings()) @@ -3824,7 +3864,7 @@ public final class StoryItemSetContainerComponent: Component { isHidden = storiesHidden } - items.append(.action(ContextMenuActionItem(text: isHidden ? "Unhide \(component.slice.peer.compactDisplayTitle)" : "Hide \(component.slice.peer.compactDisplayTitle)", icon: { theme in + items.append(.action(ContextMenuActionItem(text: isHidden ? "Unarchive" : "Archive", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/MoveToChats" : "Chat/Context Menu/MoveToContacts"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) @@ -3864,6 +3904,25 @@ public final class StoryItemSetContainerComponent: Component { component.controller()?.present(tooltipScreen, in: .current) }))) + #if DEBUG + let saveText: String + if case .file = component.slice.item.storyItem.media { + saveText = "Save Video" + } else { + saveText = "Save Image" + } + items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.requestSave() + }))) + #endif + items.append(.action(ContextMenuActionItem(text: "Report", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, a in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 585d6c7a18..d199aae70f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -2353,7 +2353,7 @@ final class StoryItemSetContainerSendMessage { }) } - func openPeerMention(view: StoryItemSetContainerComponent.View, name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) { + func openPeerMention(view: StoryItemSetContainerComponent.View, name: String, navigation: ChatControllerInteractionNavigateToPeer = .info, sourceMessageId: MessageId? = nil) { guard let component = view.component, let parentController = component.controller() else { return } diff --git a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift index dae56a4db9..9534a88132 100644 --- a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift @@ -92,6 +92,7 @@ public final class StorySetIndicatorComponent: Component { public let peer: EnginePeer public let items: [EngineStoryItem] public let hasUnseen: Bool + public let hasUnseenPrivate: Bool public let totalCount: Int public let theme: PresentationTheme public let action: () -> Void @@ -101,6 +102,7 @@ public final class StorySetIndicatorComponent: Component { peer: EnginePeer, items: [EngineStoryItem], hasUnseen: Bool, + hasUnseenPrivate: Bool, totalCount: Int, theme: PresentationTheme, action: @escaping () -> Void @@ -109,6 +111,7 @@ public final class StorySetIndicatorComponent: Component { self.peer = peer self.items = items self.hasUnseen = hasUnseen + self.hasUnseenPrivate = hasUnseenPrivate self.totalCount = totalCount self.theme = theme self.action = action @@ -121,6 +124,9 @@ public final class StorySetIndicatorComponent: Component { if lhs.hasUnseen != rhs.hasUnseen { return false } + if lhs.hasUnseenPrivate != rhs.hasUnseenPrivate { + return false + } if lhs.totalCount != rhs.totalCount { return false } @@ -349,7 +355,9 @@ public final class StorySetIndicatorComponent: Component { let borderColors: [UInt32] - if component.hasUnseen { + if component.hasUnseenPrivate { + borderColors = [component.theme.chatList.storyUnseenPrivateColors.topColor.argb, component.theme.chatList.storyUnseenPrivateColors.bottomColor.argb] + } else if component.hasUnseen { borderColors = [component.theme.chatList.storyUnseenColors.topColor.argb, component.theme.chatList.storyUnseenColors.bottomColor.argb] } else { borderColors = [UIColor(white: 1.0, alpha: 0.3).argb, UIColor(white: 1.0, alpha: 0.3).argb] diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 7c35d1db72..25312c768e 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -457,9 +457,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) }, iconSource: nil, action: { _, f in - /*c.dismiss(completion: { - controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context)) - })*/ f(.dismissWithoutContent) controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context)) }))) @@ -625,6 +622,14 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + for media in messages[0].media { + if let story = media as? TelegramMediaStory { + if let story = message.associatedStories[story.storyId], story.data.isEmpty { + canPin = false + } + } + } + var loadStickerSaveStatusSignal: Signal = .single(nil) if let loadStickerSaveStatus = loadStickerSaveStatus { loadStickerSaveStatusSignal = context.engine.stickers.isStickerSaved(id: loadStickerSaveStatus) @@ -1943,6 +1948,10 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer } } else if let action = media as? TelegramMediaAction, case .phoneCall = action.action { optionsMap[id]!.insert(.rateCall) + } else if let story = media as? TelegramMediaStory { + if let story = message.associatedStories[story.storyId], story.data.isEmpty { + isShareProtected = true + } } } if id.namespace == Namespaces.Message.ScheduledCloud { @@ -1962,7 +1971,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer optionsMap[id]!.insert(.deleteLocally) } } else if id.peerId == accountPeerId { - if !(message.flags.isSending || message.flags.contains(.Failed)) { + if !(message.flags.isSending || message.flags.contains(.Failed)) && !isShareProtected { optionsMap[id]!.insert(.forward) } optionsMap[id]!.insert(.deleteLocally) @@ -2006,7 +2015,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer banPeer = nil } } - if !message.containsSecretMedia && !isAction { + if !message.containsSecretMedia && !isAction && !isShareProtected { if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.isCopyProtected() { if !(message.flags.isSending || message.flags.contains(.Failed)) { optionsMap[id]!.insert(.forward) @@ -2023,7 +2032,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer } } else if let group = peer as? TelegramGroup { if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia { - if !isAction && !message.isCopyProtected() { + if !isAction && !message.isCopyProtected() && !isShareProtected { if !(message.flags.isSending || message.flags.contains(.Failed)) { optionsMap[id]!.insert(.forward) } @@ -2057,7 +2066,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer } } } else if let user = peer as? TelegramUser { - if !isScheduled && message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction && !message.id.peerId.isReplies && !message.isCopyProtected() { + if !isScheduled && message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction && !message.id.peerId.isReplies && !message.isCopyProtected() && !isShareProtected { if !(message.flags.isSending || message.flags.contains(.Failed)) { optionsMap[id]!.insert(.forward) } diff --git a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift index 62e9404cfa..1c479b5518 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift @@ -536,7 +536,11 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { - return .openMessage + if let item = self.item, item.message.media.contains(where: { $0 is TelegramMediaStory }) { + return .none + } else { + return .openMessage + } } else { return .none } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 60975f5cfd..d20b793bba 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3903,7 +3903,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .placeholder: return nil } - }, state.items.count, state.hasUnseen) + }, state.items.count, state.hasUnseen, state.hasUnseenCloseFriends) } self.requestLayout(animated: false)