This commit is contained in:
Ali 2023-07-10 22:38:18 +04:00
parent 855dc9c9c0
commit d6e97be56b
19 changed files with 258 additions and 92 deletions

View File

@ -6911,7 +6911,7 @@ Sorry for the inconvenience.";
"SponsoredMessageMenu.Info" = "What are sponsored\nmessages?"; "SponsoredMessageMenu.Info" = "What are sponsored\nmessages?";
"SponsoredMessageInfoScreen.Title" = "What are sponsored messages?"; "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.Action" = "Learn More";
"SponsoredMessageInfo.Url" = "https://telegram.org/ads"; "SponsoredMessageInfo.Url" = "https://telegram.org/ads";

View File

@ -17,6 +17,7 @@ swift_library(
"//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext", "//submodules/AccountContext:AccountContext",
"//submodules/Markdown",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -7,6 +7,7 @@ import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import AccountContext import AccountContext
import Markdown
public final class AdInfoScreen: ViewController { public final class AdInfoScreen: ViewController {
private final class Node: ViewControllerTracingNode { private final class Node: ViewControllerTracingNode {
@ -84,9 +85,16 @@ public final class AdInfoScreen: ViewController {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never 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 items: [Item] = []
var didAddUrl = false var didAddUrl = false
for component in rawText.components(separatedBy: "[url]") { for component in rawText.components(separatedBy: "[url]") {
@ -100,20 +108,40 @@ public final class AdInfoScreen: ViewController {
let textNode = ImmediateTextNode() let textNode = ImmediateTextNode()
textNode.maximumNumberOfLines = 0 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)) 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 { if !didAddUrl {
didAddUrl = true didAddUrl = true
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: { items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
openUrl?() openUrl?(defaultUrl)
}))) })))
} }
} }
if !didAddUrl { if !didAddUrl {
didAddUrl = true didAddUrl = true
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: { items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
openUrl?() openUrl?(defaultUrl)
}))) })))
} }
self.items = items 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 { guard let strongSelf = self else {
return return
} }
strongSelf.context.sharedContext.applicationBindings.openUrl(strongSelf.presentationData.strings.SponsoredMessageInfo_Url) strongSelf.context.sharedContext.applicationBindings.openUrl(url)
} }
} }

View File

@ -186,7 +186,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
self.contentOffset = offset self.contentOffset = offset
self.contentOffsetChanged(offset: offset) self.contentOffsetChanged(offset: offset)
if self.contactListNode.listNode.isTracking { /*if self.contactListNode.listNode.isTracking {
if case let .known(value) = offset { if case let .known(value) = offset {
if !self.storiesUnlocked { if !self.storiesUnlocked {
if value < -40.0 { if value < -40.0 {
@ -220,7 +220,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
default: default:
break break
} }
} }*/
} }
self.contactListNode.contentScrollingEnded = { [weak self] listView in self.contactListNode.contentScrollingEnded = { [weak self] listView in
@ -280,43 +280,18 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
} }
private func contentScrollingEnded(listView: ListView) -> Bool { private func contentScrollingEnded(listView: ListView) -> Bool {
if "".isEmpty { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
return false
}
/*if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset { if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
if navigationBarComponentView.effectiveStoriesInsetHeight > 0.0 { if clippedScrollOffset > 0.0 && clippedScrollOffset < ChatListNavigationBar.searchScrollHeight {
if clippedScrollOffset > 0.0 && clippedScrollOffset < navigationBarComponentView.effectiveStoriesInsetHeight { if clippedScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 {
if clippedScrollOffset < navigationBarComponentView.effectiveStoriesInsetHeight * 0.5 { let _ = listView.scrollToOffsetFromTop(0.0, animated: true)
let _ = listView.scrollToOffsetFromTop(0.0, animated: true)
} else {
let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight, animated: true)
}
return true
} else { } else {
let searchScrollOffset = clippedScrollOffset - navigationBarComponentView.effectiveStoriesInsetHeight let _ = listView.scrollToOffsetFromTop(ChatListNavigationBar.searchScrollHeight, animated: true)
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
} }
return true
} }
} }
}*/ }
return false return false
} }

View File

@ -1235,7 +1235,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
if let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset, topItemFound { if let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset, topItemFound {
if !self.stackFromBottom { 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) bottomItemEdge = max(bottomItemEdge, topItemEdge + completeHeight)
} else { } else {
effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - completeHeight) 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 let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset {
if !self.stackFromBottom { 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) bottomItemEdge = max(bottomItemEdge, topItemEdge + completeHeight)
} }
} }

View File

@ -580,7 +580,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
public let topShadowNode: ASImageNode public let topShadowNode: ASImageNode
public let bottomShadowNode: 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<Empty>? private var expandedStorySetIndicator: ComponentView<Empty>?
public var expandedStorySetIndicatorTransitionView: (UIView, CGRect)? { public var expandedStorySetIndicatorTransitionView: (UIView, CGRect)? {
if let setView = self.expandedStorySetIndicator?.view as? StorySetIndicatorComponent.View { if let setView = self.expandedStorySetIndicator?.view as? StorySetIndicatorComponent.View {
@ -1268,6 +1268,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
peer: storyParams.peer, peer: storyParams.peer,
items: storyParams.items, items: storyParams.items,
hasUnseen: storyParams.hasUnseen, hasUnseen: storyParams.hasUnseen,
hasUnseenPrivate: storyParams.hasUnseenPrivate,
totalCount: storyParams.count, totalCount: storyParams.count,
theme: defaultDarkPresentationTheme, theme: defaultDarkPresentationTheme,
action: { [weak self] in action: { [weak self] in

View File

@ -1074,6 +1074,10 @@ public final class SparseItemGrid: ASDisplayNode {
} }
for id in removeIds { for id in removeIds {
if let item = self.visibleItems.removeValue(forKey: id) { if let item = self.visibleItems.removeValue(forKey: id) {
if let blurLayer = item.blurLayer {
item.blurLayer = nil
blurLayer.removeFromSuperlayer()
}
if let layer = item.layer { if let layer = item.layer {
items.itemBinding.unbindLayer(layer: layer) items.itemBinding.unbindLayer(layer: layer)
layer.removeFromSuperlayer() layer.removeFromSuperlayer()

View File

@ -607,7 +607,11 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput
if let firstFrameFile = firstFrameFile { if let firstFrameFile = firstFrameFile {
account.postbox.mediaBox.storeCachedResourceRepresentation(resource.id.stringRepresentation, representationId: "first-frame", keepDuration: .general, tempFile: 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( let fileMedia = TelegramMediaFile(

View File

@ -544,9 +544,9 @@ public final class PeerStoryListContext {
self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputUser? in self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(peerId).flatMap(apiInputUser) 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 { guard let inputUser = inputUser else {
return .single(([], 0, nil)) return .single(([], 0, nil, false))
} }
let signal: Signal<Api.stories.Stories, MTRpcError> let signal: Signal<Api.stories.Stories, MTRpcError>
@ -562,18 +562,20 @@ public final class PeerStoryListContext {
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in |> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
return .single(nil) 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 { 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 storyItems: [EngineStoryItem] = []
var totalCount: Int = 0 var totalCount: Int = 0
var hasMore: Bool = false
switch result { switch result {
case let .stories(count, stories, users): case let .stories(count, stories, users):
totalCount = Int(count) totalCount = Int(count)
hasMore = stories.count >= 100
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) 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 { guard let `self` = self else {
return return
} }
@ -650,7 +652,11 @@ public final class PeerStoryListContext {
updatedState.peerReference = peerReference 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 { if updatedState.loadMoreToken != nil {
updatedState.totalCount = max(totalCount, updatedState.items.count) updatedState.totalCount = max(totalCount, updatedState.items.count)
} else { } else {

View File

@ -26,6 +26,8 @@ swift_library(
"//submodules/UndoUI", "//submodules/UndoUI",
"//submodules/TelegramUI/Components/BottomButtonPanelComponent", "//submodules/TelegramUI/Components/BottomButtonPanelComponent",
"//submodules/TelegramUI/Components/MoreHeaderButton", "//submodules/TelegramUI/Components/MoreHeaderButton",
"//submodules/TelegramUI/Components/MediaEditorScreen",
"//submodules/SaveToCameraRoll",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -14,6 +14,8 @@ import ChatTitleView
import BottomButtonPanelComponent import BottomButtonPanelComponent
import UndoUI import UndoUI
import MoreHeaderButton import MoreHeaderButton
import MediaEditorScreen
import SaveToCameraRoll
final class PeerInfoStoryGridScreenComponent: Component { final class PeerInfoStoryGridScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -93,11 +95,11 @@ final class PeerInfoStoryGridScreenComponent: Component {
}, action: { [weak self] _, a in }, action: { [weak self] _, a in
a(.default) a(.default)
guard let self, let component = self.component else { guard let self else {
return return
} }
let _ = component self.saveSelected()
}))) })))
items.append(.action(ContextMenuActionItem(text: strings.Common_Delete, textColor: .destructive, icon: { theme in 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
@ -279,6 +281,72 @@ final class PeerInfoStoryGridScreenComponent: Component {
controller.presentInGlobalOverlay(contextController) 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<Float, NoError>] = []
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<Float, NoError> = .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<EnvironmentType>, transition: Transition) -> CGSize { func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component self.component = component
self.state = state self.state = state

View File

@ -1487,29 +1487,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} }
public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> { public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> {
//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 let listSource = self.listSource
return Signal { subscriber in return Signal { _ in
listSource.loadHole(anchor: anchor.messageId, direction: mappedDirection, completion: { listSource.loadMore()
subscriber.putCompletion()
})
return EmptyDisposable return EmptyDisposable
}*/ }
|> runOn(.mainQueue())
return .never()
} }
public func updateContentType(contentType: ContentType) { public func updateContentType(contentType: ContentType) {
@ -1575,7 +1559,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) let timezoneOffset = Int32(TimeZone.current.secondsFromGMT())
var mappedItems: [SparseItemGrid.Item] = [] var mappedItems: [SparseItemGrid.Item] = []
let mappedHoles: [SparseItemGrid.HoleAnchor] = [] var mappedHoles: [SparseItemGrid.HoleAnchor] = []
var totalCount: Int = 0 var totalCount: Int = 0
if let peerReference = state.peerReference { if let peerReference = state.peerReference {
for item in state.items { for item in state.items {
@ -1586,6 +1570,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
localMonthTimestamp: Month(localTimestamp: item.timestamp + timezoneOffset).packedValue 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 = state.totalCount
totalCount = max(mappedItems.count, totalCount) totalCount = max(mappedItems.count, totalCount)

View File

@ -1247,6 +1247,10 @@ private final class StoryContainerScreenComponent: Component {
} else if slice.previousItemId != nil { } else if slice.previousItemId != nil {
component.content.navigate(navigation: .item(.previous)) component.content.navigate(navigation: .item(.previous))
} else if let environment = self.environment { } else if let environment = self.environment {
if let sourceIsAvatar = component.transitionIn?.sourceIsAvatar, sourceIsAvatar {
} else {
self.dismissWithoutTransitionOut = true
}
environment.controller()?.dismiss() environment.controller()?.dismiss()
} }

View File

@ -1750,7 +1750,7 @@ public final class StoryItemSetContainerComponent: Component {
self.sendMessageContext.videoRecorderValue?.dismissVideo() self.sendMessageContext.videoRecorderValue?.dismissVideo()
self.sendMessageContext.discardMediaRecordingPreview(view: self) self.sendMessageContext.discardMediaRecordingPreview(view: self)
}, },
attachmentAction: { [weak self] in attachmentAction: component.slice.peer.isService ? nil : { [weak self] in
guard let self else { guard let self else {
return return
} }
@ -3635,7 +3635,7 @@ public final class StoryItemSetContainerComponent: Component {
self.requestSave() 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 items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in }, 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) let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
var items: [ContextMenuItem] = [] 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()) 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 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) return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/MoveToChats" : "Chat/Context Menu/MoveToContacts"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in }, action: { [weak self] _, a in
a(.default) a(.default)
@ -3864,6 +3904,25 @@ public final class StoryItemSetContainerComponent: Component {
component.controller()?.present(tooltipScreen, in: .current) 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 items.append(.action(ContextMenuActionItem(text: "Report", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, a in }, action: { [weak self] c, a in

View File

@ -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 { guard let component = view.component, let parentController = component.controller() else {
return return
} }

View File

@ -92,6 +92,7 @@ public final class StorySetIndicatorComponent: Component {
public let peer: EnginePeer public let peer: EnginePeer
public let items: [EngineStoryItem] public let items: [EngineStoryItem]
public let hasUnseen: Bool public let hasUnseen: Bool
public let hasUnseenPrivate: Bool
public let totalCount: Int public let totalCount: Int
public let theme: PresentationTheme public let theme: PresentationTheme
public let action: () -> Void public let action: () -> Void
@ -101,6 +102,7 @@ public final class StorySetIndicatorComponent: Component {
peer: EnginePeer, peer: EnginePeer,
items: [EngineStoryItem], items: [EngineStoryItem],
hasUnseen: Bool, hasUnseen: Bool,
hasUnseenPrivate: Bool,
totalCount: Int, totalCount: Int,
theme: PresentationTheme, theme: PresentationTheme,
action: @escaping () -> Void action: @escaping () -> Void
@ -109,6 +111,7 @@ public final class StorySetIndicatorComponent: Component {
self.peer = peer self.peer = peer
self.items = items self.items = items
self.hasUnseen = hasUnseen self.hasUnseen = hasUnseen
self.hasUnseenPrivate = hasUnseenPrivate
self.totalCount = totalCount self.totalCount = totalCount
self.theme = theme self.theme = theme
self.action = action self.action = action
@ -121,6 +124,9 @@ public final class StorySetIndicatorComponent: Component {
if lhs.hasUnseen != rhs.hasUnseen { if lhs.hasUnseen != rhs.hasUnseen {
return false return false
} }
if lhs.hasUnseenPrivate != rhs.hasUnseenPrivate {
return false
}
if lhs.totalCount != rhs.totalCount { if lhs.totalCount != rhs.totalCount {
return false return false
} }
@ -349,7 +355,9 @@ public final class StorySetIndicatorComponent: Component {
let borderColors: [UInt32] 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] borderColors = [component.theme.chatList.storyUnseenColors.topColor.argb, component.theme.chatList.storyUnseenColors.bottomColor.argb]
} else { } else {
borderColors = [UIColor(white: 1.0, alpha: 0.3).argb, UIColor(white: 1.0, alpha: 0.3).argb] borderColors = [UIColor(white: 1.0, alpha: 0.3).argb, UIColor(white: 1.0, alpha: 0.3).argb]

View File

@ -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 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { _, f in }, iconSource: nil, action: { _, f in
/*c.dismiss(completion: {
controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context))
})*/
f(.dismissWithoutContent) f(.dismissWithoutContent)
controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context)) 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<Bool?, NoError> = .single(nil) var loadStickerSaveStatusSignal: Signal<Bool?, NoError> = .single(nil)
if let loadStickerSaveStatus = loadStickerSaveStatus { if let loadStickerSaveStatus = loadStickerSaveStatus {
loadStickerSaveStatusSignal = context.engine.stickers.isStickerSaved(id: 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 { } else if let action = media as? TelegramMediaAction, case .phoneCall = action.action {
optionsMap[id]!.insert(.rateCall) 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 { if id.namespace == Namespaces.Message.ScheduledCloud {
@ -1962,7 +1971,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
optionsMap[id]!.insert(.deleteLocally) optionsMap[id]!.insert(.deleteLocally)
} }
} else if id.peerId == accountPeerId { } 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(.forward)
} }
optionsMap[id]!.insert(.deleteLocally) optionsMap[id]!.insert(.deleteLocally)
@ -2006,7 +2015,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
banPeer = nil banPeer = nil
} }
} }
if !message.containsSecretMedia && !isAction { if !message.containsSecretMedia && !isAction && !isShareProtected {
if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.isCopyProtected() { if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.isCopyProtected() {
if !(message.flags.isSending || message.flags.contains(.Failed)) { if !(message.flags.isSending || message.flags.contains(.Failed)) {
optionsMap[id]!.insert(.forward) optionsMap[id]!.insert(.forward)
@ -2023,7 +2032,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
} }
} else if let group = peer as? TelegramGroup { } else if let group = peer as? TelegramGroup {
if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia { 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)) { if !(message.flags.isSending || message.flags.contains(.Failed)) {
optionsMap[id]!.insert(.forward) optionsMap[id]!.insert(.forward)
} }
@ -2057,7 +2066,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
} }
} }
} else if let user = peer as? TelegramUser { } 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)) { if !(message.flags.isSending || message.flags.contains(.Failed)) {
optionsMap[id]!.insert(.forward) optionsMap[id]!.insert(.forward)
} }

View File

@ -536,7 +536,11 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
} }
if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { 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 { } else {
return .none return .none
} }

View File

@ -3903,7 +3903,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .placeholder: case .placeholder:
return nil return nil
} }
}, state.items.count, state.hasUnseen) }, state.items.count, state.hasUnseen, state.hasUnseenCloseFriends)
} }
self.requestLayout(animated: false) self.requestLayout(animated: false)