Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-07-11 18:35:31 +02:00
commit 2b40dc2989
50 changed files with 1916 additions and 719 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";
@ -7090,6 +7090,7 @@ Sorry for the inconvenience.";
"Time.HoursAgo_many" = "%@ hours ago"; "Time.HoursAgo_many" = "%@ hours ago";
"Time.HoursAgo_0" = "%@ hours ago"; "Time.HoursAgo_0" = "%@ hours ago";
"Time.AtDate" = "%@"; "Time.AtDate" = "%@";
"Time.AtPreciseDate" = "%@ at %@";
"Stickers.ShowMore" = "Show More"; "Stickers.ShowMore" = "Show More";

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)?
#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
let rawText = self.presentationData.strings.SponsoredMessageInfoScreen_Text
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

@ -669,6 +669,8 @@ public final class AvatarNode: ASDisplayNode {
private var storyIndicator: ComponentView<Empty>? private var storyIndicator: ComponentView<Empty>?
public private(set) var storyPresentationParams: StoryPresentationParams? public private(set) var storyPresentationParams: StoryPresentationParams?
private var loadingStatuses = Bag<Disposable>()
public struct StoryStats: Equatable { public struct StoryStats: Equatable {
public var totalCount: Int public var totalCount: Int
public var unseenCount: Int public var unseenCount: Int
@ -742,6 +744,10 @@ public final class AvatarNode: ASDisplayNode {
self.addSubnode(self.contentNode) self.addSubnode(self.contentNode)
} }
deinit {
self.cancelLoading()
}
override public var frame: CGRect { override public var frame: CGRect {
get { get {
return super.frame return super.frame
@ -894,7 +900,8 @@ public final class AvatarNode: ASDisplayNode {
counters: AvatarStoryIndicatorComponent.Counters( counters: AvatarStoryIndicatorComponent.Counters(
totalCount: storyStats.totalCount, totalCount: storyStats.totalCount,
unseenCount: storyStats.unseenCount unseenCount: storyStats.unseenCount
) ),
displayProgress: !self.loadingStatuses.isEmpty
)), )),
environment: {}, environment: {},
containerSize: indicatorSize containerSize: indicatorSize
@ -918,4 +925,43 @@ public final class AvatarNode: ASDisplayNode {
} }
} }
} }
public func cancelLoading() {
for disposable in self.loadingStatuses.copyItems() {
disposable.dispose()
}
self.loadingStatuses.removeAll()
self.updateStoryIndicator(transition: .immediate)
}
public func pushLoadingStatus(signal: Signal<Never, NoError>) -> Disposable {
let disposable = MetaDisposable()
let index = self.loadingStatuses.add(disposable)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in
self?.updateStoryIndicator(transition: .immediate)
})
disposable.set(signal.start(completed: { [weak self] in
Queue.mainQueue().async {
guard let self else {
return
}
self.loadingStatuses.remove(index)
if self.loadingStatuses.isEmpty {
self.updateStoryIndicator(transition: .immediate)
}
}
}))
return ActionDisposable { [weak self] in
guard let self else {
return
}
self.loadingStatuses.remove(index)
if self.loadingStatuses.isEmpty {
self.updateStoryIndicator(transition: .immediate)
}
}
}
} }

View File

@ -118,6 +118,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var didAppear = false private var didAppear = false
private var dismissSearchOnDisappear = false private var dismissSearchOnDisappear = false
public var onDidAppear: (() -> Void)?
private var passcodeLockTooltipDisposable = MetaDisposable() private var passcodeLockTooltipDisposable = MetaDisposable()
private var didShowPasscodeLockTooltipController = false private var didShowPasscodeLockTooltipController = false
@ -187,6 +188,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private(set) var orderedStorySubscriptions: EngineStorySubscriptions? private(set) var orderedStorySubscriptions: EngineStorySubscriptions?
private var displayedStoriesTooltip: Bool = false private var displayedStoriesTooltip: Bool = false
public var hasStorySubscriptions: Bool {
if let rawStorySubscriptions = self.rawStorySubscriptions, !rawStorySubscriptions.items.isEmpty {
return true
} else {
return false
}
}
private var storyProgressDisposable: Disposable? private var storyProgressDisposable: Disposable?
private var storySubscriptionsDisposable: Disposable? private var storySubscriptionsDisposable: Disposable?
private var preloadStorySubscriptionsDisposable: Disposable? private var preloadStorySubscriptionsDisposable: Disposable?
@ -1059,7 +1068,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let navigationController = strongSelf.navigationController as? NavigationController { if let navigationController = strongSelf.navigationController as? NavigationController {
let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .chatList(groupId: groupId), controlsHistoryPreload: false, enableDebugActions: false) let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .chatList(groupId: groupId), controlsHistoryPreload: false, enableDebugActions: false)
chatListController.navigationPresentation = .master chatListController.navigationPresentation = .master
#if DEBUG && false
navigationController.pushViewController(chatListController, animated: false, completion: {})
chatListController.onDidAppear = { [weak chatListController] in
Queue.mainQueue().after(0.1, {
guard let chatListController else {
return
}
if chatListController.hasStorySubscriptions {
chatListController.scrollToStoriesAnimated()
}
})
}
#else
navigationController.pushViewController(chatListController) navigationController.pushViewController(chatListController)
#endif
strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true) strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true)
} }
} }
@ -1314,87 +1337,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else { guard let self else {
return return
} }
guard let itemNode = itemNode as? ChatListItemNode else {
let isHidden: Bool return
let focusedPeerId: EnginePeer.Id?
let singlePeer: Bool
switch subject {
case let .peer(peerId):
isHidden = self.location == .chatList(groupId: .archive)
focusedPeerId = peerId
singlePeer = true
case .archive:
isHidden = true
focusedPeerId = nil
singlePeer = false
} }
let storyContent = StoryContentContextImpl(context: self.context, isHidden: isHidden, focusedPeerId: focusedPeerId, singlePeer: singlePeer) switch subject {
let _ = (storyContent.state case .archive:
|> filter { $0.slice != nil } StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode)
|> take(1) case let .peer(peerId):
|> deliverOnMainQueue).start(next: { [weak self, weak itemNode] state in StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode)
guard let self else { }
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let itemNode = itemNode as? ChatListItemNode {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: itemNode.avatarNode.view,
sourceRect: itemNode.avatarNode.view.bounds,
sourceCornerRadius: itemNode.avatarNode.view.bounds.height * 0.5,
sourceIsAvatar: true
)
itemNode.avatarNode.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { _, _ in
if let itemNode = itemNode as? ChatListItemNode {
let transitionView = itemNode.avatarNode.view
let destinationView = itemNode.view
let rect = transitionView.convert(transitionView.bounds, to: destinationView)
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak transitionView] in
let parentView = UIView()
if let copyView = transitionView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: CGSize(width: state.destinationSize.width, height: state.destinationSize.height), amount: state.progress)
let scaleSize = state.sourceSize.interpolate(to: CGSize(width: state.destinationSize.width - 7.0, height: state.destinationSize.height - 7.0), amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: scaleSize.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: rect,
destinationCornerRadius: rect.height * 0.5,
destinationIsAvatar: true,
completed: { [weak itemNode] in
guard let itemNode else {
return
}
itemNode.avatarNode.isHidden = false
}
)
}
return nil
}
)
self.push(storyContainerScreen)
})
} }
self.chatListDisplayNode.peerContextAction = { [weak self] peer, source, node, gesture, location in self.chatListDisplayNode.peerContextAction = { [weak self] peer, source, node, gesture, location in
@ -1813,12 +1765,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|> filter { $0 } |> filter { $0 }
|> take(1)) |> take(1))
} else { } else {
self.storiesReady.set(.single(true)) let signals: [Signal<Bool, NoError>] = [
self.primaryInfoReady.get(),
self.storiesReady.get()
]
self.ready.set(combineLatest([ if case .chatList(.archive) = self.location {
self.chatListDisplayNode.mainContainerNode.ready, //signals.append(self.mainReady.get())
self.primaryInfoReady.get() } else {
]) self.storiesReady.set(.single(true))
}
self.ready.set(combineLatest(signals)
|> map { values -> Bool in |> map { values -> Bool in
return !values.contains(where: { !$0 }) return !values.contains(where: { !$0 })
} }
@ -1919,7 +1877,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.requestLayout(transition: transition) self.requestLayout(transition: transition)
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil
if rawStorySubscriptions.items.isEmpty { if !shouldDisplayStoriesInChatListHeader(storySubscriptions: rawStorySubscriptions, isHidden: self.location == .chatList(groupId: .archive)) {
self.chatListDisplayNode.scrollToTopIfStoriesAreExpanded() self.chatListDisplayNode.scrollToTopIfStoriesAreExpanded()
} }
@ -2022,7 +1980,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return return
} }
if let orderedStorySubscriptions = self.orderedStorySubscriptions, !orderedStorySubscriptions.items.isEmpty { if case .chatList(groupId: .root) = self.location, let orderedStorySubscriptions = self.orderedStorySubscriptions, !orderedStorySubscriptions.items.isEmpty {
let _ = (ApplicationSpecificNotice.displayChatListStoriesTooltip(accountManager: self.context.sharedContext.accountManager) let _ = (ApplicationSpecificNotice.displayChatListStoriesTooltip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] didDisplay in |> deliverOnMainQueue).start(next: { [weak self] didDisplay in
guard let self else { guard let self else {
@ -2162,6 +2120,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
guard case .chatList(.root) = self.location else { guard case .chatList(.root) = self.location else {
if !self.didSuggestLocalization {
self.didSuggestLocalization = true
let _ = (self.chatListDisplayNode.mainContainerNode.ready
|> filter { $0 }
|> take(1)
|> timeout(0.5, queue: .mainQueue(), alternate: .single(true))).start(next: { [weak self] _ in
guard let self else {
return
}
self.onDidAppear?()
})
}
return return
} }
@ -2362,6 +2334,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
} }
}) })
self.onDidAppear?()
} }
self.chatListDisplayNode.mainContainerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in self.chatListDisplayNode.mainContainerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in
@ -2941,6 +2915,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.chatListDisplayNode.scrollToStories(animated: false) self.chatListDisplayNode.scrollToStories(animated: false)
} }
public func scrollToStoriesAnimated() {
self.chatListDisplayNode.scrollToStories(animated: true)
}
private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
var tabContainerOffset: CGFloat = 0.0 var tabContainerOffset: CGFloat = 0.0
if !self.displayNavigationBar { if !self.displayNavigationBar {
@ -3584,6 +3562,88 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.shouldFixStorySubscriptionOrder = true self.shouldFixStorySubscriptionOrder = true
} }
} }
if peerId != self.context.account.peerId {
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
if let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView() {
let _ = storyPeerListView
StoryContainerScreen.openPeerStoriesCustom(
context: self.context,
peerId: peerId,
isHidden: self.location == .chatList(groupId: .archive),
singlePeer: false,
parentController: self,
transitionIn: { [weak self] in
guard let self else {
return nil
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
if let componentView = self.chatListHeaderView() {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
sourceCornerRadius: transitionView.bounds.height * 0.5,
sourceIsAvatar: true
)
}
}
}
}
return transitionIn
},
transitionOut: { [weak self] peerId in
guard let self else {
return nil
}
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked {
if let componentView = self.chatListHeaderView() {
if let (transitionView, transitionContentView) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
transitionView: transitionContentView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5,
destinationIsAvatar: true,
completed: {}
)
}
}
}
}
return nil
},
setFocusedItem: { [weak self] focusedItem in
guard let self else {
return
}
if let componentView = self.chatListHeaderView() {
componentView.storyPeerListView()?.setPreviewedItem(signal: focusedItem)
}
},
setProgress: { [weak self] signal in
guard let self else {
return
}
if let componentView = self.chatListHeaderView() {
componentView.storyPeerListView()?.setLoadingItem(peerId: peerId, signal: signal)
}
}
)
return
}
}
}
}
let storyContent = StoryContentContextImpl(context: self.context, isHidden: self.location == .chatList(groupId: .archive), focusedPeerId: peerId, singlePeer: false, fixedOrder: self.fixedStorySubscriptionOrder) let storyContent = StoryContentContextImpl(context: self.context, isHidden: self.location == .chatList(groupId: .archive), focusedPeerId: peerId, singlePeer: false, fixedOrder: self.fixedStorySubscriptionOrder)
let _ = (storyContent.state let _ = (storyContent.state
|> take(1) |> take(1)

View File

@ -434,7 +434,11 @@ private final class ChatListContainerItemNode: ASDisplayNode {
if case .forum = location { if case .forum = location {
subject = .forum(hasGeneral: hasOnlyGeneralThread) subject = .forum(hasGeneral: hasOnlyGeneralThread)
} else { } else {
subject = .chats(hasArchive: hasOnlyArchive) if case .chatList(groupId: .archive) = location {
subject = .archive
} else {
subject = .chats(hasArchive: hasOnlyArchive)
}
} }
} }

View File

@ -13,6 +13,7 @@ import AccountContext
final class ChatListEmptyNode: ASDisplayNode { final class ChatListEmptyNode: ASDisplayNode {
enum Subject { enum Subject {
case chats(hasArchive: Bool) case chats(hasArchive: Bool)
case archive
case filter(showEdit: Bool) case filter(showEdit: Bool)
case forum(hasGeneral: Bool) case forum(hasGeneral: Bool)
} }
@ -132,11 +133,14 @@ final class ChatListEmptyNode: ASDisplayNode {
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
let text: String let text: String
var descriptionText = "" var descriptionText = ""
let buttonText: String let buttonText: String?
switch self.subject { switch self.subject {
case let .chats(hasArchive): case let .chats(hasArchive):
text = hasArchive ? strings.ChatList_EmptyChatListWithArchive : strings.ChatList_EmptyChatList text = hasArchive ? strings.ChatList_EmptyChatListWithArchive : strings.ChatList_EmptyChatList
buttonText = strings.ChatList_EmptyChatListNewMessage buttonText = strings.ChatList_EmptyChatListNewMessage
case .archive:
text = strings.ChatList_EmptyChatList
buttonText = nil
case .filter: case .filter:
text = strings.ChatList_EmptyChatListFilterTitle text = strings.ChatList_EmptyChatListFilterTitle
descriptionText = strings.ChatList_EmptyChatListFilterText descriptionText = strings.ChatList_EmptyChatListFilterText
@ -152,7 +156,12 @@ final class ChatListEmptyNode: ASDisplayNode {
self.textNode.attributedText = string self.textNode.attributedText = string
self.descriptionNode.attributedText = descriptionString self.descriptionNode.attributedText = descriptionString
self.buttonNode.title = buttonText if let buttonText {
self.buttonNode.title = buttonText
self.buttonNode.isHidden = false
} else {
self.buttonNode.isHidden = true
}
self.activityIndicator.type = .custom(theme.list.itemAccentColor, 22.0, 1.0, false) self.activityIndicator.type = .custom(theme.list.itemAccentColor, 22.0, 1.0, false)

View File

@ -26,7 +26,6 @@ import TextNodeWithEntities
import ComponentFlow import ComponentFlow
import EmojiStatusComponent import EmojiStatusComponent
import AvatarVideoNode import AvatarVideoNode
import AvatarStoryIndicatorComponent
public enum ChatListItemContent { public enum ChatListItemContent {
public struct ThreadInfo: Equatable { public struct ThreadInfo: Equatable {
@ -941,7 +940,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarIconView: ComponentHostView<Empty>? var avatarIconView: ComponentHostView<Empty>?
var avatarIconComponent: EmojiStatusComponent? var avatarIconComponent: EmojiStatusComponent?
var avatarVideoNode: AvatarVideoNode? var avatarVideoNode: AvatarVideoNode?
var avatarStoryIndicator: ComponentView<Empty>? var avatarTapRecognizer: UITapGestureRecognizer?
private var inlineNavigationMarkLayer: SimpleLayer? private var inlineNavigationMarkLayer: SimpleLayer?
@ -1310,6 +1309,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
item.interaction.activateChatPreview(item, threadId, strongSelf.contextContainer, gesture, nil) item.interaction.activateChatPreview(item, threadId, strongSelf.contextContainer, gesture, nil)
} }
self.onDidLoad { [weak self] _ in
guard let self else {
return
}
let avatarTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.avatarStoryTapGesture(_:)))
self.avatarTapRecognizer = avatarTapRecognizer
self.avatarNode.view.addGestureRecognizer(avatarTapRecognizer)
}
} }
deinit { deinit {
@ -1327,28 +1335,48 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let previousItem = self.item let previousItem = self.item
self.item = item self.item = item
var storyState: ChatListItemContent.StoryState?
if case let .peer(peerData) = item.content {
storyState = peerData.storyState
} else if case let .groupReference(groupReference) = item.content {
storyState = groupReference.storyState
}
var peer: EnginePeer? var peer: EnginePeer?
var displayAsMessage = false var displayAsMessage = false
var enablePreview = true var enablePreview = true
switch item.content { switch item.content {
case let .peer(peerData): case let .peer(peerData):
displayAsMessage = peerData.displayAsMessage displayAsMessage = peerData.displayAsMessage
if displayAsMessage, case let .user(author) = peerData.messages.last?.author { if displayAsMessage, case let .user(author) = peerData.messages.last?.author {
peer = .user(author) peer = .user(author)
} else { } else {
peer = peerData.peer.chatMainPeer peer = peerData.peer.chatMainPeer
} }
if peerData.peer.peerId.namespace == Namespaces.Peer.SecretChat { if peerData.peer.peerId.namespace == Namespaces.Peer.SecretChat {
enablePreview = false enablePreview = false
} }
case let .groupReference(groupReferenceData): case let .groupReference(groupReferenceData):
if let previousItem = previousItem, case let .groupReference(previousGroupReferenceData) = previousItem.content, groupReferenceData.hiddenByDefault != previousGroupReferenceData.hiddenByDefault { if let previousItem = previousItem, case let .groupReference(previousGroupReferenceData) = previousItem.content, groupReferenceData.hiddenByDefault != previousGroupReferenceData.hiddenByDefault {
UIView.transition(with: self.avatarNode.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { UIView.transition(with: self.avatarNode.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
}, completion: nil) }, completion: nil)
} }
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReferenceData.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReferenceData.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
} }
self.avatarNode.setStoryStats(storyStats: storyState.flatMap { storyState in
return AvatarNode.StoryStats(
totalCount: storyState.stats.totalCount,
unseenCount: storyState.stats.unseenCount,
hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends
)
}, presentationParams: AvatarNode.StoryPresentationParams(
colors: AvatarNode.Colors(theme: item.presentationData.theme),
lineWidth: 2.33,
inactiveLineWidth: 1.33
), transition: .immediate)
self.avatarTapRecognizer?.isEnabled = storyState != nil
if let peer = peer { if let peer = peer {
var overrideImage: AvatarNodeImageOverride? var overrideImage: AvatarNodeImageOverride?
if peer.id.isReplies { if peer.id.isReplies {
@ -2792,13 +2820,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0) let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0)
var storyState: ChatListItemContent.StoryState?
if case let .peer(peerData) = item.content {
storyState = peerData.storyState
} else if case let .groupReference(groupReference) = item.content {
storyState = groupReference.storyState
}
let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
var avatarScaleOffset: CGFloat = 0.0 var avatarScaleOffset: CGFloat = 0.0
var avatarScale: CGFloat = 1.0 var avatarScale: CGFloat = 1.0
@ -2810,11 +2831,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
avatarScaleOffset = targetAvatarScaleOffset * inlineNavigationLocation.progress avatarScaleOffset = targetAvatarScaleOffset * inlineNavigationLocation.progress
} }
let storyIndicatorScale = avatarScale
if storyState != nil {
avatarScale *= (avatarFrame.width - 4.0 * 2.0) / avatarFrame.width
}
transition.updateFrame(node: strongSelf.avatarContainerNode, frame: avatarFrame) transition.updateFrame(node: strongSelf.avatarContainerNode, frame: avatarFrame)
transition.updatePosition(node: strongSelf.avatarNode, position: avatarFrame.offsetBy(dx: -avatarFrame.minX, dy: -avatarFrame.minY).center.offsetBy(dx: avatarScaleOffset, dy: 0.0)) transition.updatePosition(node: strongSelf.avatarNode, position: avatarFrame.offsetBy(dx: -avatarFrame.minX, dy: -avatarFrame.minY).center.offsetBy(dx: avatarScaleOffset, dy: 0.0))
transition.updateBounds(node: strongSelf.avatarNode, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) transition.updateBounds(node: strongSelf.avatarNode, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
@ -2822,55 +2838,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarNode.updateSize(size: avatarFrame.size) strongSelf.avatarNode.updateSize(size: avatarFrame.size)
strongSelf.updateVideoVisibility() strongSelf.updateVideoVisibility()
if let storyState {
var indicatorTransition = Transition(transition)
let avatarStoryIndicator: ComponentView<Empty>
if let current = strongSelf.avatarStoryIndicator {
avatarStoryIndicator = current
} else {
indicatorTransition = .immediate
avatarStoryIndicator = ComponentView()
strongSelf.avatarStoryIndicator = avatarStoryIndicator
}
var indicatorFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + 4.0, y: avatarFrame.minY + 4.0), size: CGSize(width: avatarFrame.width - 4.0 - 4.0, height: avatarFrame.height - 4.0 - 4.0))
indicatorFrame.origin.x -= (avatarFrame.width - avatarFrame.width * storyIndicatorScale) * 0.5
let _ = avatarStoryIndicator.update(
transition: indicatorTransition,
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyState.stats.unseenCount != 0,
hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends,
colors: AvatarStoryIndicatorComponent.Colors(theme: item.presentationData.theme),
activeLineWidth: 2.33,
inactiveLineWidth: 1.33,
counters: AvatarStoryIndicatorComponent.Counters(
totalCount: storyState.stats.totalCount,
unseenCount: storyState.stats.unseenCount
)
)),
environment: {},
containerSize: indicatorFrame.size
)
if let avatarStoryIndicatorView = avatarStoryIndicator.view {
if avatarStoryIndicatorView.superview == nil {
avatarStoryIndicatorView.isUserInteractionEnabled = true
avatarStoryIndicatorView.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.avatarStoryTapGesture(_:))))
strongSelf.contextContainer.view.insertSubview(avatarStoryIndicatorView, belowSubview: strongSelf.avatarContainerNode.view)
}
indicatorTransition.setPosition(view: avatarStoryIndicatorView, position: indicatorFrame.center)
indicatorTransition.setBounds(view: avatarStoryIndicatorView, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
indicatorTransition.setScale(view: avatarStoryIndicatorView, scale: storyIndicatorScale)
}
} else {
if let avatarStoryIndicator = strongSelf.avatarStoryIndicator {
strongSelf.avatarStoryIndicator = nil
avatarStoryIndicator.view?.removeFromSuperview()
}
}
var itemPeerId: EnginePeer.Id? var itemPeerId: EnginePeer.Id?
if case let .chatList(index) = item.index { if case let .chatList(index) = item.index {
itemPeerId = index.messageIndex.id.peerId itemPeerId = index.messageIndex.id.peerId
@ -3900,8 +3867,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let _ = item.interaction.inlineNavigationLocation { if let _ = item.interaction.inlineNavigationLocation {
} else { } else {
if let avatarStoryIndicatorView = self.avatarStoryIndicator?.view, let result = avatarStoryIndicatorView.hitTest(self.view.convert(point, to: avatarStoryIndicatorView), with: event) { if self.avatarNode.storyStats != nil {
return result if let result = self.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarNode.view), with: event) {
return result
}
} }
} }
@ -3919,7 +3888,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
case .groupReference: case .groupReference:
item.interaction.openStories(.archive, self) item.interaction.openStories(.archive, self)
} }
} }
} }
} }

View File

@ -1246,6 +1246,8 @@ public final class ChatListNode: ListView {
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
self.autoSetReady = autoSetReady self.autoSetReady = autoSetReady
let isMainTab = chatListFilter == nil && location == .chatList(groupId: .root)
var isSelecting = false var isSelecting = false
if case .peers(_, true, _, _, _, _) = mode { if case .peers(_, true, _, _, _, _) = mode {
isSelecting = true isSelecting = true
@ -1933,7 +1935,7 @@ public final class ChatListNode: ListView {
notice = nil notice = nil
} }
let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId) let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId, isMainTab: isMainTab)
var isEmpty = true var isEmpty = true
var entries = rawEntries.filter { entry in var entries = rawEntries.filter { entry in
switch entry { switch entry {
@ -2403,6 +2405,7 @@ public final class ChatListNode: ListView {
strongSelf.enqueueHistoryPreloadUpdate() strongSelf.enqueueHistoryPreloadUpdate()
} }
var refreshStoryPeerIds: [PeerId] = []
var isHiddenItemVisible = false var isHiddenItemVisible = false
if let range = range.visibleRange { if let range = range.visibleRange {
let entryCount = chatListView.filteredEntries.count let entryCount = chatListView.filteredEntries.count
@ -2418,6 +2421,11 @@ public final class ChatListNode: ListView {
if let threadInfo, threadInfo.isHidden { if let threadInfo, threadInfo.isHidden {
isHiddenItemVisible = true isHiddenItemVisible = true
} }
if let peer = peerEntry.peer.chatMainPeer, !peerEntry.isContact, case let .user(user) = peer {
refreshStoryPeerIds.append(user.id)
}
break break
case .GroupReferenceEntry: case .GroupReferenceEntry:
isHiddenItemVisible = true isHiddenItemVisible = true
@ -2433,6 +2441,9 @@ public final class ChatListNode: ListView {
return state return state
} }
} }
if !refreshStoryPeerIds.isEmpty {
strongSelf.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: refreshStoryPeerIds)
}
} }
} }

View File

@ -568,7 +568,17 @@ struct ChatListContactPeer {
} }
} }
func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer], accountPeerId: EnginePeer.Id) -> (entries: [ChatListNodeEntry], loading: Bool) { func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer], accountPeerId: EnginePeer.Id, isMainTab: Bool) -> (entries: [ChatListNodeEntry], loading: Bool) {
var groupItems = view.groupItems
if isMainTab && state.archiveStoryState != nil && groupItems.isEmpty {
groupItems.append(EngineChatList.GroupItem(
id: .archive,
topMessage: nil,
items: [],
unreadCount: 0
))
}
var result: [ChatListNodeEntry] = [] var result: [ChatListNodeEntry] = []
if !view.hasEarlier { if !view.hasEarlier {
@ -588,7 +598,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
if !view.hasLater, case .chatList = mode { if !view.hasLater, case .chatList = mode {
var groupEntryCount = 0 var groupEntryCount = 0
for _ in view.groupItems { for _ in groupItems {
groupEntryCount += 1 groupEntryCount += 1
} }
pinnedIndexOffset += UInt16(groupEntryCount) pinnedIndexOffset += UInt16(groupEntryCount)
@ -831,7 +841,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
} }
if !view.hasLater, case .chatList = mode { if !view.hasLater, case .chatList = mode {
for groupReference in view.groupItems { for groupReference in groupItems {
let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: EnginePeer.Id(0), namespace: 0, id: 0), timestamp: 1) let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: EnginePeer.Id(0), namespace: 0, id: 0), timestamp: 1)
var mappedStoryState: ChatListNodeState.StoryState? var mappedStoryState: ChatListNodeState.StoryState?
if let archiveStoryState = state.archiveStoryState { if let archiveStoryState = state.archiveStoryState {

View File

@ -513,51 +513,9 @@ public class ContactsController: ViewController {
return return
} }
let storyContent = StoryContentContextImpl(context: self.context, isHidden: true, focusedPeerId: peer.id, singlePeer: true) if let itemNode = sourceNode as? ContactsPeerItemNode {
let _ = (storyContent.state StoryContainerScreen.openPeerStories(context: self.context, peerId: peer.id, parentController: self, avatarNode: itemNode.avatarNode)
|> take(1) }
|> deliverOnMainQueue).start(next: { [weak self, weak sourceNode] storyContentState in
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let itemNode = sourceNode as? ContactsPeerItemNode {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: itemNode.avatarNode.view,
sourceRect: itemNode.avatarNode.view.bounds,
sourceCornerRadius: itemNode.avatarNode.view.bounds.height * 0.5,
sourceIsAvatar: true
)
itemNode.avatarNode.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { _, _ in
if let itemNode = sourceNode as? ContactsPeerItemNode {
let rect = itemNode.avatarNode.view.convert(itemNode.avatarNode.view.bounds, to: itemNode.view)
return StoryContainerScreen.TransitionOut(
destinationView: itemNode.view,
transitionView: nil,
destinationRect: rect,
destinationCornerRadius: rect.height * 0.5,
destinationIsAvatar: true,
completed: { [weak itemNode] in
guard let itemNode else {
return
}
itemNode.avatarNode.isHidden = false
}
)
}
return nil
}
)
self.push(storyContainerScreen)
})
} }
} }

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

@ -149,6 +149,7 @@ public final class TextNodeLayoutArguments {
public let textStroke: (UIColor, CGFloat)? public let textStroke: (UIColor, CGFloat)?
public let displaySpoilers: Bool public let displaySpoilers: Bool
public let displayEmbeddedItemsUnderSpoilers: Bool public let displayEmbeddedItemsUnderSpoilers: Bool
public let customTruncationToken: NSAttributedString?
public init( public init(
attributedString: NSAttributedString?, attributedString: NSAttributedString?,
@ -167,7 +168,8 @@ public final class TextNodeLayoutArguments {
textShadowBlur: CGFloat? = nil, textShadowBlur: CGFloat? = nil,
textStroke: (UIColor, CGFloat)? = nil, textStroke: (UIColor, CGFloat)? = nil,
displaySpoilers: Bool = false, displaySpoilers: Bool = false,
displayEmbeddedItemsUnderSpoilers: Bool = false displayEmbeddedItemsUnderSpoilers: Bool = false,
customTruncationToken: NSAttributedString? = nil
) { ) {
self.attributedString = attributedString self.attributedString = attributedString
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
@ -186,6 +188,7 @@ public final class TextNodeLayoutArguments {
self.textStroke = textStroke self.textStroke = textStroke
self.displaySpoilers = displaySpoilers self.displaySpoilers = displaySpoilers
self.displayEmbeddedItemsUnderSpoilers = displayEmbeddedItemsUnderSpoilers self.displayEmbeddedItemsUnderSpoilers = displayEmbeddedItemsUnderSpoilers
self.customTruncationToken = customTruncationToken
} }
public func withAttributedString(_ attributedString: NSAttributedString?) -> TextNodeLayoutArguments { public func withAttributedString(_ attributedString: NSAttributedString?) -> TextNodeLayoutArguments {
@ -206,7 +209,8 @@ public final class TextNodeLayoutArguments {
textShadowBlur: self.textShadowBlur, textShadowBlur: self.textShadowBlur,
textStroke: self.textStroke, textStroke: self.textStroke,
displaySpoilers: self.displaySpoilers, displaySpoilers: self.displaySpoilers,
displayEmbeddedItemsUnderSpoilers: self.displayEmbeddedItemsUnderSpoilers displayEmbeddedItemsUnderSpoilers: self.displayEmbeddedItemsUnderSpoilers,
customTruncationToken: self.customTruncationToken
) )
} }
} }
@ -998,7 +1002,7 @@ open class TextNode: ASDisplayNode {
} }
} }
static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool) -> TextNodeLayout { static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout {
if let attributedString = attributedString { if let attributedString = attributedString {
let stringLength = attributedString.length let stringLength = attributedString.length
@ -1168,7 +1172,17 @@ open class TextNode: ASDisplayNode {
layoutSize.height += fontLineSpacing layoutSize.height += fontLineSpacing
} }
let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) var didClipLinebreak = false
var lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex)
let nsString = (attributedString.string as NSString)
for i in lineRange.location ..< (lineRange.location + lineRange.length) {
if nsString.character(at: i) == 0x0a {
lineRange.length = max(0, i - lineRange.location)
didClipLinebreak = true
break
}
}
var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount) var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount)
if brokenLineRange.location + brokenLineRange.length > attributedString.length { if brokenLineRange.location + brokenLineRange.length > attributedString.length {
brokenLineRange.length = attributedString.length - brokenLineRange.location brokenLineRange.length = attributedString.length - brokenLineRange.location
@ -1186,16 +1200,44 @@ open class TextNode: ASDisplayNode {
lineConstrainedSize.width -= bottomCutoutSize.width lineConstrainedSize.width -= bottomCutoutSize.width
} }
if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(lineConstrainedSize.width) { let truncatedTokenString: NSAttributedString
coreTextLine = originalLine if let customTruncationToken {
truncatedTokenString = customTruncationToken
} else { } else {
var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:]
truncationTokenAttributes[NSAttributedString.Key.font] = font truncationTokenAttributes[NSAttributedString.Key.font] = font
truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber
let tokenString = "\u{2026}" let tokenString = "\u{2026}"
let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
}
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(lineConstrainedSize.width) {
if didClipLinebreak {
let mergedLine = NSMutableAttributedString()
mergedLine.append(attributedString.attributedSubstring(from: NSRange(location: lineRange.location, length: lineRange.length)))
mergedLine.append(truncatedTokenString)
coreTextLine = CTLineCreateWithAttributedString(mergedLine)
let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun]
for run in runs {
let runAttributes: NSDictionary = CTRunGetAttributes(run)
if let _ = runAttributes["CTForegroundColorFromContext"] {
brokenLineRange.length = CTRunGetStringRange(run).location - brokenLineRange.location
break
}
}
if brokenLineRange.location + brokenLineRange.length > attributedString.length {
brokenLineRange.length = attributedString.length - brokenLineRange.location
}
truncated = true
} else {
coreTextLine = originalLine
}
} else {
coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken
let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun] let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun]
for run in runs { for run in runs {
@ -1647,11 +1689,11 @@ open class TextNode: ASDisplayNode {
if stringMatch { if stringMatch {
layout = existingLayout layout = existingLayout
} else { } else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers) layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers, customTruncationToken: arguments.customTruncationToken)
updated = true updated = true
} }
} else { } else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers) layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers, customTruncationToken: arguments.customTruncationToken)
updated = true updated = true
} }
@ -2292,11 +2334,11 @@ open class TextView: UIView {
if stringMatch { if stringMatch {
layout = existingLayout layout = existingLayout
} else { } else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers) layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers, customTruncationToken: arguments.customTruncationToken)
updated = true updated = true
} }
} else { } else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers) layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers, displayEmbeddedItemsUnderSpoilers: arguments.displayEmbeddedItemsUnderSpoilers, customTruncationToken: arguments.customTruncationToken)
updated = true updated = true
} }

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

@ -274,14 +274,17 @@ func fetchPeerStoryStats(postbox: PostboxImpl, peerId: PeerId) -> PeerStoryStats
if topItems.id == 0 { if topItems.id == 0 {
return nil return nil
} }
guard let state = postbox.storyPeerStatesTable.get(key: .peer(peerId)) else {
return nil var maxSeenId: Int32 = 0
if let state = postbox.storyPeerStatesTable.get(key: .peer(peerId)) {
maxSeenId = state.maxSeenId
} }
if topItems.isExact { if topItems.isExact {
let stats = postbox.storyItemsTable.getStats(peerId: peerId, maxSeenId: state.maxSeenId) let stats = postbox.storyItemsTable.getStats(peerId: peerId, maxSeenId: maxSeenId)
return PeerStoryStats(totalCount: stats.total, unseenCount: stats.unseen) return PeerStoryStats(totalCount: stats.total, unseenCount: stats.unseen)
} else { } else {
return PeerStoryStats(totalCount: 1, unseenCount: topItems.id > state.maxSeenId ? 1 : 0) return PeerStoryStats(totalCount: 1, unseenCount: topItems.id > maxSeenId ? 1 : 0)
} }
} }

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

@ -296,6 +296,9 @@ public final class AccountViewTracker {
private var refreshStoriesForMessageIdsAndTimestamps: [MessageId: Int32] = [:] private var refreshStoriesForMessageIdsAndTimestamps: [MessageId: Int32] = [:]
private var nextUpdatedUnsupportedMediaDisposableId: Int32 = 0 private var nextUpdatedUnsupportedMediaDisposableId: Int32 = 0
private var updatedUnsupportedMediaDisposables = DisposableDict<Int32>() private var updatedUnsupportedMediaDisposables = DisposableDict<Int32>()
private var refreshStoriesForPeerIdsAndTimestamps: [PeerId: Int32] = [:]
private var refreshStoriesForPeerIdsDebounceDisposable: Disposable?
private var pendingRefreshStoriesForPeerIds: [PeerId] = []
private var updatedSeenPersonalMessageIds = Set<MessageId>() private var updatedSeenPersonalMessageIds = Set<MessageId>()
private var updatedReactionsSeenForMessageIds = Set<MessageId>() private var updatedReactionsSeenForMessageIds = Set<MessageId>()
@ -1262,6 +1265,89 @@ public final class AccountViewTracker {
} }
} }
public func refreshStoryStatsForPeerIds(peerIds: [PeerId]) {
self.queue.async {
self.pendingRefreshStoriesForPeerIds.append(contentsOf: peerIds)
if self.refreshStoriesForPeerIdsDebounceDisposable == nil {
self.refreshStoriesForPeerIdsDebounceDisposable = (Signal<Never, NoError>.complete() |> delay(0.15, queue: self.queue)).start(completed: {
self.refreshStoriesForPeerIdsDebounceDisposable = nil
let pendingPeerIds = self.pendingRefreshStoriesForPeerIds
self.pendingRefreshStoriesForPeerIds.removeAll()
self.internalRefreshStoryStatsForPeerIds(peerIds: pendingPeerIds)
})
}
}
}
private func internalRefreshStoryStatsForPeerIds(peerIds: [PeerId]) {
self.queue.async {
var addedPeerIds: [PeerId] = []
let timestamp = Int32(CFAbsoluteTimeGetCurrent())
for peerId in peerIds {
let messageTimestamp = self.refreshStoriesForPeerIdsAndTimestamps[peerId]
var refresh = false
if let messageTimestamp = messageTimestamp {
refresh = messageTimestamp < timestamp - 60
} else {
refresh = true
}
if refresh {
self.refreshStoriesForPeerIdsAndTimestamps[peerId] = timestamp
addedPeerIds.append(peerId)
}
}
if !addedPeerIds.isEmpty {
let disposableId = self.nextUpdatedUnsupportedMediaDisposableId
self.nextUpdatedUnsupportedMediaDisposableId += 1
if let account = self.account {
let signal = account.postbox.transaction { transaction -> [Api.InputUser] in
return addedPeerIds.compactMap { transaction.getPeer($0).flatMap(apiInputUser) }
}
|> mapToSignal { inputUsers -> Signal<Never, NoError> in
guard !inputUsers.isEmpty else {
return .complete()
}
var requests: [Signal<Never, NoError>] = []
let batchCount = 50
var startIndex = 0
while startIndex < inputUsers.count {
var slice: [Api.InputUser] = []
for i in startIndex ..< min(startIndex + batchCount, inputUsers.count) {
slice.append(inputUsers[i])
}
startIndex += batchCount
requests.append(account.network.request(Api.functions.users.getUsers(id: slice))
|> `catch` { _ -> Signal<[Api.User], NoError> in
return .single([])
}
|> mapToSignal { result -> Signal<Never, NoError> in
return account.postbox.transaction { transaction in
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: result))
}
|> ignoreValues
})
}
return combineLatest(requests)
|> ignoreValues
}
|> afterDisposed { [weak self] in
self?.queue.async {
self?.updatedUnsupportedMediaDisposables.set(nil, forKey: disposableId)
}
}
self.updatedUnsupportedMediaDisposables.set(signal.start(), forKey: disposableId)
}
}
}
}
public func updateMarkAllMentionsSeen(peerId: PeerId, threadId: Int64?) { public func updateMarkAllMentionsSeen(peerId: PeerId, threadId: Int64?) {
self.queue.async { self.queue.async {
guard let account = self.account else { guard let account = self.account else {

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

@ -49,6 +49,9 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul
if let storiesMaxId = storiesMaxId { if let storiesMaxId = storiesMaxId {
transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: storiesMaxId) transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: storiesMaxId)
} }
/*#if DEBUG
transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: 10)
#endif*/
case .userEmpty: case .userEmpty:
break break
} }

View File

@ -362,7 +362,7 @@ public func stringForRelativeLiveLocationUpdateTimestamp(strings: PresentationSt
} }
} }
public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String { public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String {
let difference = timestamp - relativeTimestamp let difference = timestamp - relativeTimestamp
if difference < 60 { if difference < 60 {
return strings.Time_JustNow return strings.Time_JustNow
@ -392,6 +392,8 @@ public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dat
day = .yesterday day = .yesterday
} }
return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min).string return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min).string
} else if preciseTime {
return strings.Time_AtPreciseDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat), stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)).string
} else { } else {
return strings.Time_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string return strings.Time_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string
} }

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

@ -1009,11 +1009,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
var transitionIn: StoryContainerScreen.TransitionIn? var transitionIn: StoryContainerScreen.TransitionIn?
let story = item.story let story = item.story
var foundItem: SparseItemGridDisplayItem?
var foundItemLayer: SparseItemGridLayer? var foundItemLayer: SparseItemGridLayer?
self.itemGrid.forEachVisibleItem { item in self.itemGrid.forEachVisibleItem { item in
guard let itemLayer = item.layer as? ItemLayer else { guard let itemLayer = item.layer as? ItemLayer else {
return return
} }
foundItem = item
if let listItem = itemLayer.item, listItem.story.id == story.id { if let listItem = itemLayer.item, listItem.story.id == story.id {
foundItemLayer = itemLayer foundItemLayer = itemLayer
} }
@ -1026,6 +1028,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
sourceCornerRadius: 0.0, sourceCornerRadius: 0.0,
sourceIsAvatar: false sourceIsAvatar: false
) )
if let blurLayer = foundItem?.blurLayer {
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
transition.setAlpha(layer: blurLayer, alpha: 0.0)
}
} }
let storyContainerScreen = StoryContainerScreen( let storyContainerScreen = StoryContainerScreen(
@ -1037,16 +1044,23 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return nil return nil
} }
var foundItem: SparseItemGridDisplayItem?
var foundItemLayer: SparseItemGridLayer? var foundItemLayer: SparseItemGridLayer?
self.itemGrid.forEachVisibleItem { item in self.itemGrid.forEachVisibleItem { item in
guard let itemLayer = item.layer as? ItemLayer else { guard let itemLayer = item.layer as? ItemLayer else {
return return
} }
foundItem = item
if let listItem = itemLayer.item, AnyHashable(listItem.story.id) == itemId { if let listItem = itemLayer.item, AnyHashable(listItem.story.id) == itemId {
foundItemLayer = itemLayer foundItemLayer = itemLayer
} }
} }
if let foundItemLayer { if let foundItemLayer {
if let blurLayer = foundItem?.blurLayer {
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
transition.setAlpha(layer: blurLayer, alpha: 1.0)
}
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer) let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
return StoryContainerScreen.TransitionOut( return StoryContainerScreen.TransitionOut(
destinationView: self.view, destinationView: self.view,
@ -1487,29 +1501,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 +1573,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 +1584,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)
@ -1875,11 +1876,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} }
private func updateHiddenItems() { private func updateHiddenItems() {
self.itemGrid.forEachVisibleItem { item in self.itemGrid.forEachVisibleItem { itemValue in
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else { guard let itemLayer = itemValue.layer as? ItemLayer, let item = itemLayer.item else {
return return
} }
itemLayer.isHidden = self.itemInteraction.hiddenMedia.contains(item.story.id) let itemHidden = self.itemInteraction.hiddenMedia.contains(item.story.id)
itemLayer.isHidden = itemHidden
if let blurLayer = itemValue.blurLayer {
let transition = Transition.immediate
if itemHidden {
transition.setAlpha(layer: blurLayer, alpha: 0.0)
} else {
transition.setAlpha(layer: blurLayer, alpha: 1.0)
}
}
} }
} }

View File

@ -13,6 +13,7 @@ swift_library(
"//submodules/Display", "//submodules/Display",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/TelegramPresentationData", "//submodules/TelegramPresentationData",
"//submodules/Components/HierarchyTrackingLayer",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit import UIKit
import Display import Display
import ComponentFlow import ComponentFlow
import HierarchyTrackingLayer
import TelegramPresentationData import TelegramPresentationData
public final class AvatarStoryIndicatorComponent: Component { public final class AvatarStoryIndicatorComponent: Component {
@ -43,6 +44,7 @@ public final class AvatarStoryIndicatorComponent: Component {
public let activeLineWidth: CGFloat public let activeLineWidth: CGFloat
public let inactiveLineWidth: CGFloat public let inactiveLineWidth: CGFloat
public let counters: Counters? public let counters: Counters?
public let displayProgress: Bool
public init( public init(
hasUnseen: Bool, hasUnseen: Bool,
@ -50,7 +52,8 @@ public final class AvatarStoryIndicatorComponent: Component {
colors: Colors, colors: Colors,
activeLineWidth: CGFloat, activeLineWidth: CGFloat,
inactiveLineWidth: CGFloat, inactiveLineWidth: CGFloat,
counters: Counters? counters: Counters?,
displayProgress: Bool = false
) { ) {
self.hasUnseen = hasUnseen self.hasUnseen = hasUnseen
self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems
@ -58,6 +61,7 @@ public final class AvatarStoryIndicatorComponent: Component {
self.activeLineWidth = activeLineWidth self.activeLineWidth = activeLineWidth
self.inactiveLineWidth = inactiveLineWidth self.inactiveLineWidth = inactiveLineWidth
self.counters = counters self.counters = counters
self.displayProgress = displayProgress
} }
public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool { public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool {
@ -79,11 +83,167 @@ public final class AvatarStoryIndicatorComponent: Component {
if lhs.counters != rhs.counters { if lhs.counters != rhs.counters {
return false return false
} }
if lhs.displayProgress != rhs.displayProgress {
return false
}
return true return true
} }
private final class ProgressLayer: HierarchyTrackingLayer {
enum Value: Equatable {
case indefinite
case progress(Float)
}
private struct Params: Equatable {
var size: CGSize
var lineWidth: CGFloat
var value: Value
}
private var currentParams: Params?
private let uploadProgressLayer = SimpleShapeLayer()
private let indefiniteDashLayer = SimpleShapeLayer()
private let indefiniteReplicatorLayer = CAReplicatorLayer()
override init() {
super.init()
self.uploadProgressLayer.fillColor = nil
self.uploadProgressLayer.strokeColor = UIColor.white.cgColor
self.uploadProgressLayer.lineCap = .round
self.indefiniteDashLayer.fillColor = nil
self.indefiniteDashLayer.strokeColor = UIColor.white.cgColor
self.indefiniteDashLayer.lineCap = .round
self.indefiniteDashLayer.lineJoin = .round
self.indefiniteDashLayer.strokeEnd = 0.0333
let count = 1.0 / self.indefiniteDashLayer.strokeEnd
let angle = (2.0 * Double.pi) / Double(count)
self.indefiniteReplicatorLayer.addSublayer(self.indefiniteDashLayer)
self.indefiniteReplicatorLayer.instanceCount = Int(count)
self.indefiniteReplicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
self.indefiniteReplicatorLayer.transform = CATransform3DMakeRotation(-.pi / 2.0, 0.0, 0.0, 1.0)
self.indefiniteReplicatorLayer.instanceDelay = 0.025
self.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.updateAnimations(transition: .immediate)
}
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reset() {
self.currentParams = nil
self.indefiniteDashLayer.path = nil
self.uploadProgressLayer.path = nil
}
func updateAnimations(transition: Transition) {
guard let params = self.currentParams else {
return
}
switch params.value {
case let .progress(progress):
if self.indefiniteReplicatorLayer.superlayer != nil {
self.indefiniteReplicatorLayer.removeFromSuperlayer()
}
if self.uploadProgressLayer.superlayer == nil {
self.addSublayer(self.uploadProgressLayer)
}
transition.setShapeLayerStrokeEnd(layer: self.uploadProgressLayer, strokeEnd: CGFloat(progress))
if self.uploadProgressLayer.animation(forKey: "rotation") == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.duration = 2.0
rotationAnimation.fromValue = NSNumber(value: Float(0.0))
rotationAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
rotationAnimation.repeatCount = Float.infinity
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
self.uploadProgressLayer.add(rotationAnimation, forKey: "rotation")
}
case .indefinite:
if self.uploadProgressLayer.superlayer == nil {
self.uploadProgressLayer.removeFromSuperlayer()
}
if self.indefiniteReplicatorLayer.superlayer == nil {
self.addSublayer(self.indefiniteReplicatorLayer)
}
if self.indefiniteReplicatorLayer.animation(forKey: "rotation") == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.duration = 4.0
rotationAnimation.fromValue = NSNumber(value: -.pi / 2.0)
rotationAnimation.toValue = NSNumber(value: -.pi / 2.0 + Double.pi * 2.0)
rotationAnimation.repeatCount = Float.infinity
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
self.indefiniteReplicatorLayer.add(rotationAnimation, forKey: "rotation")
}
if self.indefiniteDashLayer.animation(forKey: "dash") == nil {
let dashAnimation = CAKeyframeAnimation(keyPath: "strokeStart")
dashAnimation.keyTimes = [0.0, 0.45, 0.55, 1.0]
dashAnimation.values = [
self.indefiniteDashLayer.strokeStart,
self.indefiniteDashLayer.strokeEnd,
self.indefiniteDashLayer.strokeEnd,
self.indefiniteDashLayer.strokeStart,
]
dashAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
dashAnimation.duration = 2.5
dashAnimation.repeatCount = .infinity
self.indefiniteDashLayer.add(dashAnimation, forKey: "dash")
}
}
}
func update(size: CGSize, radius: CGFloat, lineWidth: CGFloat, value: Value, transition: Transition) {
let params = Params(
size: size,
lineWidth: lineWidth,
value: value
)
if self.currentParams == params {
return
}
self.currentParams = params
self.indefiniteDashLayer.lineWidth = lineWidth
self.uploadProgressLayer.lineWidth = lineWidth
let bounds = CGRect(origin: .zero, size: size)
if self.uploadProgressLayer.path == nil {
let path = CGMutablePath()
path.addEllipse(in: CGRect(origin: CGPoint(x: (size.width - radius * 2.0) * 0.5, y: (size.height - radius * 2.0) * 0.5), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
self.uploadProgressLayer.path = path
self.uploadProgressLayer.frame = bounds
}
if self.indefiniteDashLayer.path == nil {
let path = CGMutablePath()
path.addEllipse(in: CGRect(origin: CGPoint(x: (size.width - radius * 2.0) * 0.5, y: (size.height - radius * 2.0) * 0.5), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
self.indefiniteDashLayer.path = path
self.indefiniteReplicatorLayer.frame = bounds
self.indefiniteDashLayer.frame = bounds
}
self.updateAnimations(transition: transition)
}
}
public final class View: UIView { public final class View: UIView {
private let indicatorView: UIImageView private let indicatorView: UIImageView
private var progressLayer: ProgressLayer?
private var colorLayer: SimpleGradientLayer?
private var component: AvatarStoryIndicatorComponent? private var component: AvatarStoryIndicatorComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -110,25 +270,26 @@ public final class AvatarStoryIndicatorComponent: Component {
diameter = availableSize.width + maxOuterInset * 2.0 diameter = availableSize.width + maxOuterInset * 2.0
let imageDiameter = availableSize.width + ceilToScreenPixels(maxOuterInset) * 2.0 let imageDiameter = availableSize.width + ceilToScreenPixels(maxOuterInset) * 2.0
let activeColors: [CGColor]
let inactiveColors: [CGColor]
if component.hasUnseenCloseFriendsItems {
activeColors = component.colors.unseenCloseFriendsColors.map(\.cgColor)
} else {
activeColors = component.colors.unseenColors.map(\.cgColor)
}
inactiveColors = component.colors.seenColors.map(\.cgColor)
let radius = (diameter - component.activeLineWidth) * 0.5
self.indicatorView.image = generateImage(CGSize(width: imageDiameter, height: imageDiameter), rotatedContext: { size, context in self.indicatorView.image = generateImage(CGSize(width: imageDiameter, height: imageDiameter), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
let activeColors: [CGColor]
let inactiveColors: [CGColor]
if component.hasUnseenCloseFriendsItems {
activeColors = component.colors.unseenCloseFriendsColors.map(\.cgColor)
} else {
activeColors = component.colors.unseenColors.map(\.cgColor)
}
inactiveColors = component.colors.seenColors.map(\.cgColor)
var locations: [CGFloat] = [0.0, 1.0] var locations: [CGFloat] = [0.0, 1.0]
if let counters = component.counters, counters.totalCount > 1 { if let counters = component.counters, counters.totalCount > 1 {
let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
let radius = (diameter - component.activeLineWidth) * 0.5
let spacing: CGFloat = 2.0 let spacing: CGFloat = 2.0
let angularSpacing: CGFloat = spacing / radius let angularSpacing: CGFloat = spacing / radius
let circleLength = CGFloat.pi * 2.0 * radius let circleLength = CGFloat.pi * 2.0 * radius
@ -197,7 +358,61 @@ public final class AvatarStoryIndicatorComponent: Component {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
} }
}) })
transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter))) let indicatorFrame = CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter))
transition.setFrame(view: self.indicatorView, frame: indicatorFrame)
let progressTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut))
if component.displayProgress {
let colorLayer: SimpleGradientLayer
if let current = self.colorLayer {
colorLayer = current
} else {
colorLayer = SimpleGradientLayer()
self.colorLayer = colorLayer
self.layer.addSublayer(colorLayer)
colorLayer.opacity = 0.0
}
progressTransition.setAlpha(view: self.indicatorView, alpha: 0.0)
progressTransition.setAlpha(layer: colorLayer, alpha: 1.0)
let colors: [CGColor] = activeColors
/*if component.hasUnseen {
colors = activeColors
} else {
colors = inactiveColors
}*/
let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth
colorLayer.colors = colors
colorLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
colorLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
let progressLayer: ProgressLayer
if let current = self.progressLayer {
progressLayer = current
} else {
progressLayer = ProgressLayer()
self.progressLayer = progressLayer
colorLayer.mask = progressLayer
}
colorLayer.frame = indicatorFrame
progressLayer.frame = CGRect(origin: CGPoint(), size: indicatorFrame.size)
progressLayer.update(size: indicatorFrame.size, radius: radius, lineWidth: lineWidth, value: .indefinite, transition: .immediate)
} else {
progressTransition.setAlpha(view: self.indicatorView, alpha: 1.0)
self.progressLayer = nil
if let colorLayer = self.colorLayer {
self.colorLayer = nil
progressTransition.setAlpha(layer: colorLayer, alpha: 0.0, completion: { [weak colorLayer] _ in
colorLayer?.removeFromSuperlayer()
})
}
}
return availableSize return availableSize
} }

View File

@ -57,7 +57,7 @@ public final class PeerListItemComponent: Component {
let selectionState: SelectionState let selectionState: SelectionState
let hasNext: Bool let hasNext: Bool
let action: (EnginePeer) -> Void let action: (EnginePeer) -> Void
let openStories: ((EnginePeer, UIView) -> Void)? let openStories: ((EnginePeer, AvatarNode) -> Void)?
public init( public init(
context: AccountContext, context: AccountContext,
@ -74,7 +74,7 @@ public final class PeerListItemComponent: Component {
selectionState: SelectionState, selectionState: SelectionState,
hasNext: Bool, hasNext: Bool,
action: @escaping (EnginePeer) -> Void, action: @escaping (EnginePeer) -> Void,
openStories: ((EnginePeer, UIView) -> Void)? = nil openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
@ -211,7 +211,7 @@ public final class PeerListItemComponent: Component {
guard let component = self.component, let peer = component.peer else { guard let component = self.component, let peer = component.peer else {
return return
} }
component.openStories?(peer, self.avatarNode.view) component.openStories?(peer, self.avatarNode)
} }
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {

View File

@ -76,6 +76,7 @@ swift_library(
"//submodules/OpenInExternalAppUI", "//submodules/OpenInExternalAppUI",
"//submodules/MediaPasteboardUI", "//submodules/MediaPasteboardUI",
"//submodules/WebPBinding", "//submodules/WebPBinding",
"//submodules/Utils/RangeSet",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -0,0 +1,205 @@
import Foundation
import UIKit
import Display
import AccountContext
import SwiftSignalKit
import TelegramCore
import Postbox
import AvatarNode
public extension StoryContainerScreen {
static func openArchivedStories(context: AccountContext, parentController: ViewController, avatarNode: AvatarNode) {
let storyContent = StoryContentContextImpl(context: context, isHidden: true, focusedPeerId: nil, singlePeer: false)
let signal = storyContent.state
|> take(1)
|> mapToSignal { state -> Signal<Void, NoError> in
if let slice = state.slice {
return waitUntilStoryMediaPreloaded(context: context, peerId: slice.peer.id, storyItem: slice.item.storyItem)
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|> map { _ -> Void in
}
|> then(.single(Void()))
} else {
return .single(Void())
}
}
|> deliverOnMainQueue
|> map { [weak parentController, weak avatarNode] _ -> Void in
var transitionIn: StoryContainerScreen.TransitionIn?
if let avatarNode {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: avatarNode.view,
sourceRect: avatarNode.view.bounds,
sourceCornerRadius: avatarNode.view.bounds.width * 0.5,
sourceIsAvatar: false
)
avatarNode.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
if let avatarNode {
let destinationView = avatarNode.view
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak avatarNode] in
guard let avatarNode else {
return
}
avatarNode.isHidden = false
}
)
} else {
return nil
}
}
)
parentController?.push(storyContainerScreen)
}
|> ignoreValues
let _ = avatarNode.pushLoadingStatus(signal: signal)
}
static func openPeerStories(context: AccountContext, peerId: EnginePeer.Id, parentController: ViewController, avatarNode: AvatarNode) {
return openPeerStoriesCustom(
context: context,
peerId: peerId,
isHidden: false,
singlePeer: true,
parentController: parentController,
transitionIn: { [weak avatarNode] in
if let avatarNode {
let transitionIn = StoryContainerScreen.TransitionIn(
sourceView: avatarNode.view,
sourceRect: avatarNode.view.bounds,
sourceCornerRadius: avatarNode.view.bounds.width * 0.5,
sourceIsAvatar: false
)
avatarNode.isHidden = true
return transitionIn
} else {
return nil
}
},
transitionOut: { [weak avatarNode] _ in
if let avatarNode {
let destinationView = avatarNode.view
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak avatarNode] in
guard let avatarNode else {
return
}
avatarNode.isHidden = false
}
)
} else {
return nil
}
},
setFocusedItem: { _ in
},
setProgress: { [weak avatarNode] signal in
guard let avatarNode else {
return
}
let _ = avatarNode.pushLoadingStatus(signal: signal)
}
)
}
static func openPeerStoriesCustom(
context: AccountContext,
peerId: EnginePeer.Id,
isHidden: Bool,
singlePeer: Bool,
parentController: ViewController,
transitionIn: @escaping () -> StoryContainerScreen.TransitionIn?,
transitionOut: @escaping (EnginePeer.Id) -> StoryContainerScreen.TransitionOut?,
setFocusedItem: @escaping (Signal<StoryId?, NoError>) -> Void,
setProgress: @escaping (Signal<Never, NoError>) -> Void
) {
let storyContent = StoryContentContextImpl(context: context, isHidden: isHidden, focusedPeerId: peerId, singlePeer: singlePeer)
let signal = storyContent.state
|> take(1)
|> mapToSignal { state -> Signal<StoryContentContextState, NoError> in
if let slice = state.slice {
return waitUntilStoryMediaPreloaded(context: context, peerId: slice.peer.id, storyItem: slice.item.storyItem)
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|> map { _ -> StoryContentContextState in
}
|> then(.single(state))
} else {
return .single(state)
}
}
|> deliverOnMainQueue
|> map { [weak parentController] state -> Void in
if state.slice == nil {
return
}
let transitionIn: StoryContainerScreen.TransitionIn? = transitionIn()
let storyContainerScreen = StoryContainerScreen(
context: context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
return transitionOut(peerId)
}
)
setFocusedItem(storyContainerScreen.focusedItem)
parentController?.push(storyContainerScreen)
}
|> ignoreValues
setProgress(signal)
}
}

View File

@ -8,15 +8,22 @@ import TelegramStringFormatting
import MultilineTextComponent import MultilineTextComponent
final class StoryAuthorInfoComponent: Component { final class StoryAuthorInfoComponent: Component {
struct Counters: Equatable {
var position: Int
var totalCount: Int
}
let context: AccountContext let context: AccountContext
let peer: EnginePeer? let peer: EnginePeer?
let timestamp: Int32 let timestamp: Int32
let counters: Counters?
let isEdited: Bool let isEdited: Bool
init(context: AccountContext, peer: EnginePeer?, timestamp: Int32, isEdited: Bool) { init(context: AccountContext, peer: EnginePeer?, timestamp: Int32, counters: Counters?, isEdited: Bool) {
self.context = context self.context = context
self.peer = peer self.peer = peer
self.timestamp = timestamp self.timestamp = timestamp
self.counters = counters
self.isEdited = isEdited self.isEdited = isEdited
} }
@ -30,6 +37,9 @@ final class StoryAuthorInfoComponent: Component {
if lhs.timestamp != rhs.timestamp { if lhs.timestamp != rhs.timestamp {
return false return false
} }
if lhs.counters != rhs.counters {
return false
}
if lhs.isEdited != rhs.isEdited { if lhs.isEdited != rhs.isEdited {
return false return false
} }
@ -39,6 +49,7 @@ final class StoryAuthorInfoComponent: Component {
final class View: UIView { final class View: UIView {
private let title = ComponentView<Empty>() private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>() private let subtitle = ComponentView<Empty>()
private var counterLabel: ComponentView<Empty>?
private var component: StoryAuthorInfoComponent? private var component: StoryAuthorInfoComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -71,7 +82,7 @@ final class StoryAuthorInfoComponent: Component {
} }
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: component.timestamp, relativeTo: timestamp) var subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, preciseTime: true, relativeTimestamp: component.timestamp, relativeTo: timestamp)
if component.isEdited { if component.isEdited {
subtitle.append("") subtitle.append("")
@ -118,6 +129,36 @@ final class StoryAuthorInfoComponent: Component {
transition.setFrame(view: subtitleView, frame: subtitleFrame) transition.setFrame(view: subtitleView, frame: subtitleFrame)
} }
let countersSpacing: CGFloat = 5.0
if let counters = component.counters {
let counterLabel: ComponentView<Empty>
if let current = self.counterLabel {
counterLabel = current
} else {
counterLabel = ComponentView()
self.counterLabel = counterLabel
}
let counterSize = counterLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "\(counters.position + 1)/\(counters.totalCount)", font: Font.regular(11.0), textColor: UIColor(white: 1.0, alpha: 0.43))),
truncationType: .end,
maximumNumberOfLines: 1
)),
environment: {},
containerSize: CGSize(width: max(1.0, availableSize.width - titleSize.width - countersSpacing), height: 100.0)
)
if let counterLabelView = counterLabel.view {
if counterLabelView.superview == nil {
self.addSubview(counterLabelView)
}
counterLabelView.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + countersSpacing, y: titleFrame.minY + 1.0 + floorToScreenPixels((titleFrame.height - counterSize.height) * 0.5)), size: counterSize)
}
} else if let counterLabel = self.counterLabel {
self.counterLabel = nil
counterLabel.view?.removeFromSuperview()
}
return size return size
} }
} }

View File

@ -7,6 +7,7 @@ import AccountContext
import TelegramCore import TelegramCore
import Postbox import Postbox
import MediaResources import MediaResources
import RangeSet
private struct StoryKey: Hashable { private struct StoryKey: Hashable {
var peerId: EnginePeer.Id var peerId: EnginePeer.Id
@ -283,6 +284,7 @@ public final class StoryContentContextImpl: StoryContentContext {
let allItems = mappedItems.map { item in let allItems = mappedItems.map { item in
return StoryContentItem( return StoryContentItem(
position: nil, position: nil,
dayCounters: nil,
peerId: peer.id, peerId: peer.id,
storyItem: item, storyItem: item,
entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles) entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles)
@ -295,6 +297,7 @@ public final class StoryContentContextImpl: StoryContentContext {
additionalPeerData: additionalPeerData, additionalPeerData: additionalPeerData,
item: StoryContentItem( item: StoryContentItem(
position: mappedFocusedIndex ?? focusedIndex, position: mappedFocusedIndex ?? focusedIndex,
dayCounters: nil,
peerId: peer.id, peerId: peer.id,
storyItem: mappedItem, storyItem: mappedItem,
entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles) entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles)
@ -758,6 +761,8 @@ public final class StoryContentContextImpl: StoryContentContext {
}) })
} }
} }
} else {
self.updateState()
} }
} }
@ -1027,6 +1032,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
let mainItem = StoryContentItem( let mainItem = StoryContentItem(
position: 0, position: 0,
dayCounters: nil,
peerId: peer.id, peerId: peer.id,
storyItem: mappedItem, storyItem: mappedItem,
entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles) entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles)
@ -1171,20 +1177,58 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
} }
} }
struct DayIndex: Hashable {
var year: Int32
var day: Int32
init(timestamp: Int32) {
var time: time_t = time_t(timestamp)
var timeinfo: tm = tm()
localtime_r(&time, &timeinfo)
self.year = timeinfo.tm_year
self.day = timeinfo.tm_yday
}
}
let stateValue: StoryContentContextState let stateValue: StoryContentContextState
if let focusedIndex = focusedIndex { if let focusedIndex = focusedIndex {
let item = state.items[focusedIndex] let item = state.items[focusedIndex]
self.focusedId = item.id self.focusedId = item.id
var allItems: [StoryContentItem] = [] var allItems: [StoryContentItem] = []
var dayCounts: [DayIndex: Int] = [:]
var itemDayIndices: [Int32: (Int, DayIndex)] = [:]
for i in 0 ..< state.items.count { for i in 0 ..< state.items.count {
let stateItem = state.items[i] let stateItem = state.items[i]
allItems.append(StoryContentItem( allItems.append(StoryContentItem(
position: i, position: i,
dayCounters: nil,
peerId: peer.id, peerId: peer.id,
storyItem: stateItem, storyItem: stateItem,
entityFiles: extractItemEntityFiles(item: stateItem, allEntityFiles: state.allEntityFiles) entityFiles: extractItemEntityFiles(item: stateItem, allEntityFiles: state.allEntityFiles)
)) ))
let day = DayIndex(timestamp: stateItem.timestamp)
let dayCount: Int
if let current = dayCounts[day] {
dayCount = current + 1
dayCounts[day] = dayCount
} else {
dayCount = 1
dayCounts[day] = dayCount
}
itemDayIndices[stateItem.id] = (dayCount - 1, day)
}
var dayCounters: StoryContentItem.DayCounters?
if let (offset, day) = itemDayIndices[item.id], let dayCount = dayCounts[day] {
dayCounters = StoryContentItem.DayCounters(
position: offset,
totalCount: dayCount
)
} }
stateValue = StoryContentContextState( stateValue = StoryContentContextState(
@ -1193,6 +1237,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
additionalPeerData: additionalPeerData, additionalPeerData: additionalPeerData,
item: StoryContentItem( item: StoryContentItem(
position: focusedIndex, position: focusedIndex,
dayCounters: dayCounters,
peerId: peer.id, peerId: peer.id,
storyItem: item, storyItem: item,
entityFiles: extractItemEntityFiles(item: item, allEntityFiles: state.allEntityFiles) entityFiles: extractItemEntityFiles(item: item, allEntityFiles: state.allEntityFiles)
@ -1381,6 +1426,88 @@ public func preloadStoryMedia(context: AccountContext, peer: PeerReference, stor
return combineLatest(signals) |> ignoreValues return combineLatest(signals) |> ignoreValues
} }
public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: EnginePeer.Id, storyItem: EngineStoryItem) -> Signal<Never, NoError> {
return context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> mapToSignal { peerValue -> Signal<Never, NoError> in
guard let peerValue else {
return .complete()
}
guard let peer = PeerReference(peerValue._asPeer()) else {
return .complete()
}
var statusSignals: [Signal<Never, NoError>] = []
var loadSignals: [Signal<Never, NoError>] = []
switch storyItem.media {
case let .image(image):
if let representation = largestImageRepresentation(image.representations) {
statusSignals.append(
context.account.postbox.mediaBox.resourceData(representation.resource)
|> filter { data in
return data.complete
}
|> take(1)
|> ignoreValues
)
loadSignals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .other, reference: .media(media: .story(peer: peer, id: storyItem.id, media: storyItem.media._asMedia()), resource: representation.resource), range: nil)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
})
}
case let .file(file):
var fetchRange: (Range<Int64>, MediaBoxFetchPriority)?
for attribute in file.attributes {
if case let .Video(_, _, _, preloadSize) = attribute {
if let preloadSize {
fetchRange = (0 ..< Int64(preloadSize), .default)
}
break
}
}
statusSignals.append(
context.account.postbox.mediaBox.resourceRangesStatus(file.resource)
|> filter { ranges in
if let fetchRange {
return ranges.isSuperset(of: RangeSet(fetchRange.0))
} else {
return true
}
}
|> take(1)
|> ignoreValues
)
loadSignals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .other, reference: .media(media: .story(peer: peer, id: storyItem.id, media: storyItem.media._asMedia()), resource: file.resource), range: fetchRange)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
})
loadSignals.append(context.account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedVideoFirstFrameRepresentation(), complete: true, fetch: true, attemptSynchronously: false)
|> ignoreValues)
default:
break
}
return Signal { subscriber in
let statusDisposable = combineLatest(statusSignals).start(completed: {
subscriber.putCompletion()
})
let loadDisposable = combineLatest(loadSignals).start()
return ActionDisposable {
statusDisposable.dispose()
loadDisposable.dispose()
}
}
}
}
func extractItemEntityFiles(item: EngineStoryItem, allEntityFiles: [MediaId: TelegramMediaFile]) -> [MediaId: TelegramMediaFile] { func extractItemEntityFiles(item: EngineStoryItem, allEntityFiles: [MediaId: TelegramMediaFile]) -> [MediaId: TelegramMediaFile] {
var result: [MediaId: TelegramMediaFile] = [:] var result: [MediaId: TelegramMediaFile] = [:]
for entity in item.entities { for entity in item.entities {

View File

@ -1022,33 +1022,51 @@ private final class StoryContainerScreenComponent: Component {
) )
} }
self.contentUpdatedDisposable?.dispose()
var update = false var update = false
self.contentUpdatedDisposable = (component.content.updated
|> deliverOnMainQueue).start(next: { [weak self] _ in let contentUpdated: (StoryContainerScreenComponent) -> Void = { [weak self] component in
guard let self, let component = self.component else { guard let self else {
return return
} }
if update {
var focusedItemId: StoryId? var focusedItemId: StoryId?
var isVideo = false var isVideo = false
if let slice = component.content.stateValue?.slice { if let slice = component.content.stateValue?.slice {
focusedItemId = StoryId(peerId: slice.peer.id, id: slice.item.storyItem.id) focusedItemId = StoryId(peerId: slice.peer.id, id: slice.item.storyItem.id)
if case .file = slice.item.storyItem.media { if case .file = slice.item.storyItem.media {
isVideo = true isVideo = true
}
} }
self.focusedItem.set(focusedItemId) }
self.focusedItem.set(focusedItemId)
self.contentWantsVolumeButtonMonitoring.set(isVideo) self.contentWantsVolumeButtonMonitoring.set(isVideo)
if update {
if component.content.stateValue?.slice == nil { if component.content.stateValue?.slice == nil {
self.environment?.controller()?.dismiss() self.environment?.controller()?.dismiss()
} else { } else {
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
} }
} else {
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
self.state?.updated(transition: .immediate)
}
} }
}
self.contentUpdatedDisposable?.dispose()
self.contentUpdatedDisposable = (component.content.updated
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self, let component = self.component else {
return
}
contentUpdated(component)
}) })
if component.content.stateValue?.slice != nil {
contentUpdated(component)
}
update = true update = true
} }
@ -1247,6 +1265,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()
} }
@ -1562,7 +1584,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
private var didAnimateIn: Bool = false private var didAnimateIn: Bool = false
private var isDismissed: Bool = false private var isDismissed: Bool = false
private let focusedItemPromise = Promise<StoryId?>(nil) private let focusedItemPromise = Promise<StoryId?>()
public var focusedItem: Signal<StoryId?, NoError> { public var focusedItem: Signal<StoryId?, NoError> {
return self.focusedItemPromise.get() return self.focusedItemPromise.get()
} }
@ -1753,4 +1775,3 @@ func allowedStoryReactions(context: AccountContext) -> Signal<[ReactionItem], No
return result return result
} }
} }

View File

@ -19,13 +19,29 @@ public final class StoryContentItem: Equatable {
case off case off
} }
public struct DayCounters: Equatable {
public var position: Int
public var totalCount: Int
public init(position: Int, totalCount: Int) {
self.position = position
self.totalCount = totalCount
}
}
public final class SharedState { public final class SharedState {
public init() { public init() {
} }
} }
public enum ProgressMode {
case play
case pause
case blurred
}
open class View: UIView { open class View: UIView {
open func setIsProgressPaused(_ isProgressPaused: Bool) { open func setProgressMode(_ progressMode: ProgressMode) {
} }
open func rewind() { open func rewind() {
@ -78,17 +94,20 @@ public final class StoryContentItem: Equatable {
} }
public let position: Int? public let position: Int?
public let dayCounters: DayCounters?
public let peerId: EnginePeer.Id? public let peerId: EnginePeer.Id?
public let storyItem: EngineStoryItem public let storyItem: EngineStoryItem
public let entityFiles: [EngineMedia.Id: TelegramMediaFile] public let entityFiles: [EngineMedia.Id: TelegramMediaFile]
public init( public init(
position: Int?, position: Int?,
dayCounters: DayCounters?,
peerId: EnginePeer.Id?, peerId: EnginePeer.Id?,
storyItem: EngineStoryItem, storyItem: EngineStoryItem,
entityFiles: [EngineMedia.Id: TelegramMediaFile] entityFiles: [EngineMedia.Id: TelegramMediaFile]
) { ) {
self.position = position self.position = position
self.dayCounters = dayCounters
self.peerId = peerId self.peerId = peerId
self.storyItem = storyItem self.storyItem = storyItem
self.entityFiles = entityFiles self.entityFiles = entityFiles
@ -98,6 +117,9 @@ public final class StoryContentItem: Equatable {
if lhs.position != rhs.position { if lhs.position != rhs.position {
return false return false
} }
if lhs.dayCounters != rhs.dayCounters {
return false
}
if lhs.peerId != rhs.peerId { if lhs.peerId != rhs.peerId {
return false return false
} }

View File

@ -100,23 +100,36 @@ final class StoryContentCaptionComponent: Component {
} }
} }
private final class ContentItem {
var textNode: TextNodeWithEntities?
var spoilerTextNode: TextNodeWithEntities?
var linkHighlightingNode: LinkHighlightingNode?
var dustNode: InvisibleInkDustNode?
init() {
}
func update() {
}
}
final class View: UIView, UIScrollViewDelegate { final class View: UIView, UIScrollViewDelegate {
private let scrollViewContainer: UIView private let scrollViewContainer: UIView
private let scrollView: UIScrollView private let scrollView: UIScrollView
private let collapsedText: ContentItem
private let expandedText: ContentItem
private let scrollMaskContainer: UIView private let scrollMaskContainer: UIView
private let scrollFullMaskView: UIView private let scrollFullMaskView: UIView
private let scrollCenterMaskView: UIView private let scrollCenterMaskView: UIView
private let scrollBottomMaskView: UIImageView private let scrollBottomMaskView: UIImageView
private let scrollTopMaskView: UIImageView
private let shadowGradientLayer: SimpleGradientLayer private let shadowGradientLayer: SimpleGradientLayer
private let shadowPlainLayer: SimpleLayer private let shadowPlainLayer: SimpleLayer
private var textNode: TextNodeWithEntities?
private var spoilerTextNode: TextNodeWithEntities?
private var linkHighlightingNode: LinkHighlightingNode?
private var dustNode: InvisibleInkDustNode?
private var component: StoryContentCaptionComponent? private var component: StoryContentCaptionComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -125,6 +138,8 @@ final class StoryContentCaptionComponent: Component {
private var ignoreScrolling: Bool = false private var ignoreScrolling: Bool = false
private var ignoreExternalState: Bool = false private var ignoreExternalState: Bool = false
private var isExpanded: Bool = false
override init(frame: CGRect) { override init(frame: CGRect) {
self.shadowGradientLayer = SimpleGradientLayer() self.shadowGradientLayer = SimpleGradientLayer()
self.shadowPlainLayer = SimpleLayer() self.shadowPlainLayer = SimpleLayer()
@ -155,6 +170,15 @@ final class StoryContentCaptionComponent: Component {
], locations: [0.0, 1.0])) ], locations: [0.0, 1.0]))
self.scrollMaskContainer.addSubview(self.scrollBottomMaskView) self.scrollMaskContainer.addSubview(self.scrollBottomMaskView)
self.scrollTopMaskView = UIImageView(image: generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [
UIColor(white: 1.0, alpha: 0.0),
UIColor(white: 1.0, alpha: 1.0)
], locations: [0.0, 1.0]))
self.scrollMaskContainer.addSubview(self.scrollTopMaskView)
self.collapsedText = ContentItem()
self.expandedText = ContentItem()
super.init(frame: frame) super.init(frame: frame)
self.layer.addSublayer(self.shadowGradientLayer) self.layer.addSublayer(self.shadowGradientLayer)
@ -178,7 +202,8 @@ final class StoryContentCaptionComponent: Component {
if !self.bounds.contains(point) { if !self.bounds.contains(point) {
return nil return nil
} }
if let textView = self.textNode?.textNode.view {
if let textView = self.collapsedText.textNode?.textNode.view {
let textLocalPoint = self.convert(point, to: textView) let textLocalPoint = self.convert(point, to: textView)
if textLocalPoint.y >= -7.0 { if textLocalPoint.y >= -7.0 {
return textView return textView
@ -190,7 +215,11 @@ final class StoryContentCaptionComponent: Component {
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state { if case .ended = recognizer.state {
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) if self.isExpanded {
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
} }
} }
@ -238,6 +267,8 @@ final class StoryContentCaptionComponent: Component {
let isExpanded = expandFraction > 0.0 let isExpanded = expandFraction > 0.0
self.isExpanded = isExpanded
if component.externalState.isExpanded != isExpanded { if component.externalState.isExpanded != isExpanded {
component.externalState.isExpanded = isExpanded component.externalState.isExpanded = isExpanded
@ -248,16 +279,18 @@ final class StoryContentCaptionComponent: Component {
} }
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
switch recognizer.state { switch recognizer.state {
case .ended: case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = self.textNode { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = contentItem.textNode {
let titleFrame = textNode.textNode.view.bounds let titleFrame = textNode.textNode.view.bounds
if titleFrame.contains(location) { if titleFrame.contains(location) {
if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
let action: Action? let action: Action?
if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) { if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(contentItem.dustNode?.isRevealed ?? true) {
let convertedPoint = recognizer.view?.convert(location, to: self.dustNode?.view) ?? location let convertedPoint = recognizer.view?.convert(location, to: contentItem.dustNode?.view) ?? location
self.dustNode?.revealAtLocation(convertedPoint) contentItem.dustNode?.revealAtLocation(convertedPoint)
return return
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true var concealed = true
@ -278,19 +311,24 @@ final class StoryContentCaptionComponent: Component {
} else { } else {
action = nil action = nil
} }
guard let action else { if let action {
return switch gesture {
case .tap:
component.action(action)
case .longTap:
component.longTapAction(action)
default:
return
}
} else {
if case .tap = gesture {
if self.isExpanded {
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
}
} }
switch gesture {
case .tap:
component.action(action)
case .longTap:
component.longTapAction(action)
default:
return
}
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
return
} }
} }
} }
@ -300,7 +338,9 @@ final class StoryContentCaptionComponent: Component {
} }
private func updateTouchesAtPoint(_ point: CGPoint?) { private func updateTouchesAtPoint(_ point: CGPoint?) {
guard let textNode = self.textNode else { let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
guard let textNode = contentItem.textNode else {
return return
} }
var rects: [CGRect]? var rects: [CGRect]?
@ -329,20 +369,20 @@ final class StoryContentCaptionComponent: Component {
} }
} }
if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, let dustNode = self.dustNode, !dustNode.isRevealed { if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, let dustNode = contentItem.dustNode, !dustNode.isRevealed {
} else if let rects = rects { } else if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode { if let current = contentItem.linkHighlightingNode {
linkHighlightingNode = current linkHighlightingNode = current
} else { } else {
linkHighlightingNode = LinkHighlightingNode(color: UIColor(white: 1.0, alpha: 0.5)) linkHighlightingNode = LinkHighlightingNode(color: UIColor(white: 1.0, alpha: 0.5))
self.linkHighlightingNode = linkHighlightingNode contentItem.linkHighlightingNode = linkHighlightingNode
self.scrollView.insertSubview(linkHighlightingNode.view, belowSubview: textNode.textNode.view) self.scrollView.insertSubview(linkHighlightingNode.view, belowSubview: textNode.textNode.view)
} }
linkHighlightingNode.frame = textNode.textNode.view.frame linkHighlightingNode.frame = textNode.textNode.view.frame
linkHighlightingNode.updateRects(rects) linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode { } else if let linkHighlightingNode = contentItem.linkHighlightingNode {
self.linkHighlightingNode = nil contentItem.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode() linkHighlightingNode?.removeFromSupernode()
}) })
@ -375,8 +415,21 @@ final class StoryContentCaptionComponent: Component {
entityFiles: component.entityFiles entityFiles: component.entityFiles
) )
let makeLayout = TextNodeWithEntities.asyncLayout(self.textNode) let truncationToken = NSMutableAttributedString()
let textLayout = makeLayout(TextNodeLayoutArguments( truncationToken.append(NSAttributedString(string: "\u{2026} ", font: Font.regular(16.0), textColor: .white))
truncationToken.append(NSAttributedString(string: "Show more", font: Font.semibold(16.0), textColor: .white))
//TODO:localize
let collapsedTextLayout = TextNodeWithEntities.asyncLayout(self.collapsedText.textNode)(TextNodeLayoutArguments(
attributedString: attributedText,
maximumNumberOfLines: 3,
truncationType: .end,
constrainedSize: textContainerSize,
textShadowColor: UIColor(white: 0.0, alpha: 0.25),
textShadowBlur: 4.0,
customTruncationToken: truncationToken
))
let expandedTextLayout = TextNodeWithEntities.asyncLayout(self.expandedText.textNode)(TextNodeLayoutArguments(
attributedString: attributedText, attributedString: attributedText,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
truncationType: .end, truncationType: .end,
@ -385,92 +438,177 @@ final class StoryContentCaptionComponent: Component {
textShadowBlur: 4.0 textShadowBlur: 4.0
)) ))
let makeSpoilerLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode) let collapsedSpoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? if !collapsedTextLayout.0.spoilers.isEmpty {
if !textLayout.0.spoilers.isEmpty { collapsedSpoilerTextLayoutAndApply = TextNodeWithEntities.asyncLayout(self.collapsedText.spoilerTextNode)(TextNodeLayoutArguments(attributedString: attributedText, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textContainerSize, textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowBlur: 4.0, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true))
spoilerTextLayoutAndApply = makeSpoilerLayout(TextNodeLayoutArguments(attributedString: attributedText, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textContainerSize, textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowBlur: 4.0, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true))
} else { } else {
spoilerTextLayoutAndApply = nil collapsedSpoilerTextLayoutAndApply = nil
} }
let maxHeight: CGFloat = 50.0 let expandedSpoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
let visibleTextHeight = min(maxHeight, textLayout.0.size.height) if !expandedTextLayout.0.spoilers.isEmpty {
let textOverflowHeight: CGFloat = textLayout.0.size.height - visibleTextHeight expandedSpoilerTextLayoutAndApply = TextNodeWithEntities.asyncLayout(self.expandedText.spoilerTextNode)(TextNodeLayoutArguments(attributedString: attributedText, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textContainerSize, textShadowColor: UIColor(white: 0.0, alpha: 0.25), textShadowBlur: 4.0, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true))
} else {
expandedSpoilerTextLayoutAndApply = nil
}
let visibleTextHeight = collapsedTextLayout.0.size.height
let textOverflowHeight: CGFloat = expandedTextLayout.0.size.height - visibleTextHeight
let scrollContentSize = CGSize(width: availableSize.width, height: availableSize.height + textOverflowHeight) let scrollContentSize = CGSize(width: availableSize.width, height: availableSize.height + textOverflowHeight)
let textNode = textLayout.1(TextNodeWithEntities.Arguments( do {
context: component.context, let collapsedTextNode = collapsedTextLayout.1(TextNodeWithEntities.Arguments(
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true
))
if self.textNode !== textNode {
self.textNode?.textNode.view.removeFromSuperview()
self.textNode = textNode
if textNode.textNode.view.superview == nil {
self.scrollView.addSubview(textNode.textNode.view)
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { point in
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
guard let self else {
return
}
self.updateTouchesAtPoint(point)
}
textNode.textNode.view.addGestureRecognizer(recognizer)
}
textNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
}
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: textLayout.0.size)
textNode.textNode.frame = textFrame
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
let spoilerTextNode = spoilerTextApply(TextNodeWithEntities.Arguments(
context: component.context, context: component.context,
cache: component.context.animationCache, cache: component.context.animationCache,
renderer: component.context.animationRenderer, renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0), placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true attemptSynchronous: true
)) ))
if self.spoilerTextNode == nil { if self.collapsedText.textNode !== collapsedTextNode {
spoilerTextNode.textNode.alpha = 0.0 self.collapsedText.textNode?.textNode.view.removeFromSuperview()
spoilerTextNode.textNode.isUserInteractionEnabled = false
spoilerTextNode.textNode.contentMode = .topLeft
spoilerTextNode.textNode.contentsScale = UIScreenScale
spoilerTextNode.textNode.displaysAsynchronously = false
self.scrollView.insertSubview(spoilerTextNode.textNode.view, belowSubview: textNode.textNode.view)
spoilerTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0)) self.collapsedText.textNode = collapsedTextNode
if collapsedTextNode.textNode.view.superview == nil {
self.scrollView.addSubview(collapsedTextNode.textNode.view)
self.spoilerTextNode = spoilerTextNode let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { point in
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
guard let self else {
return
}
self.updateTouchesAtPoint(point)
}
collapsedTextNode.textNode.view.addGestureRecognizer(recognizer)
}
collapsedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
} }
self.spoilerTextNode?.textNode.frame = textFrame let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: collapsedTextLayout.0.size)
collapsedTextNode.textNode.frame = collapsedTextFrame
let dustNode: InvisibleInkDustNode if let (_, collapsedSpoilerTextApply) = collapsedSpoilerTextLayoutAndApply {
if let current = self.dustNode { let collapsedSpoilerTextNode = collapsedSpoilerTextApply(TextNodeWithEntities.Arguments(
dustNode = current context: component.context,
} else { cache: component.context.animationCache,
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency) renderer: component.context.animationRenderer,
self.dustNode = dustNode placeholderColor: UIColor(white: 0.2, alpha: 1.0),
self.scrollView.insertSubview(dustNode.view, aboveSubview: spoilerTextNode.textNode.view) attemptSynchronous: true
))
if self.collapsedText.spoilerTextNode == nil {
collapsedSpoilerTextNode.textNode.alpha = 0.0
collapsedSpoilerTextNode.textNode.isUserInteractionEnabled = false
collapsedSpoilerTextNode.textNode.contentMode = .topLeft
collapsedSpoilerTextNode.textNode.contentsScale = UIScreenScale
collapsedSpoilerTextNode.textNode.displaysAsynchronously = false
self.scrollView.insertSubview(collapsedSpoilerTextNode.textNode.view, belowSubview: collapsedTextNode.textNode.view)
collapsedSpoilerTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
self.collapsedText.spoilerTextNode = collapsedSpoilerTextNode
}
self.collapsedText.spoilerTextNode?.textNode.frame = collapsedTextFrame
let collapsedDustNode: InvisibleInkDustNode
if let current = self.collapsedText.dustNode {
collapsedDustNode = current
} else {
collapsedDustNode = InvisibleInkDustNode(textNode: collapsedSpoilerTextNode.textNode, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency)
self.collapsedText.dustNode = collapsedDustNode
self.scrollView.insertSubview(collapsedDustNode.view, aboveSubview: collapsedSpoilerTextNode.textNode.view)
}
collapsedDustNode.frame = collapsedTextFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 0.0)
collapsedDustNode.update(size: collapsedDustNode.frame.size, color: .white, textColor: .white, rects: collapsedTextLayout.0.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: collapsedTextLayout.0.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
} else if let collapsedSpoilerTextNode = self.collapsedText.spoilerTextNode {
self.collapsedText.spoilerTextNode = nil
collapsedSpoilerTextNode.textNode.removeFromSupernode()
if let collapsedDustNode = self.collapsedText.dustNode {
self.collapsedText.dustNode = nil
collapsedDustNode.view.removeFromSuperview()
}
} }
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 0.0) }
dustNode.update(size: dustNode.frame.size, color: .white, textColor: .white, rects: textLayout.0.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.0.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
} else if let spoilerTextNode = self.spoilerTextNode {
self.spoilerTextNode = nil
spoilerTextNode.textNode.removeFromSupernode()
if let dustNode = self.dustNode { do {
self.dustNode = nil let expandedTextNode = expandedTextLayout.1(TextNodeWithEntities.Arguments(
dustNode.removeFromSupernode() context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true
))
if self.expandedText.textNode !== expandedTextNode {
self.expandedText.textNode?.textNode.view.removeFromSuperview()
self.expandedText.textNode = expandedTextNode
if expandedTextNode.textNode.view.superview == nil {
self.scrollView.addSubview(expandedTextNode.textNode.view)
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { point in
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
guard let self else {
return
}
self.updateTouchesAtPoint(point)
}
expandedTextNode.textNode.view.addGestureRecognizer(recognizer)
}
expandedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
}
let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: expandedTextLayout.0.size)
expandedTextNode.textNode.frame = expandedTextFrame
if let (_, expandedSpoilerTextApply) = expandedSpoilerTextLayoutAndApply {
let expandedSpoilerTextNode = expandedSpoilerTextApply(TextNodeWithEntities.Arguments(
context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: UIColor(white: 0.2, alpha: 1.0),
attemptSynchronous: true
))
if self.expandedText.spoilerTextNode == nil {
expandedSpoilerTextNode.textNode.alpha = 0.0
expandedSpoilerTextNode.textNode.isUserInteractionEnabled = false
expandedSpoilerTextNode.textNode.contentMode = .topLeft
expandedSpoilerTextNode.textNode.contentsScale = UIScreenScale
expandedSpoilerTextNode.textNode.displaysAsynchronously = false
self.scrollView.insertSubview(expandedSpoilerTextNode.textNode.view, belowSubview: expandedTextNode.textNode.view)
expandedSpoilerTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
self.expandedText.spoilerTextNode = expandedSpoilerTextNode
}
self.expandedText.spoilerTextNode?.textNode.frame = expandedTextFrame
let expandedDustNode: InvisibleInkDustNode
if let current = self.expandedText.dustNode {
expandedDustNode = current
} else {
expandedDustNode = InvisibleInkDustNode(textNode: expandedSpoilerTextNode.textNode, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency)
self.expandedText.dustNode = expandedDustNode
self.scrollView.insertSubview(expandedDustNode.view, aboveSubview: expandedSpoilerTextNode.textNode.view)
}
expandedDustNode.frame = expandedTextFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 0.0)
expandedDustNode.update(size: expandedDustNode.frame.size, color: .white, textColor: .white, rects: expandedTextLayout.0.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: expandedTextLayout.0.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
} else if let expandedSpoilerTextNode = self.expandedText.spoilerTextNode {
self.expandedText.spoilerTextNode = nil
expandedSpoilerTextNode.textNode.removeFromSupernode()
if let expandedDustNode = self.expandedText.dustNode {
self.expandedText.dustNode = nil
expandedDustNode.view.removeFromSuperview()
}
} }
} }
@ -515,12 +653,41 @@ final class StoryContentCaptionComponent: Component {
let gradientEdgeHeight: CGFloat = 18.0 let gradientEdgeHeight: CGFloat = 18.0
transition.setFrame(view: self.scrollFullMaskView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: availableSize.height))) transition.setFrame(view: self.scrollFullMaskView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientEdgeHeight), size: CGSize(width: availableSize.width, height: availableSize.height - gradientEdgeHeight)))
transition.setFrame(view: self.scrollCenterMaskView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: availableSize.height - gradientEdgeHeight))) transition.setFrame(view: self.scrollCenterMaskView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientEdgeHeight), size: CGSize(width: availableSize.width, height: availableSize.height - gradientEdgeHeight * 2.0)))
transition.setFrame(view: self.scrollBottomMaskView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - gradientEdgeHeight), size: CGSize(width: availableSize.width, height: gradientEdgeHeight))) transition.setFrame(view: self.scrollBottomMaskView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - gradientEdgeHeight), size: CGSize(width: availableSize.width, height: gradientEdgeHeight)))
transition.setFrame(view: self.scrollTopMaskView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: gradientEdgeHeight)))
self.ignoreExternalState = false self.ignoreExternalState = false
var isExpandedTransition = transition
if transition.animation.isImmediate, let hint = transition.userData(TransitionHint.self), case .isExpandedUpdated = hint.kind {
isExpandedTransition = transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut))
}
if let textNode = self.collapsedText.textNode {
isExpandedTransition.setAlpha(view: textNode.textNode.view, alpha: self.isExpanded ? 0.0 : 1.0)
}
if let spoilerTextNode = self.collapsedText.spoilerTextNode {
isExpandedTransition.setAlpha(view: spoilerTextNode.textNode.view, alpha: self.isExpanded ? 0.0 : 1.0)
}
if let dustNode = self.collapsedText.dustNode {
isExpandedTransition.setAlpha(view: dustNode.view, alpha: self.isExpanded ? 0.0 : 1.0)
}
if let textNode = self.expandedText.textNode {
isExpandedTransition.setAlpha(view: textNode.textNode.view, alpha: !self.isExpanded ? 0.0 : 1.0)
}
if let spoilerTextNode = self.expandedText.spoilerTextNode {
isExpandedTransition.setAlpha(view: spoilerTextNode.textNode.view, alpha: !self.isExpanded ? 0.0 : 1.0)
}
if let dustNode = self.expandedText.dustNode {
isExpandedTransition.setAlpha(view: dustNode.view, alpha: !self.isExpanded ? 0.0 : 1.0)
}
isExpandedTransition.setAlpha(layer: self.shadowPlainLayer, alpha: self.isExpanded ? 0.0 : 1.0)
isExpandedTransition.setAlpha(layer: self.shadowGradientLayer, alpha: self.isExpanded ? 0.0 : 1.0)
return availableSize return availableSize
} }
} }

View File

@ -67,7 +67,7 @@ final class StoryItemContentComponent: Component {
private var unsupportedText: ComponentView<Empty>? private var unsupportedText: ComponentView<Empty>?
private var unsupportedButton: ComponentView<Empty>? private var unsupportedButton: ComponentView<Empty>?
private var isProgressPaused: Bool = true private var progressMode: StoryContentItem.ProgressMode = .pause
private var currentProgressTimer: SwiftSignalKit.Timer? private var currentProgressTimer: SwiftSignalKit.Timer?
private var currentProgressTimerValue: Double = 0.0 private var currentProgressTimerValue: Double = 0.0
private var videoProgressDisposable: Disposable? private var videoProgressDisposable: Disposable?
@ -96,7 +96,7 @@ final class StoryItemContentComponent: Component {
guard let self else { guard let self else {
return return
} }
self.updateIsProgressPaused(update: true) self.updateProgressMode(update: true)
} }
} }
@ -119,7 +119,7 @@ final class StoryItemContentComponent: Component {
if self.videoNode != nil { if self.videoNode != nil {
return return
} }
if self.isProgressPaused { if case .pause = self.progressMode {
return return
} }
@ -169,7 +169,17 @@ final class StoryItemContentComponent: Component {
guard let self else { guard let self else {
return return
} }
self.environment?.presentationProgressUpdated(1.0, false, true)
if self.progressMode == .blurred {
self.rewind()
if let videoNode = self.videoNode {
if self.contentLoaded {
videoNode.play()
}
}
} else {
self.environment?.presentationProgressUpdated(1.0, false, true)
}
} }
videoNode.ownsContentNodeUpdated = { [weak self] value in videoNode.ownsContentNodeUpdated = { [weak self] value in
guard let self, let component = self.component else { guard let self, let component = self.component else {
@ -206,10 +216,10 @@ final class StoryItemContentComponent: Component {
} }
} }
override func setIsProgressPaused(_ isProgressPaused: Bool) { override func setProgressMode(_ progressMode: StoryContentItem.ProgressMode) {
if self.isProgressPaused != isProgressPaused { if self.progressMode != progressMode {
self.isProgressPaused = isProgressPaused self.progressMode = progressMode
self.updateIsProgressPaused(update: true) self.updateProgressMode(update: true)
} }
} }
@ -239,9 +249,9 @@ final class StoryItemContentComponent: Component {
} }
} }
private func updateIsProgressPaused(update: Bool) { private func updateProgressMode(update: Bool) {
if let videoNode = self.videoNode { if let videoNode = self.videoNode {
var canPlay = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy var canPlay = self.progressMode != .pause && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
if let component = self.component { if let component = self.component {
if component.item.isPending { if component.item.isPending {
canPlay = false canPlay = false
@ -261,7 +271,7 @@ final class StoryItemContentComponent: Component {
} }
private func updateProgressTimer() { private func updateProgressTimer() {
var needsTimer = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy var needsTimer = self.progressMode != .pause && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
if let component = self.component { if let component = self.component {
if component.item.isPending { if component.item.isPending {
needsTimer = false needsTimer = false
@ -274,7 +284,7 @@ final class StoryItemContentComponent: Component {
timeout: 1.0 / 60.0, timeout: 1.0 / 60.0,
repeat: true, repeat: true,
completion: { [weak self] in completion: { [weak self] in
guard let self, !self.isProgressPaused, self.contentLoaded, self.hierarchyTrackingLayer.isInHierarchy else { guard let self, self.progressMode != .pause, self.contentLoaded, self.hierarchyTrackingLayer.isInHierarchy else {
return return
} }
@ -288,6 +298,10 @@ final class StoryItemContentComponent: Component {
} }
} }
if self.progressMode != .play {
return
}
#if DEBUG && true #if DEBUG && true
let currentProgressTimerLimit: Double = 10.0 let currentProgressTimerLimit: Double = 10.0
#else #else
@ -626,7 +640,7 @@ final class StoryItemContentComponent: Component {
self.backgroundColor = UIColor(rgb: 0x181818) self.backgroundColor = UIColor(rgb: 0x181818)
} }
self.updateIsProgressPaused(update: false) self.updateProgressMode(update: false)
if reloadMedia && synchronousLoad { if reloadMedia && synchronousLoad {
print("\(CFAbsoluteTimeGetCurrent()) Synchronous: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") print("\(CFAbsoluteTimeGetCurrent()) Synchronous: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")

View File

@ -877,46 +877,50 @@ public final class StoryItemSetContainerComponent: Component {
} }
private func isProgressPaused() -> Bool { private func isProgressPaused() -> Bool {
return self.itemProgressMode() == .pause
}
private func itemProgressMode() -> StoryContentItem.ProgressMode {
guard let component = self.component else { guard let component = self.component else {
return false return .pause
} }
if component.pinchState != nil { if component.pinchState != nil {
return true return .pause
} }
if self.inputPanelExternalState.isEditing || component.isProgressPaused || self.sendMessageContext.actionSheet != nil || self.sendMessageContext.isViewingAttachedStickers || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList { if self.inputPanelExternalState.isEditing || component.isProgressPaused || self.sendMessageContext.actionSheet != nil || self.sendMessageContext.isViewingAttachedStickers || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList {
return true return .pause
} }
if let reactionContextNode = self.reactionContextNode, reactionContextNode.isReactionSearchActive { if let reactionContextNode = self.reactionContextNode, reactionContextNode.isReactionSearchActive {
return true return .pause
} }
if self.privacyController != nil { if self.privacyController != nil {
return true return .pause
} }
if self.isReporting { if self.isReporting {
return true return .pause
} }
if self.isEditingStory { if self.isEditingStory {
return true return .pause
} }
if self.sendMessageContext.attachmentController != nil { if self.sendMessageContext.attachmentController != nil {
return true return .pause
} }
if self.sendMessageContext.shareController != nil { if self.sendMessageContext.shareController != nil {
return true return .pause
} }
if self.sendMessageContext.tooltipScreen != nil { if self.sendMessageContext.tooltipScreen != nil {
return true return .pause
} }
if let navigationController = component.controller()?.navigationController as? NavigationController { if let navigationController = component.controller()?.navigationController as? NavigationController {
let topViewController = navigationController.topViewController let topViewController = navigationController.topViewController
if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) { if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) {
return true return .pause
} }
} }
if let captionItem = self.captionItem, captionItem.externalState.isExpanded { if let captionItem = self.captionItem, captionItem.externalState.isExpanded {
return true return .blurred
} }
return false return .play
} }
private func updateScrolling(transition: Transition) { private func updateScrolling(transition: Transition) {
@ -1094,13 +1098,13 @@ public final class StoryItemSetContainerComponent: Component {
itemTransition.setCornerRadius(layer: visibleItem.contentContainerView.layer, cornerRadius: 12.0 * (1.0 / itemScale)) itemTransition.setCornerRadius(layer: visibleItem.contentContainerView.layer, cornerRadius: 12.0 * (1.0 / itemScale))
itemTransition.setAlpha(view: visibleItem.contentContainerView, alpha: 1.0 * (1.0 - fractionDistanceToCenter) + 0.75 * fractionDistanceToCenter) itemTransition.setAlpha(view: visibleItem.contentContainerView, alpha: 1.0 * (1.0 - fractionDistanceToCenter) + 0.75 * fractionDistanceToCenter)
var itemProgressPaused = self.isProgressPaused() var itemProgressMode = self.itemProgressMode()
if index != centralIndex { if index != centralIndex {
itemProgressPaused = true itemProgressMode = .pause
} }
if let view = view as? StoryContentItem.View { if let view = view as? StoryContentItem.View {
view.setIsProgressPaused(itemProgressPaused) view.setProgressMode(itemProgressMode)
} }
} }
} }
@ -1121,7 +1125,7 @@ public final class StoryItemSetContainerComponent: Component {
} }
func updateIsProgressPaused() { func updateIsProgressPaused() {
let isProgressPaused = self.isProgressPaused() let progressMode = self.itemProgressMode()
var centralId: Int32? var centralId: Int32?
if let component = self.component { if let component = self.component {
centralId = component.slice.item.storyItem.id centralId = component.slice.item.storyItem.id
@ -1130,7 +1134,11 @@ public final class StoryItemSetContainerComponent: Component {
for (id, visibleItem) in self.visibleItems { for (id, visibleItem) in self.visibleItems {
if let view = visibleItem.view.view { if let view = visibleItem.view.view {
if let view = view as? StoryContentItem.View { if let view = view as? StoryContentItem.View {
view.setIsProgressPaused(isProgressPaused || id != centralId) var itemMode = progressMode
if id != centralId {
itemMode = .pause
}
view.setProgressMode(itemMode)
} }
} }
} }
@ -1766,7 +1774,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
} }
@ -2078,11 +2086,11 @@ public final class StoryItemSetContainerComponent: Component {
} }
self.navigateToPeer(peer: peer, chat: false) self.navigateToPeer(peer: peer, chat: false)
}, },
openPeerStories: { [weak self] peer, sourceView in openPeerStories: { [weak self] peer, avatarNode in
guard let self else { guard let self else {
return return
} }
self.openPeerStories(peer: peer, sourceView: sourceView) self.openPeerStories(peer: peer, avatarNode: avatarNode)
} }
)), )),
environment: {}, environment: {},
@ -2455,8 +2463,22 @@ public final class StoryItemSetContainerComponent: Component {
} }
var currentCenterInfoItem: InfoItem? var currentCenterInfoItem: InfoItem?
if focusedItem != nil { if let focusedItem {
let centerInfoComponent = AnyComponent(StoryAuthorInfoComponent(context: component.context, peer: component.slice.peer, timestamp: component.slice.item.storyItem.timestamp, isEdited: component.slice.item.storyItem.isEdited)) var counters: StoryAuthorInfoComponent.Counters?
if focusedItem.dayCounters != nil, let position = focusedItem.position {
counters = StoryAuthorInfoComponent.Counters(
position: position,
totalCount: component.slice.totalCount
)
}
let centerInfoComponent = AnyComponent(StoryAuthorInfoComponent(
context: component.context,
peer: component.slice.peer,
timestamp: component.slice.item.storyItem.timestamp,
counters: counters,
isEdited: component.slice.item.storyItem.isEdited
))
if let centerInfoItem = self.centerInfoItem, centerInfoItem.component == centerInfoComponent { if let centerInfoItem = self.centerInfoItem, centerInfoItem.component == centerInfoComponent {
currentCenterInfoItem = centerInfoItem currentCenterInfoItem = centerInfoItem
} else { } else {
@ -2641,7 +2663,7 @@ public final class StoryItemSetContainerComponent: Component {
} }
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width, height: contentFrame.height) containerSize: CGSize(width: availableSize.width, height: contentFrame.height - 60.0)
) )
captionItem.view.parentState = state captionItem.view.parentState = state
let captionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.height - captionSize.height), size: captionSize) let captionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.height - captionSize.height), size: captionSize)
@ -2997,11 +3019,18 @@ public final class StoryItemSetContainerComponent: Component {
let navigationStripSideInset: CGFloat = 8.0 let navigationStripSideInset: CGFloat = 8.0
let navigationStripTopInset: CGFloat = 8.0 let navigationStripTopInset: CGFloat = 8.0
var index = max(0, min(index, component.slice.totalCount - 1))
var count = component.slice.totalCount
if let dayCounters = focusedItem.dayCounters {
index = dayCounters.position
count = dayCounters.totalCount
}
let _ = self.navigationStrip.update( let _ = self.navigationStrip.update(
transition: transition, transition: transition,
component: AnyComponent(MediaNavigationStripComponent( component: AnyComponent(MediaNavigationStripComponent(
index: max(0, min(index, component.slice.totalCount - 1)), index: index,
count: component.slice.totalCount count: count
)), )),
environment: { environment: {
MediaNavigationStripComponent.EnvironmentType( MediaNavigationStripComponent.EnvironmentType(
@ -3233,75 +3262,15 @@ public final class StoryItemSetContainerComponent: Component {
} }
} }
func openPeerStories(peer: EnginePeer, sourceView: UIView) { func openPeerStories(peer: EnginePeer, avatarNode: AvatarNode) {
guard let component = self.component else { guard let component = self.component else {
return return
} }
guard let controller = component.controller() else {
return
}
let storyContent = StoryContentContextImpl(context: component.context, isHidden: false, focusedPeerId: peer.id, singlePeer: true) StoryContainerScreen.openPeerStories(context: component.context, peerId: peer.id, parentController: controller, avatarNode: avatarNode)
let _ = (storyContent.state
|> filter { $0.slice != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak sourceView] _ in
guard let self, let component = self.component else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let sourceView {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: sourceView,
sourceRect: sourceView.bounds,
sourceCornerRadius: sourceView.bounds.width * 0.5,
sourceIsAvatar: false
)
sourceView.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: component.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
if let sourceView {
let destinationView = sourceView
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak sourceView] in
guard let sourceView else {
return
}
sourceView.isHidden = false
}
)
} else {
return nil
}
}
)
component.controller()?.push(storyContainerScreen)
})
} }
private func openStoryEditing() { private func openStoryEditing() {
@ -3655,7 +3624,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
@ -3788,6 +3757,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())
items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Don't Notify", icon: { theme in items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Don't Notify", icon: { theme in
@ -3839,7 +3848,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)
@ -3879,6 +3888,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

@ -2462,7 +2462,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

@ -14,6 +14,7 @@ import ShimmerEffect
import StoryFooterPanelComponent import StoryFooterPanelComponent
import PeerListItemComponent import PeerListItemComponent
import AnimatedStickerComponent import AnimatedStickerComponent
import AvatarNode
final class StoryItemSetViewListComponent: Component { final class StoryItemSetViewListComponent: Component {
final class AnimationHint { final class AnimationHint {
@ -56,7 +57,7 @@ final class StoryItemSetViewListComponent: Component {
let deleteAction: () -> Void let deleteAction: () -> Void
let moreAction: (UIView, ContextGesture?) -> Void let moreAction: (UIView, ContextGesture?) -> Void
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let openPeerStories: (EnginePeer, UIView) -> Void let openPeerStories: (EnginePeer, AvatarNode) -> Void
init( init(
externalState: ExternalState, externalState: ExternalState,
@ -75,7 +76,7 @@ final class StoryItemSetViewListComponent: Component {
deleteAction: @escaping () -> Void, deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void, moreAction: @escaping (UIView, ContextGesture?) -> Void,
openPeer: @escaping (EnginePeer) -> Void, openPeer: @escaping (EnginePeer) -> Void,
openPeerStories: @escaping (EnginePeer, UIView) -> Void openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void
) { ) {
self.externalState = externalState self.externalState = externalState
self.context = context self.context = context
@ -499,11 +500,11 @@ final class StoryItemSetViewListComponent: Component {
} }
component.openPeer(peer) component.openPeer(peer)
}, },
openStories: { [weak self] peer, sourceView in openStories: { [weak self] peer, avatarNode in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
component.openPeerStories(peer, sourceView) component.openPeerStories(peer, avatarNode)
} }
)), )),
environment: {}, environment: {},

View File

@ -338,6 +338,9 @@ public final class StoryPeerListComponent: Component {
private var previewedItemDisposable: Disposable? private var previewedItemDisposable: Disposable?
private var previewedItemId: EnginePeer.Id? private var previewedItemId: EnginePeer.Id?
private var loadingItemDisposable: Disposable?
private var loadingItemId: EnginePeer.Id?
private var animationState: AnimationState? private var animationState: AnimationState?
private var animator: ConstantDisplayLinkAnimator? private var animator: ConstantDisplayLinkAnimator?
@ -403,6 +406,7 @@ public final class StoryPeerListComponent: Component {
deinit { deinit {
self.loadMoreDisposable.dispose() self.loadMoreDisposable.dispose()
self.previewedItemDisposable?.dispose() self.previewedItemDisposable?.dispose()
self.loadingItemDisposable?.dispose()
} }
@objc private func collapsedButtonPressed() { @objc private func collapsedButtonPressed() {
@ -444,6 +448,22 @@ public final class StoryPeerListComponent: Component {
}) })
} }
public func setLoadingItem(peerId: EnginePeer.Id, signal: Signal<Never, NoError>) {
self.loadingItemId = peerId
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in
self?.state?.updated(transition: .immediate)
})
self.loadingItemDisposable?.dispose()
self.loadingItemDisposable = (signal |> deliverOnMainQueue).start(completed: { [weak self] in
guard let self else {
return
}
self.loadingItemId = nil
self.state?.updated(transition: .immediate)
})
}
public func anchorForTooltip() -> (UIView, CGRect)? { public func anchorForTooltip() -> (UIView, CGRect)? {
return (self.collapsedButton, self.collapsedButton.bounds) return (self.collapsedButton, self.collapsedButton.bounds)
} }
@ -869,8 +889,9 @@ public final class StoryPeerListComponent: Component {
} }
hasUnseenCloseFriendsItems = false hasUnseenCloseFriendsItems = false
} else if peer.id == self.loadingItemId {
itemRingAnimation = .loading
} }
//itemRingAnimation = .loading
let measuredItem = calculateItem(i) let measuredItem = calculateItem(i)

View File

@ -13,6 +13,7 @@ import ContextUI
import AsyncDisplayKit import AsyncDisplayKit
import StoryContainerScreen import StoryContainerScreen
import MultilineTextComponent import MultilineTextComponent
import HierarchyTrackingLayer
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? { private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y) let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
@ -124,7 +125,7 @@ private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?,
return path return path
} }
private final class StoryProgressLayer: SimpleLayer { private final class StoryProgressLayer: HierarchyTrackingLayer {
enum Value: Equatable { enum Value: Equatable {
case indefinite case indefinite
case progress(Float) case progress(Float)
@ -147,12 +148,10 @@ private final class StoryProgressLayer: SimpleLayer {
self.uploadProgressLayer.fillColor = nil self.uploadProgressLayer.fillColor = nil
self.uploadProgressLayer.strokeColor = UIColor.white.cgColor self.uploadProgressLayer.strokeColor = UIColor.white.cgColor
self.uploadProgressLayer.lineWidth = 2.0
self.uploadProgressLayer.lineCap = .round self.uploadProgressLayer.lineCap = .round
self.indefiniteDashLayer.fillColor = nil self.indefiniteDashLayer.fillColor = nil
self.indefiniteDashLayer.strokeColor = UIColor.white.cgColor self.indefiniteDashLayer.strokeColor = UIColor.white.cgColor
self.indefiniteDashLayer.lineWidth = 2.0
self.indefiniteDashLayer.lineCap = .round self.indefiniteDashLayer.lineCap = .round
self.indefiniteDashLayer.lineJoin = .round self.indefiniteDashLayer.lineJoin = .round
self.indefiniteDashLayer.strokeEnd = 0.0333 self.indefiniteDashLayer.strokeEnd = 0.0333
@ -164,6 +163,13 @@ private final class StoryProgressLayer: SimpleLayer {
self.indefiniteReplicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0) self.indefiniteReplicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
self.indefiniteReplicatorLayer.transform = CATransform3DMakeRotation(-.pi / 2.0, 0.0, 0.0, 1.0) self.indefiniteReplicatorLayer.transform = CATransform3DMakeRotation(-.pi / 2.0, 0.0, 0.0, 1.0)
self.indefiniteReplicatorLayer.instanceDelay = 0.025 self.indefiniteReplicatorLayer.instanceDelay = 0.025
self.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.updateAnimations(transition: .immediate)
}
} }
override init(layer: Any) { override init(layer: Any) {
@ -180,35 +186,12 @@ private final class StoryProgressLayer: SimpleLayer {
self.uploadProgressLayer.path = nil self.uploadProgressLayer.path = nil
} }
func update(size: CGSize, lineWidth: CGFloat, value: Value, transition: Transition) { func updateAnimations(transition: Transition) {
let params = Params( guard let params = self.currentParams else {
size: size,
lineWidth: lineWidth,
value: value
)
if self.currentParams == params {
return return
} }
self.currentParams = params
let lineWidth: CGFloat = 2.0 switch params.value {
let bounds = CGRect(origin: .zero, size: size)
if self.uploadProgressLayer.path == nil {
let path = CGMutablePath()
path.addEllipse(in: CGRect(origin: CGPoint(x: lineWidth * 0.5, y: lineWidth * 0.5), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth)))
self.uploadProgressLayer.path = path
self.uploadProgressLayer.frame = bounds
}
if self.indefiniteDashLayer.path == nil {
let path = CGMutablePath()
path.addEllipse(in: CGRect(origin: CGPoint(x: lineWidth * 0.5, y: lineWidth * 0.5), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth)))
self.indefiniteDashLayer.path = path
self.indefiniteReplicatorLayer.frame = bounds
self.indefiniteDashLayer.frame = bounds
}
switch value {
case let .progress(progress): case let .progress(progress):
if self.indefiniteReplicatorLayer.superlayer != nil { if self.indefiniteReplicatorLayer.superlayer != nil {
self.indefiniteReplicatorLayer.removeFromSuperlayer() self.indefiniteReplicatorLayer.removeFromSuperlayer()
@ -258,6 +241,39 @@ private final class StoryProgressLayer: SimpleLayer {
} }
} }
} }
func update(size: CGSize, lineWidth: CGFloat, radius: CGFloat, value: Value, transition: Transition) {
let params = Params(
size: size,
lineWidth: lineWidth,
value: value
)
if self.currentParams == params {
return
}
self.currentParams = params
self.uploadProgressLayer.lineWidth = lineWidth
self.indefiniteDashLayer.lineWidth = lineWidth
let bounds = CGRect(origin: .zero, size: size)
if self.uploadProgressLayer.path == nil {
let path = CGMutablePath()
path.addEllipse(in: CGRect(origin: CGPoint(x: (size.width - radius * 2.0) * 0.5, y: (size.height - radius * 2.0) * 0.5), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
self.uploadProgressLayer.path = path
self.uploadProgressLayer.frame = bounds
}
if self.indefiniteDashLayer.path == nil {
let path = CGMutablePath()
path.addEllipse(in: CGRect(origin: CGPoint(x: (size.width - radius * 2.0) * 0.5, y: (size.height - radius * 2.0) * 0.5), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
self.indefiniteDashLayer.path = path
self.indefiniteReplicatorLayer.frame = bounds
self.indefiniteDashLayer.frame = bounds
}
self.updateAnimations(transition: transition)
}
} }
private var sharedAvatarBackgroundImage: UIImage? private var sharedAvatarBackgroundImage: UIImage?
@ -845,9 +861,9 @@ public final class StoryPeerListItemComponent: Component {
} else { } else {
progressTransition = .easeInOut(duration: 0.3) progressTransition = .easeInOut(duration: 0.3)
} }
progressLayer.update(size: progressFrame.size, lineWidth: indicatorLineUnseenWidth, value: .progress(progress), transition: progressTransition) progressLayer.update(size: progressFrame.size, lineWidth: indicatorLineUnseenWidth, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, value: .progress(progress), transition: progressTransition)
case .loading: case .loading:
progressLayer.update(size: progressFrame.size, lineWidth: indicatorLineUnseenWidth, value: .indefinite, transition: transition) progressLayer.update(size: progressFrame.size, lineWidth: indicatorLineUnseenWidth, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, value: .indefinite, transition: transition)
} }
self.indicatorShapeSeenLayer.opacity = 0.0 self.indicatorShapeSeenLayer.opacity = 0.0
self.indicatorShapeUnseenLayer.opacity = 0.0 self.indicatorShapeUnseenLayer.opacity = 0.0

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

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "smoothGradient.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -26,6 +26,7 @@ import AccountUtils
import ContextUI import ContextUI
import TelegramCallsUI import TelegramCallsUI
import AuthorizationUI import AuthorizationUI
import ChatListUI
final class UnauthorizedApplicationContext { final class UnauthorizedApplicationContext {
let sharedContext: SharedAccountContextImpl let sharedContext: SharedAccountContextImpl
@ -852,8 +853,56 @@ final class AuthorizedApplicationContext {
} }
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) { func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) {
if let _ = storyId { if let storyId {
self.rootController.chatListController?.openStories(peerId: peerId) if let chatListController = self.rootController.chatListController as? ChatListControllerImpl {
let _ = (chatListController.context.account.postbox.transaction { transaction -> Bool in
if let peer = transaction.getPeer(storyId.peerId) as? TelegramUser, let storiesHidden = peer.storiesHidden, storiesHidden {
return true
} else {
return false
}
}
|> deliverOnMainQueue).start(next: { [weak self] isArchived in
guard let self, let chatListController = self.rootController.chatListController as? ChatListControllerImpl else {
return
}
if isArchived {
if let navigationController = (chatListController.navigationController as? NavigationController) {
var viewControllers = navigationController.viewControllers
if let index = viewControllers.firstIndex(where: { c in
if let c = c as? ChatListControllerImpl {
if case .chatList(groupId: .archive) = c.location {
return true
}
}
return false
}) {
(viewControllers[index] as? ChatListControllerImpl)?.scrollToStories()
viewControllers.removeSubrange((index + 1) ..< viewControllers.count)
navigationController.setViewControllers(viewControllers, animated: false)
} else {
let archive = ChatListControllerImpl(context: chatListController.context, location: .chatList(groupId: .archive), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false)
archive.onDidAppear = { [weak archive] in
Queue.mainQueue().after(0.1, {
guard let archive else {
return
}
if archive.hasStorySubscriptions {
archive.scrollToStoriesAnimated()
}
})
}
navigationController.pushViewController(archive, animated: false, completion: {})
}
}
} else {
chatListController.scrollToStories()
if let navigationController = (chatListController.navigationController as? NavigationController) {
navigationController.popToRoot(animated: true)
}
}
})
}
} else { } else {
var visiblePeerId: PeerId? var visiblePeerId: PeerId?
if let controller = self.rootController.topViewController as? ChatControllerImpl, controller.chatLocation.peerId == peerId, controller.chatLocation.threadId == threadId { if let controller = self.rootController.topViewController as? ChatControllerImpl, controller.chatLocation.peerId == peerId, controller.chatLocation.threadId == threadId {

View File

@ -17033,110 +17033,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
private func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNode?, avatarNode: AvatarNode?) { private func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNode?, avatarNode: AvatarNode?) {
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peerId, singlePeer: true) if let avatarNode = avatarHeaderNode?.avatarNode ?? avatarNode {
let _ = (storyContent.state StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: avatarNode)
|> filter { $0.slice != nil } }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak avatarHeaderNode, weak avatarNode] _ in
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if let avatarHeaderNode {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: avatarHeaderNode.avatarNode.view,
sourceRect: avatarHeaderNode.avatarNode.view.bounds,
sourceCornerRadius: avatarHeaderNode.avatarNode.view.bounds.width * 0.5,
sourceIsAvatar: false
)
avatarHeaderNode.avatarNode.isHidden = true
} else if let avatarNode {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: avatarNode.view,
sourceRect: avatarNode.view.bounds,
sourceCornerRadius: avatarNode.view.bounds.width * 0.5,
sourceIsAvatar: false
)
avatarNode.isHidden = true
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { peerId, _ in
if let avatarHeaderNode {
let destinationView = avatarHeaderNode.avatarNode.view
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak avatarHeaderNode] in
guard let avatarHeaderNode else {
return
}
avatarHeaderNode.avatarNode.isHidden = false
}
)
} else if let avatarNode {
let destinationView = avatarNode.view
return StoryContainerScreen.TransitionOut(
destinationView: destinationView,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak destinationView] in
let parentView = UIView()
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: destinationView.bounds,
destinationCornerRadius: destinationView.bounds.width * 0.5,
destinationIsAvatar: false,
completed: { [weak avatarNode] in
guard let avatarNode else {
return
}
avatarNode.isHidden = false
}
)
} else {
return nil
}
}
)
self.push(storyContainerScreen)
})
} }
private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) { private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) {

View File

@ -2073,6 +2073,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = [] var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = []
var allVisibleAnchorMessageIds: [(MessageId, Int)] = [] var allVisibleAnchorMessageIds: [(MessageId, Int)] = []
var visibleAdOpaqueIds: [Data] = [] var visibleAdOpaqueIds: [Data] = []
var peerIdsWithRefreshStories: [PeerId] = []
if indexRange.0 <= indexRange.1 { if indexRange.0 <= indexRange.1 {
for i in (indexRange.0 ... indexRange.1) { for i in (indexRange.0 ... indexRange.1) {
@ -2080,6 +2081,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
switch historyView.filteredEntries[i] { switch historyView.filteredEntries[i] {
case let .MessageEntry(message, _, _, _, _, _): case let .MessageEntry(message, _, _, _, _, _):
if let author = message.author as? TelegramUser {
peerIdsWithRefreshStories.append(author.id)
}
var hasUnconsumedMention = false var hasUnconsumedMention = false
var hasUnconsumedContent = false var hasUnconsumedContent = false
if message.tags.contains(.unseenPersonalMessage) { if message.tags.contains(.unseenPersonalMessage) {
@ -2187,6 +2192,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
allVisibleAnchorMessageIds.append((message.id, nodeIndex)) allVisibleAnchorMessageIds.append((message.id, nodeIndex))
} }
case let .MessageGroupEntry(_, messages, _): case let .MessageGroupEntry(_, messages, _):
if let author = messages.first?.0.author as? TelegramUser {
peerIdsWithRefreshStories.append(author.id)
}
for (message, _, _, _, _) in messages { for (message, _, _, _, _) in messages {
var hasUnconsumedMention = false var hasUnconsumedMention = false
var hasUnconsumedContent = false var hasUnconsumedContent = false
@ -2393,6 +2402,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.markAdAsSeen(opaqueId: opaqueId) self.markAdAsSeen(opaqueId: opaqueId)
} }
} }
if !peerIdsWithRefreshStories.isEmpty {
self.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: peerIdsWithRefreshStories)
}
self.currentEarlierPrefetchMessages = toEarlierMediaMessages self.currentEarlierPrefetchMessages = toEarlierMediaMessages
self.currentLaterPrefetchMessages = toLaterMediaMessages self.currentLaterPrefetchMessages = toLaterMediaMessages

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)
@ -4107,7 +4107,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} }
private func openStories(fromAvatar: Bool) { private func openStories(fromAvatar: Bool) {
guard let controller = self.controller else {
return
}
if let expiringStoryList = self.expiringStoryList, let expiringStoryListState = self.expiringStoryListState, !expiringStoryListState.items.isEmpty { if let expiringStoryList = self.expiringStoryList, let expiringStoryListState = self.expiringStoryListState, !expiringStoryListState.items.isEmpty {
if fromAvatar {
StoryContainerScreen.openPeerStories(context: self.context, peerId: self.peerId, parentController: controller, avatarNode: self.headerNode.avatarListNode.avatarContainerNode.avatarNode)
return
}
let _ = expiringStoryList let _ = expiringStoryList
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: self.peerId, singlePeer: true) let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: self.peerId, singlePeer: true)
let _ = (storyContent.state let _ = (storyContent.state
@ -7141,6 +7149,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .remove: case .remove:
data.members?.membersContext.removeMember(memberId: member.id) data.members?.membersContext.removeMember(memberId: member.id)
case let .openStories(sourceView): case let .openStories(sourceView):
guard let controller = self.controller else {
return
}
if let avatarNode = sourceView.asyncdisplaykit_node as? AvatarNode {
StoryContainerScreen.openPeerStories(context: self.context, peerId: member.id, parentController: controller, avatarNode: avatarNode)
return
}
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: member.id, singlePeer: true) let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: member.id, singlePeer: true)
let _ = (storyContent.state let _ = (storyContent.state
|> filter { $0.slice != nil } |> filter { $0.slice != nil }