mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
2b40dc2989
@ -6911,7 +6911,7 @@ Sorry for the inconvenience.";
|
||||
|
||||
"SponsoredMessageMenu.Info" = "What are sponsored\nmessages?";
|
||||
"SponsoredMessageInfoScreen.Title" = "What are sponsored messages?";
|
||||
"SponsoredMessageInfoScreen.Text" = "Unlike other apps, Telegram never uses your private data to target ads. You are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored message.\n\nUnline other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together.";
|
||||
"SponsoredMessageInfoScreen.MarkdownText" = "Unlike other apps, Telegram never uses your private data to target ads. [Learn more in the Privacy Policy](https://telegram.org/privacy#5-6-no-ads-based-on-user-data)\nYou are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored message.\n\nUnline other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together.";
|
||||
"SponsoredMessageInfo.Action" = "Learn More";
|
||||
"SponsoredMessageInfo.Url" = "https://telegram.org/ads";
|
||||
|
||||
@ -7090,6 +7090,7 @@ Sorry for the inconvenience.";
|
||||
"Time.HoursAgo_many" = "%@ hours ago";
|
||||
"Time.HoursAgo_0" = "%@ hours ago";
|
||||
"Time.AtDate" = "%@";
|
||||
"Time.AtPreciseDate" = "%@ at %@";
|
||||
|
||||
"Stickers.ShowMore" = "Show More";
|
||||
|
||||
|
@ -17,6 +17,7 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/Markdown",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -7,6 +7,7 @@ import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import Markdown
|
||||
|
||||
public final class AdInfoScreen: ViewController {
|
||||
private final class Node: ViewControllerTracingNode {
|
||||
@ -84,9 +85,16 @@ public final class AdInfoScreen: ViewController {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
var openUrl: (() -> Void)?
|
||||
var openUrl: ((String) -> Void)?
|
||||
|
||||
let rawText = self.presentationData.strings.SponsoredMessageInfoScreen_Text
|
||||
#if DEBUG && false
|
||||
let rawText = "First Line\n**Bold Text** [Description](http://google.com) text\n[url]\nabcdee"
|
||||
#else
|
||||
let rawText = self.presentationData.strings.SponsoredMessageInfoScreen_MarkdownText
|
||||
#endif
|
||||
|
||||
let defaultUrl = self.presentationData.strings.SponsoredMessageInfo_Url
|
||||
|
||||
var items: [Item] = []
|
||||
var didAddUrl = false
|
||||
for component in rawText.components(separatedBy: "[url]") {
|
||||
@ -100,20 +108,40 @@ public final class AdInfoScreen: ViewController {
|
||||
|
||||
let textNode = ImmediateTextNode()
|
||||
textNode.maximumNumberOfLines = 0
|
||||
textNode.attributedText = NSAttributedString(string: itemText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
textNode.attributedText = parseMarkdownIntoAttributedString(itemText, attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor),
|
||||
linkAttribute: { url in
|
||||
return ("URL", url)
|
||||
}
|
||||
))
|
||||
items.append(.text(textNode))
|
||||
textNode.highlightAttributeAction = { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
||||
return NSAttributedString.Key(rawValue: "URL")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
textNode.tapAttributeAction = { attributes, _ in
|
||||
if let value = attributes[NSAttributedString.Key(rawValue: "URL")] as? String {
|
||||
openUrl?(value)
|
||||
}
|
||||
}
|
||||
textNode.linkHighlightColor = self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5)
|
||||
|
||||
if !didAddUrl {
|
||||
didAddUrl = true
|
||||
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
|
||||
openUrl?()
|
||||
openUrl?(defaultUrl)
|
||||
})))
|
||||
}
|
||||
}
|
||||
if !didAddUrl {
|
||||
didAddUrl = true
|
||||
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
|
||||
openUrl?()
|
||||
openUrl?(defaultUrl)
|
||||
})))
|
||||
}
|
||||
self.items = items
|
||||
@ -133,11 +161,11 @@ public final class AdInfoScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
openUrl = { [weak self] in
|
||||
openUrl = { [weak self] url in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.context.sharedContext.applicationBindings.openUrl(strongSelf.presentationData.strings.SponsoredMessageInfo_Url)
|
||||
strongSelf.context.sharedContext.applicationBindings.openUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -669,6 +669,8 @@ public final class AvatarNode: ASDisplayNode {
|
||||
private var storyIndicator: ComponentView<Empty>?
|
||||
public private(set) var storyPresentationParams: StoryPresentationParams?
|
||||
|
||||
private var loadingStatuses = Bag<Disposable>()
|
||||
|
||||
public struct StoryStats: Equatable {
|
||||
public var totalCount: Int
|
||||
public var unseenCount: Int
|
||||
@ -742,6 +744,10 @@ public final class AvatarNode: ASDisplayNode {
|
||||
self.addSubnode(self.contentNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.cancelLoading()
|
||||
}
|
||||
|
||||
override public var frame: CGRect {
|
||||
get {
|
||||
return super.frame
|
||||
@ -894,7 +900,8 @@ public final class AvatarNode: ASDisplayNode {
|
||||
counters: AvatarStoryIndicatorComponent.Counters(
|
||||
totalCount: storyStats.totalCount,
|
||||
unseenCount: storyStats.unseenCount
|
||||
)
|
||||
),
|
||||
displayProgress: !self.loadingStatuses.isEmpty
|
||||
)),
|
||||
environment: {},
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +118,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
private var didAppear = false
|
||||
private var dismissSearchOnDisappear = false
|
||||
public var onDidAppear: (() -> Void)?
|
||||
|
||||
private var passcodeLockTooltipDisposable = MetaDisposable()
|
||||
private var didShowPasscodeLockTooltipController = false
|
||||
@ -187,6 +188,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
private(set) var orderedStorySubscriptions: EngineStorySubscriptions?
|
||||
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 storySubscriptionsDisposable: Disposable?
|
||||
private var preloadStorySubscriptionsDisposable: Disposable?
|
||||
@ -1059,7 +1068,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if let navigationController = strongSelf.navigationController as? NavigationController {
|
||||
let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .chatList(groupId: groupId), controlsHistoryPreload: false, enableDebugActions: false)
|
||||
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)
|
||||
#endif
|
||||
strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.clearHighlightAnimated(true)
|
||||
}
|
||||
}
|
||||
@ -1314,87 +1337,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let isHidden: Bool
|
||||
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
|
||||
guard let itemNode = itemNode as? ChatListItemNode else {
|
||||
return
|
||||
}
|
||||
|
||||
let storyContent = StoryContentContextImpl(context: self.context, isHidden: isHidden, focusedPeerId: focusedPeerId, singlePeer: singlePeer)
|
||||
let _ = (storyContent.state
|
||||
|> filter { $0.slice != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak itemNode] state in
|
||||
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)
|
||||
})
|
||||
switch subject {
|
||||
case .archive:
|
||||
StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode)
|
||||
case let .peer(peerId):
|
||||
StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode)
|
||||
}
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.peerContextAction = { [weak self] peer, source, node, gesture, location in
|
||||
@ -1813,12 +1765,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|> filter { $0 }
|
||||
|> take(1))
|
||||
} else {
|
||||
self.storiesReady.set(.single(true))
|
||||
let signals: [Signal<Bool, NoError>] = [
|
||||
self.primaryInfoReady.get(),
|
||||
self.storiesReady.get()
|
||||
]
|
||||
|
||||
self.ready.set(combineLatest([
|
||||
self.chatListDisplayNode.mainContainerNode.ready,
|
||||
self.primaryInfoReady.get()
|
||||
])
|
||||
if case .chatList(.archive) = self.location {
|
||||
//signals.append(self.mainReady.get())
|
||||
} else {
|
||||
self.storiesReady.set(.single(true))
|
||||
}
|
||||
|
||||
self.ready.set(combineLatest(signals)
|
||||
|> map { values -> Bool in
|
||||
return !values.contains(where: { !$0 })
|
||||
}
|
||||
@ -1919,7 +1877,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.requestLayout(transition: transition)
|
||||
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil
|
||||
|
||||
if rawStorySubscriptions.items.isEmpty {
|
||||
if !shouldDisplayStoriesInChatListHeader(storySubscriptions: rawStorySubscriptions, isHidden: self.location == .chatList(groupId: .archive)) {
|
||||
self.chatListDisplayNode.scrollToTopIfStoriesAreExpanded()
|
||||
}
|
||||
|
||||
@ -2022,7 +1980,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
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)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] didDisplay in
|
||||
guard let self else {
|
||||
@ -2162,6 +2120,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -2362,6 +2334,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.onDidAppear?()
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.mainContainerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in
|
||||
@ -2941,6 +2915,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.chatListDisplayNode.scrollToStories(animated: false)
|
||||
}
|
||||
|
||||
public func scrollToStoriesAnimated() {
|
||||
self.chatListDisplayNode.scrollToStories(animated: true)
|
||||
}
|
||||
|
||||
private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
var tabContainerOffset: CGFloat = 0.0
|
||||
if !self.displayNavigationBar {
|
||||
@ -3584,6 +3562,88 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
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.state
|
||||
|> take(1)
|
||||
|
@ -434,7 +434,11 @@ private final class ChatListContainerItemNode: ASDisplayNode {
|
||||
if case .forum = location {
|
||||
subject = .forum(hasGeneral: hasOnlyGeneralThread)
|
||||
} else {
|
||||
subject = .chats(hasArchive: hasOnlyArchive)
|
||||
if case .chatList(groupId: .archive) = location {
|
||||
subject = .archive
|
||||
} else {
|
||||
subject = .chats(hasArchive: hasOnlyArchive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import AccountContext
|
||||
final class ChatListEmptyNode: ASDisplayNode {
|
||||
enum Subject {
|
||||
case chats(hasArchive: Bool)
|
||||
case archive
|
||||
case filter(showEdit: Bool)
|
||||
case forum(hasGeneral: Bool)
|
||||
}
|
||||
@ -132,11 +133,14 @@ final class ChatListEmptyNode: ASDisplayNode {
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
let text: String
|
||||
var descriptionText = ""
|
||||
let buttonText: String
|
||||
let buttonText: String?
|
||||
switch self.subject {
|
||||
case let .chats(hasArchive):
|
||||
text = hasArchive ? strings.ChatList_EmptyChatListWithArchive : strings.ChatList_EmptyChatList
|
||||
buttonText = strings.ChatList_EmptyChatListNewMessage
|
||||
case .archive:
|
||||
text = strings.ChatList_EmptyChatList
|
||||
buttonText = nil
|
||||
case .filter:
|
||||
text = strings.ChatList_EmptyChatListFilterTitle
|
||||
descriptionText = strings.ChatList_EmptyChatListFilterText
|
||||
@ -152,7 +156,12 @@ final class ChatListEmptyNode: ASDisplayNode {
|
||||
self.textNode.attributedText = string
|
||||
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)
|
||||
|
||||
|
@ -26,7 +26,6 @@ import TextNodeWithEntities
|
||||
import ComponentFlow
|
||||
import EmojiStatusComponent
|
||||
import AvatarVideoNode
|
||||
import AvatarStoryIndicatorComponent
|
||||
|
||||
public enum ChatListItemContent {
|
||||
public struct ThreadInfo: Equatable {
|
||||
@ -941,7 +940,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
var avatarIconView: ComponentHostView<Empty>?
|
||||
var avatarIconComponent: EmojiStatusComponent?
|
||||
var avatarVideoNode: AvatarVideoNode?
|
||||
var avatarStoryIndicator: ComponentView<Empty>?
|
||||
var avatarTapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
private var inlineNavigationMarkLayer: SimpleLayer?
|
||||
|
||||
@ -1310,6 +1309,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
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 {
|
||||
@ -1327,28 +1335,48 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
let previousItem = self.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 displayAsMessage = false
|
||||
var enablePreview = true
|
||||
switch item.content {
|
||||
case let .peer(peerData):
|
||||
displayAsMessage = peerData.displayAsMessage
|
||||
if displayAsMessage, case let .user(author) = peerData.messages.last?.author {
|
||||
peer = .user(author)
|
||||
} else {
|
||||
peer = peerData.peer.chatMainPeer
|
||||
}
|
||||
if peerData.peer.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
enablePreview = false
|
||||
}
|
||||
case let .groupReference(groupReferenceData):
|
||||
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: {
|
||||
}, 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)
|
||||
case let .peer(peerData):
|
||||
displayAsMessage = peerData.displayAsMessage
|
||||
if displayAsMessage, case let .user(author) = peerData.messages.last?.author {
|
||||
peer = .user(author)
|
||||
} else {
|
||||
peer = peerData.peer.chatMainPeer
|
||||
}
|
||||
if peerData.peer.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
enablePreview = false
|
||||
}
|
||||
case let .groupReference(groupReferenceData):
|
||||
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: {
|
||||
}, 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.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 {
|
||||
var overrideImage: AvatarNodeImageOverride?
|
||||
if peer.id.isReplies {
|
||||
@ -2792,13 +2820,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
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))
|
||||
var avatarScaleOffset: CGFloat = 0.0
|
||||
var avatarScale: CGFloat = 1.0
|
||||
@ -2810,11 +2831,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
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.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))
|
||||
@ -2822,55 +2838,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.avatarNode.updateSize(size: avatarFrame.size)
|
||||
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?
|
||||
if case let .chatList(index) = item.index {
|
||||
itemPeerId = index.messageIndex.id.peerId
|
||||
@ -3900,8 +3867,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
if let _ = item.interaction.inlineNavigationLocation {
|
||||
} else {
|
||||
if let avatarStoryIndicatorView = self.avatarStoryIndicator?.view, let result = avatarStoryIndicatorView.hitTest(self.view.convert(point, to: avatarStoryIndicatorView), with: event) {
|
||||
return result
|
||||
if self.avatarNode.storyStats != nil {
|
||||
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:
|
||||
item.interaction.openStories(.archive, self)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1246,6 +1246,8 @@ public final class ChatListNode: ListView {
|
||||
self.animationRenderer = animationRenderer
|
||||
self.autoSetReady = autoSetReady
|
||||
|
||||
let isMainTab = chatListFilter == nil && location == .chatList(groupId: .root)
|
||||
|
||||
var isSelecting = false
|
||||
if case .peers(_, true, _, _, _, _) = mode {
|
||||
isSelecting = true
|
||||
@ -1933,7 +1935,7 @@ public final class ChatListNode: ListView {
|
||||
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 entries = rawEntries.filter { entry in
|
||||
switch entry {
|
||||
@ -2403,6 +2405,7 @@ public final class ChatListNode: ListView {
|
||||
strongSelf.enqueueHistoryPreloadUpdate()
|
||||
}
|
||||
|
||||
var refreshStoryPeerIds: [PeerId] = []
|
||||
var isHiddenItemVisible = false
|
||||
if let range = range.visibleRange {
|
||||
let entryCount = chatListView.filteredEntries.count
|
||||
@ -2418,6 +2421,11 @@ public final class ChatListNode: ListView {
|
||||
if let threadInfo, threadInfo.isHidden {
|
||||
isHiddenItemVisible = true
|
||||
}
|
||||
|
||||
if let peer = peerEntry.peer.chatMainPeer, !peerEntry.isContact, case let .user(user) = peer {
|
||||
refreshStoryPeerIds.append(user.id)
|
||||
}
|
||||
|
||||
break
|
||||
case .GroupReferenceEntry:
|
||||
isHiddenItemVisible = true
|
||||
@ -2433,6 +2441,9 @@ public final class ChatListNode: ListView {
|
||||
return state
|
||||
}
|
||||
}
|
||||
if !refreshStoryPeerIds.isEmpty {
|
||||
strongSelf.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: refreshStoryPeerIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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] = []
|
||||
|
||||
if !view.hasEarlier {
|
||||
@ -588,7 +598,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
|
||||
|
||||
if !view.hasLater, case .chatList = mode {
|
||||
var groupEntryCount = 0
|
||||
for _ in view.groupItems {
|
||||
for _ in groupItems {
|
||||
groupEntryCount += 1
|
||||
}
|
||||
pinnedIndexOffset += UInt16(groupEntryCount)
|
||||
@ -831,7 +841,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
|
||||
}
|
||||
|
||||
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)
|
||||
var mappedStoryState: ChatListNodeState.StoryState?
|
||||
if let archiveStoryState = state.archiveStoryState {
|
||||
|
@ -512,52 +512,10 @@ public class ContactsController: ViewController {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let storyContent = StoryContentContextImpl(context: self.context, isHidden: true, focusedPeerId: peer.id, singlePeer: true)
|
||||
let _ = (storyContent.state
|
||||
|> 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)
|
||||
})
|
||||
|
||||
if let itemNode = sourceNode as? ContactsPeerItemNode {
|
||||
StoryContainerScreen.openPeerStories(context: self.context, peerId: peer.id, parentController: self, avatarNode: itemNode.avatarNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,7 +186,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
self.contentOffset = offset
|
||||
self.contentOffsetChanged(offset: offset)
|
||||
|
||||
if self.contactListNode.listNode.isTracking {
|
||||
/*if self.contactListNode.listNode.isTracking {
|
||||
if case let .known(value) = offset {
|
||||
if !self.storiesUnlocked {
|
||||
if value < -40.0 {
|
||||
@ -220,7 +220,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
self.contactListNode.contentScrollingEnded = { [weak self] listView in
|
||||
@ -280,43 +280,18 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func contentScrollingEnded(listView: ListView) -> Bool {
|
||||
if "".isEmpty {
|
||||
return false
|
||||
}
|
||||
|
||||
/*if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
|
||||
if navigationBarComponentView.effectiveStoriesInsetHeight > 0.0 {
|
||||
if clippedScrollOffset > 0.0 && clippedScrollOffset < navigationBarComponentView.effectiveStoriesInsetHeight {
|
||||
if clippedScrollOffset < navigationBarComponentView.effectiveStoriesInsetHeight * 0.5 {
|
||||
let _ = listView.scrollToOffsetFromTop(0.0, animated: true)
|
||||
} else {
|
||||
let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight, animated: true)
|
||||
}
|
||||
return true
|
||||
if clippedScrollOffset > 0.0 && clippedScrollOffset < ChatListNavigationBar.searchScrollHeight {
|
||||
if clippedScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 {
|
||||
let _ = listView.scrollToOffsetFromTop(0.0, animated: true)
|
||||
} else {
|
||||
let searchScrollOffset = clippedScrollOffset - navigationBarComponentView.effectiveStoriesInsetHeight
|
||||
if searchScrollOffset > 0.0 && searchScrollOffset < ChatListNavigationBar.searchScrollHeight {
|
||||
if searchScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 {
|
||||
let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight, animated: true)
|
||||
} else {
|
||||
let _ = listView.scrollToOffsetFromTop(navigationBarComponentView.effectiveStoriesInsetHeight + ChatListNavigationBar.searchScrollHeight, animated: true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if clippedScrollOffset > 0.0 && clippedScrollOffset < ChatListNavigationBar.searchScrollHeight {
|
||||
if clippedScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 {
|
||||
let _ = listView.scrollToOffsetFromTop(0.0, animated: true)
|
||||
} else {
|
||||
let _ = listView.scrollToOffsetFromTop(ChatListNavigationBar.searchScrollHeight, animated: true)
|
||||
}
|
||||
return true
|
||||
let _ = listView.scrollToOffsetFromTop(ChatListNavigationBar.searchScrollHeight, animated: true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -1235,7 +1235,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
|
||||
if let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset, topItemFound {
|
||||
if !self.stackFromBottom {
|
||||
completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset - effectiveInsets.bottom - effectiveInsets.top)
|
||||
if !keepMinimalScrollHeightWithTopInset.isZero {
|
||||
completeHeight = max(completeHeight, self.visibleSize.height + effectiveInsets.top + effectiveInsets.bottom)
|
||||
}
|
||||
//completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset - effectiveInsets.bottom - effectiveInsets.top)
|
||||
bottomItemEdge = max(bottomItemEdge, topItemEdge + completeHeight)
|
||||
} else {
|
||||
effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - completeHeight)
|
||||
@ -1647,7 +1650,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
|
||||
if let keepMinimalScrollHeightWithTopInset = self.keepMinimalScrollHeightWithTopInset {
|
||||
if !self.stackFromBottom {
|
||||
completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset)
|
||||
if !keepMinimalScrollHeightWithTopInset.isZero {
|
||||
completeHeight = max(completeHeight, self.visibleSize.height + effectiveInsets.top + effectiveInsets.bottom)
|
||||
}
|
||||
//completeHeight = max(completeHeight, self.visibleSize.height + keepMinimalScrollHeightWithTopInset)
|
||||
bottomItemEdge = max(bottomItemEdge, topItemEdge + completeHeight)
|
||||
}
|
||||
}
|
||||
|
@ -149,6 +149,7 @@ public final class TextNodeLayoutArguments {
|
||||
public let textStroke: (UIColor, CGFloat)?
|
||||
public let displaySpoilers: Bool
|
||||
public let displayEmbeddedItemsUnderSpoilers: Bool
|
||||
public let customTruncationToken: NSAttributedString?
|
||||
|
||||
public init(
|
||||
attributedString: NSAttributedString?,
|
||||
@ -167,7 +168,8 @@ public final class TextNodeLayoutArguments {
|
||||
textShadowBlur: CGFloat? = nil,
|
||||
textStroke: (UIColor, CGFloat)? = nil,
|
||||
displaySpoilers: Bool = false,
|
||||
displayEmbeddedItemsUnderSpoilers: Bool = false
|
||||
displayEmbeddedItemsUnderSpoilers: Bool = false,
|
||||
customTruncationToken: NSAttributedString? = nil
|
||||
) {
|
||||
self.attributedString = attributedString
|
||||
self.backgroundColor = backgroundColor
|
||||
@ -186,6 +188,7 @@ public final class TextNodeLayoutArguments {
|
||||
self.textStroke = textStroke
|
||||
self.displaySpoilers = displaySpoilers
|
||||
self.displayEmbeddedItemsUnderSpoilers = displayEmbeddedItemsUnderSpoilers
|
||||
self.customTruncationToken = customTruncationToken
|
||||
}
|
||||
|
||||
public func withAttributedString(_ attributedString: NSAttributedString?) -> TextNodeLayoutArguments {
|
||||
@ -206,7 +209,8 @@ public final class TextNodeLayoutArguments {
|
||||
textShadowBlur: self.textShadowBlur,
|
||||
textStroke: self.textStroke,
|
||||
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 {
|
||||
let stringLength = attributedString.length
|
||||
|
||||
@ -1168,7 +1172,17 @@ open class TextNode: ASDisplayNode {
|
||||
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)
|
||||
if brokenLineRange.location + brokenLineRange.length > attributedString.length {
|
||||
brokenLineRange.length = attributedString.length - brokenLineRange.location
|
||||
@ -1186,16 +1200,44 @@ open class TextNode: ASDisplayNode {
|
||||
lineConstrainedSize.width -= bottomCutoutSize.width
|
||||
}
|
||||
|
||||
if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(lineConstrainedSize.width) {
|
||||
coreTextLine = originalLine
|
||||
let truncatedTokenString: NSAttributedString
|
||||
if let customTruncationToken {
|
||||
truncatedTokenString = customTruncationToken
|
||||
} else {
|
||||
var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:]
|
||||
truncationTokenAttributes[NSAttributedString.Key.font] = font
|
||||
truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber
|
||||
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
|
||||
let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun]
|
||||
for run in runs {
|
||||
@ -1647,11 +1689,11 @@ open class TextNode: ASDisplayNode {
|
||||
if stringMatch {
|
||||
layout = existingLayout
|
||||
} 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
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
@ -2292,11 +2334,11 @@ open class TextView: UIView {
|
||||
if stringMatch {
|
||||
layout = existingLayout
|
||||
} 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
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
|
@ -580,7 +580,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
public let topShadowNode: ASImageNode
|
||||
public let bottomShadowNode: ASImageNode
|
||||
|
||||
public var storyParams: (peer: EnginePeer, items: [EngineStoryItem], count: Int, hasUnseen: Bool)?
|
||||
public var storyParams: (peer: EnginePeer, items: [EngineStoryItem], count: Int, hasUnseen: Bool, hasUnseenPrivate: Bool)?
|
||||
private var expandedStorySetIndicator: ComponentView<Empty>?
|
||||
public var expandedStorySetIndicatorTransitionView: (UIView, CGRect)? {
|
||||
if let setView = self.expandedStorySetIndicator?.view as? StorySetIndicatorComponent.View {
|
||||
@ -1268,6 +1268,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
peer: storyParams.peer,
|
||||
items: storyParams.items,
|
||||
hasUnseen: storyParams.hasUnseen,
|
||||
hasUnseenPrivate: storyParams.hasUnseenPrivate,
|
||||
totalCount: storyParams.count,
|
||||
theme: defaultDarkPresentationTheme,
|
||||
action: { [weak self] in
|
||||
|
@ -274,14 +274,17 @@ func fetchPeerStoryStats(postbox: PostboxImpl, peerId: PeerId) -> PeerStoryStats
|
||||
if topItems.id == 0 {
|
||||
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 {
|
||||
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)
|
||||
} else {
|
||||
return PeerStoryStats(totalCount: 1, unseenCount: topItems.id > state.maxSeenId ? 1 : 0)
|
||||
return PeerStoryStats(totalCount: 1, unseenCount: topItems.id > maxSeenId ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1074,6 +1074,10 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
for id in removeIds {
|
||||
if let item = self.visibleItems.removeValue(forKey: id) {
|
||||
if let blurLayer = item.blurLayer {
|
||||
item.blurLayer = nil
|
||||
blurLayer.removeFromSuperlayer()
|
||||
}
|
||||
if let layer = item.layer {
|
||||
items.itemBinding.unbindLayer(layer: layer)
|
||||
layer.removeFromSuperlayer()
|
||||
|
@ -296,6 +296,9 @@ public final class AccountViewTracker {
|
||||
private var refreshStoriesForMessageIdsAndTimestamps: [MessageId: Int32] = [:]
|
||||
private var nextUpdatedUnsupportedMediaDisposableId: Int32 = 0
|
||||
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 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?) {
|
||||
self.queue.async {
|
||||
guard let account = self.account else {
|
||||
|
@ -607,7 +607,11 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput
|
||||
if let firstFrameFile = firstFrameFile {
|
||||
account.postbox.mediaBox.storeCachedResourceRepresentation(resource.id.stringRepresentation, representationId: "first-frame", keepDuration: .general, tempFile: firstFrameFile)
|
||||
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: firstFrameFile.path), options: .mappedIfSafe) {
|
||||
let localResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: nil, isSecretRelated: false)
|
||||
account.postbox.mediaBox.storeResourceData(localResource.id, data: data)
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: localResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
|
||||
}
|
||||
}
|
||||
|
||||
let fileMedia = TelegramMediaFile(
|
||||
|
@ -544,9 +544,9 @@ public final class PeerStoryListContext {
|
||||
self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|> mapToSignal { inputUser -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in
|
||||
|> mapToSignal { inputUser -> Signal<([EngineStoryItem], Int, PeerReference?, Bool), NoError> in
|
||||
guard let inputUser = inputUser else {
|
||||
return .single(([], 0, nil))
|
||||
return .single(([], 0, nil, false))
|
||||
}
|
||||
|
||||
let signal: Signal<Api.stories.Stories, MTRpcError>
|
||||
@ -562,18 +562,20 @@ public final class PeerStoryListContext {
|
||||
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<([EngineStoryItem], Int, PeerReference?), NoError> in
|
||||
|> mapToSignal { result -> Signal<([EngineStoryItem], Int, PeerReference?, Bool), NoError> in
|
||||
guard let result = result else {
|
||||
return .single(([], 0, nil))
|
||||
return .single(([], 0, nil, false))
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction -> ([EngineStoryItem], Int, PeerReference?) in
|
||||
return account.postbox.transaction { transaction -> ([EngineStoryItem], Int, PeerReference?, Bool) in
|
||||
var storyItems: [EngineStoryItem] = []
|
||||
var totalCount: Int = 0
|
||||
var hasMore: Bool = false
|
||||
|
||||
switch result {
|
||||
case let .stories(count, stories, users):
|
||||
totalCount = Int(count)
|
||||
hasMore = stories.count >= 100
|
||||
|
||||
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
|
||||
|
||||
@ -619,11 +621,11 @@ public final class PeerStoryListContext {
|
||||
}
|
||||
}
|
||||
|
||||
return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init))
|
||||
return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init), hasMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, peerReference in
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] storyItems, totalCount, peerReference, hasMore in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
@ -650,7 +652,11 @@ public final class PeerStoryListContext {
|
||||
updatedState.peerReference = peerReference
|
||||
}
|
||||
|
||||
updatedState.loadMoreToken = (storyItems.last?.id).flatMap(Int.init)
|
||||
if hasMore {
|
||||
updatedState.loadMoreToken = (storyItems.last?.id).flatMap(Int.init)
|
||||
} else {
|
||||
updatedState.loadMoreToken = nil
|
||||
}
|
||||
if updatedState.loadMoreToken != nil {
|
||||
updatedState.totalCount = max(totalCount, updatedState.items.count)
|
||||
} else {
|
||||
|
@ -49,6 +49,9 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul
|
||||
if let storiesMaxId = storiesMaxId {
|
||||
transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: storiesMaxId)
|
||||
}
|
||||
/*#if DEBUG
|
||||
transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: 10)
|
||||
#endif*/
|
||||
case .userEmpty:
|
||||
break
|
||||
}
|
||||
|
@ -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
|
||||
if difference < 60 {
|
||||
return strings.Time_JustNow
|
||||
@ -392,6 +392,8 @@ public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dat
|
||||
day = .yesterday
|
||||
}
|
||||
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 {
|
||||
return strings.Time_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ swift_library(
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
|
||||
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
||||
"//submodules/TelegramUI/Components/MediaEditorScreen",
|
||||
"//submodules/SaveToCameraRoll",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -14,6 +14,8 @@ import ChatTitleView
|
||||
import BottomButtonPanelComponent
|
||||
import UndoUI
|
||||
import MoreHeaderButton
|
||||
import MediaEditorScreen
|
||||
import SaveToCameraRoll
|
||||
|
||||
final class PeerInfoStoryGridScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -93,11 +95,11 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = component
|
||||
self.saveSelected()
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: strings.Common_Delete, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||
@ -279,6 +281,72 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
controller.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
|
||||
private func saveSelected() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let component = self.component, let peer else {
|
||||
return
|
||||
}
|
||||
guard let peerReference = PeerReference(peer._asPeer()) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
var signals: [Signal<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 {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
@ -1009,11 +1009,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
var transitionIn: StoryContainerScreen.TransitionIn?
|
||||
|
||||
let story = item.story
|
||||
var foundItem: SparseItemGridDisplayItem?
|
||||
var foundItemLayer: SparseItemGridLayer?
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer else {
|
||||
return
|
||||
}
|
||||
foundItem = item
|
||||
if let listItem = itemLayer.item, listItem.story.id == story.id {
|
||||
foundItemLayer = itemLayer
|
||||
}
|
||||
@ -1026,6 +1028,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
sourceCornerRadius: 0.0,
|
||||
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(
|
||||
@ -1037,16 +1044,23 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
return nil
|
||||
}
|
||||
|
||||
var foundItem: SparseItemGridDisplayItem?
|
||||
var foundItemLayer: SparseItemGridLayer?
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer else {
|
||||
return
|
||||
}
|
||||
foundItem = item
|
||||
if let listItem = itemLayer.item, AnyHashable(listItem.story.id) == itemId {
|
||||
foundItemLayer = itemLayer
|
||||
}
|
||||
}
|
||||
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)
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
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> {
|
||||
//TODO:load more
|
||||
/*guard let anchor = anchor as? VisualMediaHoleAnchor else {
|
||||
return .never()
|
||||
}
|
||||
let mappedDirection: SparseMessageList.LoadHoleDirection
|
||||
switch location {
|
||||
case .around:
|
||||
mappedDirection = .around
|
||||
case .toLower:
|
||||
mappedDirection = .later
|
||||
case .toUpper:
|
||||
mappedDirection = .earlier
|
||||
}
|
||||
let listSource = self.listSource
|
||||
return Signal { subscriber in
|
||||
listSource.loadHole(anchor: anchor.messageId, direction: mappedDirection, completion: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
return Signal { _ in
|
||||
listSource.loadMore()
|
||||
|
||||
return EmptyDisposable
|
||||
}*/
|
||||
|
||||
return .never()
|
||||
}
|
||||
|> runOn(.mainQueue())
|
||||
}
|
||||
|
||||
public func updateContentType(contentType: ContentType) {
|
||||
@ -1575,7 +1573,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
let timezoneOffset = Int32(TimeZone.current.secondsFromGMT())
|
||||
|
||||
var mappedItems: [SparseItemGrid.Item] = []
|
||||
let mappedHoles: [SparseItemGrid.HoleAnchor] = []
|
||||
var mappedHoles: [SparseItemGrid.HoleAnchor] = []
|
||||
var totalCount: Int = 0
|
||||
if let peerReference = state.peerReference {
|
||||
for item in state.items {
|
||||
@ -1586,6 +1584,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
localMonthTimestamp: Month(localTimestamp: item.timestamp + timezoneOffset).packedValue
|
||||
))
|
||||
}
|
||||
if mappedItems.count < state.totalCount, let lastItem = state.items.last {
|
||||
mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: 1, localMonthTimestamp: Month(localTimestamp: lastItem.timestamp + timezoneOffset).packedValue))
|
||||
}
|
||||
}
|
||||
totalCount = state.totalCount
|
||||
totalCount = max(mappedItems.count, totalCount)
|
||||
@ -1875,11 +1876,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
}
|
||||
|
||||
private func updateHiddenItems() {
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else {
|
||||
self.itemGrid.forEachVisibleItem { itemValue in
|
||||
guard let itemLayer = itemValue.layer as? ItemLayer, let item = itemLayer.item else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ swift_library(
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/Components/HierarchyTrackingLayer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import HierarchyTrackingLayer
|
||||
import TelegramPresentationData
|
||||
|
||||
public final class AvatarStoryIndicatorComponent: Component {
|
||||
@ -43,6 +44,7 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
public let activeLineWidth: CGFloat
|
||||
public let inactiveLineWidth: CGFloat
|
||||
public let counters: Counters?
|
||||
public let displayProgress: Bool
|
||||
|
||||
public init(
|
||||
hasUnseen: Bool,
|
||||
@ -50,7 +52,8 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
colors: Colors,
|
||||
activeLineWidth: CGFloat,
|
||||
inactiveLineWidth: CGFloat,
|
||||
counters: Counters?
|
||||
counters: Counters?,
|
||||
displayProgress: Bool = false
|
||||
) {
|
||||
self.hasUnseen = hasUnseen
|
||||
self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems
|
||||
@ -58,6 +61,7 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
self.activeLineWidth = activeLineWidth
|
||||
self.inactiveLineWidth = inactiveLineWidth
|
||||
self.counters = counters
|
||||
self.displayProgress = displayProgress
|
||||
}
|
||||
|
||||
public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool {
|
||||
@ -79,11 +83,167 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
if lhs.counters != rhs.counters {
|
||||
return false
|
||||
}
|
||||
if lhs.displayProgress != rhs.displayProgress {
|
||||
return false
|
||||
}
|
||||
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 {
|
||||
private let indicatorView: UIImageView
|
||||
private var progressLayer: ProgressLayer?
|
||||
private var colorLayer: SimpleGradientLayer?
|
||||
|
||||
private var component: AvatarStoryIndicatorComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
@ -110,25 +270,26 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
diameter = availableSize.width + 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
|
||||
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]
|
||||
|
||||
if let counters = component.counters, counters.totalCount > 1 {
|
||||
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 angularSpacing: CGFloat = spacing / 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())
|
||||
}
|
||||
})
|
||||
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
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ public final class PeerListItemComponent: Component {
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer) -> Void
|
||||
let openStories: ((EnginePeer, UIView) -> Void)?
|
||||
let openStories: ((EnginePeer, AvatarNode) -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
@ -74,7 +74,7 @@ public final class PeerListItemComponent: Component {
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer) -> Void,
|
||||
openStories: ((EnginePeer, UIView) -> Void)? = nil
|
||||
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -211,7 +211,7 @@ public final class PeerListItemComponent: Component {
|
||||
guard let component = self.component, let peer = component.peer else {
|
||||
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 {
|
||||
|
@ -76,6 +76,7 @@ swift_library(
|
||||
"//submodules/OpenInExternalAppUI",
|
||||
"//submodules/MediaPasteboardUI",
|
||||
"//submodules/WebPBinding",
|
||||
"//submodules/Utils/RangeSet",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -8,15 +8,22 @@ import TelegramStringFormatting
|
||||
import MultilineTextComponent
|
||||
|
||||
final class StoryAuthorInfoComponent: Component {
|
||||
struct Counters: Equatable {
|
||||
var position: Int
|
||||
var totalCount: Int
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let peer: EnginePeer?
|
||||
let timestamp: Int32
|
||||
let counters: Counters?
|
||||
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.peer = peer
|
||||
self.timestamp = timestamp
|
||||
self.counters = counters
|
||||
self.isEdited = isEdited
|
||||
}
|
||||
|
||||
@ -30,6 +37,9 @@ final class StoryAuthorInfoComponent: Component {
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
if lhs.counters != rhs.counters {
|
||||
return false
|
||||
}
|
||||
if lhs.isEdited != rhs.isEdited {
|
||||
return false
|
||||
}
|
||||
@ -39,6 +49,7 @@ final class StoryAuthorInfoComponent: Component {
|
||||
final class View: UIView {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private var counterLabel: ComponentView<Empty>?
|
||||
|
||||
private var component: StoryAuthorInfoComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
@ -71,7 +82,7 @@ final class StoryAuthorInfoComponent: Component {
|
||||
}
|
||||
|
||||
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 {
|
||||
subtitle.append(" • ")
|
||||
@ -117,6 +128,36 @@ final class StoryAuthorInfoComponent: Component {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import AccountContext
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import MediaResources
|
||||
import RangeSet
|
||||
|
||||
private struct StoryKey: Hashable {
|
||||
var peerId: EnginePeer.Id
|
||||
@ -283,6 +284,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
let allItems = mappedItems.map { item in
|
||||
return StoryContentItem(
|
||||
position: nil,
|
||||
dayCounters: nil,
|
||||
peerId: peer.id,
|
||||
storyItem: item,
|
||||
entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles)
|
||||
@ -295,6 +297,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
additionalPeerData: additionalPeerData,
|
||||
item: StoryContentItem(
|
||||
position: mappedFocusedIndex ?? focusedIndex,
|
||||
dayCounters: nil,
|
||||
peerId: peer.id,
|
||||
storyItem: mappedItem,
|
||||
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(
|
||||
position: 0,
|
||||
dayCounters: nil,
|
||||
peerId: peer.id,
|
||||
storyItem: mappedItem,
|
||||
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
|
||||
if let focusedIndex = focusedIndex {
|
||||
let item = state.items[focusedIndex]
|
||||
self.focusedId = item.id
|
||||
|
||||
var allItems: [StoryContentItem] = []
|
||||
|
||||
var dayCounts: [DayIndex: Int] = [:]
|
||||
var itemDayIndices: [Int32: (Int, DayIndex)] = [:]
|
||||
|
||||
for i in 0 ..< state.items.count {
|
||||
let stateItem = state.items[i]
|
||||
allItems.append(StoryContentItem(
|
||||
position: i,
|
||||
dayCounters: nil,
|
||||
peerId: peer.id,
|
||||
storyItem: stateItem,
|
||||
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(
|
||||
@ -1193,6 +1237,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
additionalPeerData: additionalPeerData,
|
||||
item: StoryContentItem(
|
||||
position: focusedIndex,
|
||||
dayCounters: dayCounters,
|
||||
peerId: peer.id,
|
||||
storyItem: item,
|
||||
entityFiles: extractItemEntityFiles(item: item, allEntityFiles: state.allEntityFiles)
|
||||
@ -1381,6 +1426,88 @@ public func preloadStoryMedia(context: AccountContext, peer: PeerReference, stor
|
||||
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] {
|
||||
var result: [MediaId: TelegramMediaFile] = [:]
|
||||
for entity in item.entities {
|
||||
|
@ -1022,33 +1022,51 @@ private final class StoryContainerScreenComponent: Component {
|
||||
)
|
||||
}
|
||||
|
||||
self.contentUpdatedDisposable?.dispose()
|
||||
var update = false
|
||||
self.contentUpdatedDisposable = (component.content.updated
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self, let component = self.component else {
|
||||
|
||||
let contentUpdated: (StoryContainerScreenComponent) -> Void = { [weak self] component in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if update {
|
||||
var focusedItemId: StoryId?
|
||||
var isVideo = false
|
||||
if let slice = component.content.stateValue?.slice {
|
||||
focusedItemId = StoryId(peerId: slice.peer.id, id: slice.item.storyItem.id)
|
||||
if case .file = slice.item.storyItem.media {
|
||||
isVideo = true
|
||||
}
|
||||
|
||||
var focusedItemId: StoryId?
|
||||
var isVideo = false
|
||||
if let slice = component.content.stateValue?.slice {
|
||||
focusedItemId = StoryId(peerId: slice.peer.id, id: slice.item.storyItem.id)
|
||||
if case .file = slice.item.storyItem.media {
|
||||
isVideo = true
|
||||
}
|
||||
self.focusedItem.set(focusedItemId)
|
||||
|
||||
self.contentWantsVolumeButtonMonitoring.set(isVideo)
|
||||
|
||||
}
|
||||
self.focusedItem.set(focusedItemId)
|
||||
self.contentWantsVolumeButtonMonitoring.set(isVideo)
|
||||
|
||||
if update {
|
||||
if component.content.stateValue?.slice == nil {
|
||||
self.environment?.controller()?.dismiss()
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
@ -1247,6 +1265,10 @@ private final class StoryContainerScreenComponent: Component {
|
||||
} else if slice.previousItemId != nil {
|
||||
component.content.navigate(navigation: .item(.previous))
|
||||
} else if let environment = self.environment {
|
||||
if let sourceIsAvatar = component.transitionIn?.sourceIsAvatar, sourceIsAvatar {
|
||||
} else {
|
||||
self.dismissWithoutTransitionOut = true
|
||||
}
|
||||
environment.controller()?.dismiss()
|
||||
}
|
||||
|
||||
@ -1562,7 +1584,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
private var didAnimateIn: Bool = false
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
private let focusedItemPromise = Promise<StoryId?>(nil)
|
||||
private let focusedItemPromise = Promise<StoryId?>()
|
||||
public var focusedItem: Signal<StoryId?, NoError> {
|
||||
return self.focusedItemPromise.get()
|
||||
}
|
||||
@ -1753,4 +1775,3 @@ func allowedStoryReactions(context: AccountContext) -> Signal<[ReactionItem], No
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,13 +19,29 @@ public final class StoryContentItem: Equatable {
|
||||
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 init() {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ProgressMode {
|
||||
case play
|
||||
case pause
|
||||
case blurred
|
||||
}
|
||||
|
||||
open class View: UIView {
|
||||
open func setIsProgressPaused(_ isProgressPaused: Bool) {
|
||||
open func setProgressMode(_ progressMode: ProgressMode) {
|
||||
}
|
||||
|
||||
open func rewind() {
|
||||
@ -78,17 +94,20 @@ public final class StoryContentItem: Equatable {
|
||||
}
|
||||
|
||||
public let position: Int?
|
||||
public let dayCounters: DayCounters?
|
||||
public let peerId: EnginePeer.Id?
|
||||
public let storyItem: EngineStoryItem
|
||||
public let entityFiles: [EngineMedia.Id: TelegramMediaFile]
|
||||
|
||||
public init(
|
||||
position: Int?,
|
||||
dayCounters: DayCounters?,
|
||||
peerId: EnginePeer.Id?,
|
||||
storyItem: EngineStoryItem,
|
||||
entityFiles: [EngineMedia.Id: TelegramMediaFile]
|
||||
) {
|
||||
self.position = position
|
||||
self.dayCounters = dayCounters
|
||||
self.peerId = peerId
|
||||
self.storyItem = storyItem
|
||||
self.entityFiles = entityFiles
|
||||
@ -98,6 +117,9 @@ public final class StoryContentItem: Equatable {
|
||||
if lhs.position != rhs.position {
|
||||
return false
|
||||
}
|
||||
if lhs.dayCounters != rhs.dayCounters {
|
||||
return false
|
||||
}
|
||||
if lhs.peerId != rhs.peerId {
|
||||
return false
|
||||
}
|
||||
|
@ -99,23 +99,36 @@ final class StoryContentCaptionComponent: Component {
|
||||
self.verticalInset = verticalInset
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContentItem {
|
||||
var textNode: TextNodeWithEntities?
|
||||
var spoilerTextNode: TextNodeWithEntities?
|
||||
var linkHighlightingNode: LinkHighlightingNode?
|
||||
var dustNode: InvisibleInkDustNode?
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
func update() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollViewContainer: UIView
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private let collapsedText: ContentItem
|
||||
private let expandedText: ContentItem
|
||||
|
||||
private let scrollMaskContainer: UIView
|
||||
private let scrollFullMaskView: UIView
|
||||
private let scrollCenterMaskView: UIView
|
||||
private let scrollBottomMaskView: UIImageView
|
||||
private let scrollTopMaskView: UIImageView
|
||||
|
||||
private let shadowGradientLayer: SimpleGradientLayer
|
||||
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 weak var state: EmptyComponentState?
|
||||
@ -125,6 +138,8 @@ final class StoryContentCaptionComponent: Component {
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var ignoreExternalState: Bool = false
|
||||
|
||||
private var isExpanded: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.shadowGradientLayer = SimpleGradientLayer()
|
||||
self.shadowPlainLayer = SimpleLayer()
|
||||
@ -154,6 +169,15 @@ final class StoryContentCaptionComponent: Component {
|
||||
UIColor(white: 1.0, alpha: 0.0)
|
||||
], locations: [0.0, 1.0]))
|
||||
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)
|
||||
|
||||
@ -178,7 +202,8 @@ final class StoryContentCaptionComponent: Component {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
if let textView = self.textNode?.textNode.view {
|
||||
|
||||
if let textView = self.collapsedText.textNode?.textNode.view {
|
||||
let textLocalPoint = self.convert(point, to: textView)
|
||||
if textLocalPoint.y >= -7.0 {
|
||||
return textView
|
||||
@ -190,7 +215,11 @@ final class StoryContentCaptionComponent: Component {
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
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
|
||||
|
||||
self.isExpanded = isExpanded
|
||||
|
||||
if component.externalState.isExpanded != isExpanded {
|
||||
component.externalState.isExpanded = isExpanded
|
||||
|
||||
@ -248,16 +279,18 @@ final class StoryContentCaptionComponent: Component {
|
||||
}
|
||||
|
||||
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
|
||||
|
||||
switch recognizer.state {
|
||||
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
|
||||
if titleFrame.contains(location) {
|
||||
if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
||||
let action: Action?
|
||||
if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) {
|
||||
let convertedPoint = recognizer.view?.convert(location, to: self.dustNode?.view) ?? location
|
||||
self.dustNode?.revealAtLocation(convertedPoint)
|
||||
if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(contentItem.dustNode?.isRevealed ?? true) {
|
||||
let convertedPoint = recognizer.view?.convert(location, to: contentItem.dustNode?.view) ?? location
|
||||
contentItem.dustNode?.revealAtLocation(convertedPoint)
|
||||
return
|
||||
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
var concealed = true
|
||||
@ -278,19 +311,24 @@ final class StoryContentCaptionComponent: Component {
|
||||
} else {
|
||||
action = nil
|
||||
}
|
||||
guard let action else {
|
||||
return
|
||||
if let action {
|
||||
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?) {
|
||||
guard let textNode = self.textNode else {
|
||||
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
|
||||
|
||||
guard let textNode = contentItem.textNode else {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = self.linkHighlightingNode {
|
||||
if let current = contentItem.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
linkHighlightingNode.frame = textNode.textNode.view.frame
|
||||
linkHighlightingNode.updateRects(rects)
|
||||
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
||||
self.linkHighlightingNode = nil
|
||||
} else if let linkHighlightingNode = contentItem.linkHighlightingNode {
|
||||
contentItem.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
@ -375,8 +415,21 @@ final class StoryContentCaptionComponent: Component {
|
||||
entityFiles: component.entityFiles
|
||||
)
|
||||
|
||||
let makeLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||
let textLayout = makeLayout(TextNodeLayoutArguments(
|
||||
let truncationToken = NSMutableAttributedString()
|
||||
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,
|
||||
maximumNumberOfLines: 0,
|
||||
truncationType: .end,
|
||||
@ -385,92 +438,177 @@ final class StoryContentCaptionComponent: Component {
|
||||
textShadowBlur: 4.0
|
||||
))
|
||||
|
||||
let makeSpoilerLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode)
|
||||
let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
|
||||
if !textLayout.0.spoilers.isEmpty {
|
||||
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))
|
||||
let collapsedSpoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
|
||||
if !collapsedTextLayout.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))
|
||||
} else {
|
||||
spoilerTextLayoutAndApply = nil
|
||||
collapsedSpoilerTextLayoutAndApply = nil
|
||||
}
|
||||
|
||||
let maxHeight: CGFloat = 50.0
|
||||
let visibleTextHeight = min(maxHeight, textLayout.0.size.height)
|
||||
let textOverflowHeight: CGFloat = textLayout.0.size.height - visibleTextHeight
|
||||
let expandedSpoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
|
||||
if !expandedTextLayout.0.spoilers.isEmpty {
|
||||
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 textNode = textLayout.1(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.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(
|
||||
do {
|
||||
let collapsedTextNode = collapsedTextLayout.1(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.spoilerTextNode == nil {
|
||||
spoilerTextNode.textNode.alpha = 0.0
|
||||
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)
|
||||
if self.collapsedText.textNode !== collapsedTextNode {
|
||||
self.collapsedText.textNode?.textNode.view.removeFromSuperview()
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
self.spoilerTextNode = spoilerTextNode
|
||||
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 current = self.dustNode {
|
||||
dustNode = current
|
||||
} else {
|
||||
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency)
|
||||
self.dustNode = dustNode
|
||||
self.scrollView.insertSubview(dustNode.view, aboveSubview: spoilerTextNode.textNode.view)
|
||||
if let (_, collapsedSpoilerTextApply) = collapsedSpoilerTextLayoutAndApply {
|
||||
let collapsedSpoilerTextNode = collapsedSpoilerTextApply(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.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let expandedTextNode = expandedTextLayout.1(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.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))
|
||||
}
|
||||
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 {
|
||||
self.dustNode = nil
|
||||
dustNode.removeFromSupernode()
|
||||
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
|
||||
|
||||
transition.setFrame(view: self.scrollFullMaskView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
||||
transition.setFrame(view: self.scrollCenterMaskView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: availableSize.height - gradientEdgeHeight)))
|
||||
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(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.scrollTopMaskView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: gradientEdgeHeight)))
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ final class StoryItemContentComponent: Component {
|
||||
private var unsupportedText: 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 currentProgressTimerValue: Double = 0.0
|
||||
private var videoProgressDisposable: Disposable?
|
||||
@ -96,7 +96,7 @@ final class StoryItemContentComponent: Component {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateIsProgressPaused(update: true)
|
||||
self.updateProgressMode(update: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ final class StoryItemContentComponent: Component {
|
||||
if self.videoNode != nil {
|
||||
return
|
||||
}
|
||||
if self.isProgressPaused {
|
||||
if case .pause = self.progressMode {
|
||||
return
|
||||
}
|
||||
|
||||
@ -169,7 +169,17 @@ final class StoryItemContentComponent: Component {
|
||||
guard let self else {
|
||||
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
|
||||
guard let self, let component = self.component else {
|
||||
@ -206,10 +216,10 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
override func setIsProgressPaused(_ isProgressPaused: Bool) {
|
||||
if self.isProgressPaused != isProgressPaused {
|
||||
self.isProgressPaused = isProgressPaused
|
||||
self.updateIsProgressPaused(update: true)
|
||||
override func setProgressMode(_ progressMode: StoryContentItem.ProgressMode) {
|
||||
if self.progressMode != progressMode {
|
||||
self.progressMode = progressMode
|
||||
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 {
|
||||
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 component.item.isPending {
|
||||
canPlay = false
|
||||
@ -261,7 +271,7 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
|
||||
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 component.item.isPending {
|
||||
needsTimer = false
|
||||
@ -274,7 +284,7 @@ final class StoryItemContentComponent: Component {
|
||||
timeout: 1.0 / 60.0,
|
||||
repeat: true,
|
||||
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
|
||||
}
|
||||
|
||||
@ -288,6 +298,10 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if self.progressMode != .play {
|
||||
return
|
||||
}
|
||||
|
||||
#if DEBUG && true
|
||||
let currentProgressTimerLimit: Double = 10.0
|
||||
#else
|
||||
@ -626,7 +640,7 @@ final class StoryItemContentComponent: Component {
|
||||
self.backgroundColor = UIColor(rgb: 0x181818)
|
||||
}
|
||||
|
||||
self.updateIsProgressPaused(update: false)
|
||||
self.updateProgressMode(update: false)
|
||||
|
||||
if reloadMedia && synchronousLoad {
|
||||
print("\(CFAbsoluteTimeGetCurrent()) Synchronous: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
|
@ -877,46 +877,50 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
private func isProgressPaused() -> Bool {
|
||||
return self.itemProgressMode() == .pause
|
||||
}
|
||||
|
||||
private func itemProgressMode() -> StoryContentItem.ProgressMode {
|
||||
guard let component = self.component else {
|
||||
return false
|
||||
return .pause
|
||||
}
|
||||
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 {
|
||||
return true
|
||||
return .pause
|
||||
}
|
||||
if let reactionContextNode = self.reactionContextNode, reactionContextNode.isReactionSearchActive {
|
||||
return true
|
||||
return .pause
|
||||
}
|
||||
if self.privacyController != nil {
|
||||
return true
|
||||
return .pause
|
||||
}
|
||||
if self.isReporting {
|
||||
return true
|
||||
return .pause
|
||||
}
|
||||
if self.isEditingStory {
|
||||
return true
|
||||
return .pause
|
||||
}
|
||||
if self.sendMessageContext.attachmentController != nil {
|
||||
return true
|
||||
return .pause
|
||||
}
|
||||
if self.sendMessageContext.shareController != nil {
|
||||
return true
|
||||
return .pause
|
||||
}
|
||||
if self.sendMessageContext.tooltipScreen != nil {
|
||||
return true
|
||||
return .pause
|
||||
}
|
||||
if let navigationController = component.controller()?.navigationController as? NavigationController {
|
||||
let topViewController = navigationController.topViewController
|
||||
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 {
|
||||
return true
|
||||
return .blurred
|
||||
}
|
||||
return false
|
||||
return .play
|
||||
}
|
||||
|
||||
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.setAlpha(view: visibleItem.contentContainerView, alpha: 1.0 * (1.0 - fractionDistanceToCenter) + 0.75 * fractionDistanceToCenter)
|
||||
|
||||
var itemProgressPaused = self.isProgressPaused()
|
||||
var itemProgressMode = self.itemProgressMode()
|
||||
if index != centralIndex {
|
||||
itemProgressPaused = true
|
||||
itemProgressMode = .pause
|
||||
}
|
||||
|
||||
if let view = view as? StoryContentItem.View {
|
||||
view.setIsProgressPaused(itemProgressPaused)
|
||||
view.setProgressMode(itemProgressMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1121,7 +1125,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
func updateIsProgressPaused() {
|
||||
let isProgressPaused = self.isProgressPaused()
|
||||
let progressMode = self.itemProgressMode()
|
||||
var centralId: Int32?
|
||||
if let component = self.component {
|
||||
centralId = component.slice.item.storyItem.id
|
||||
@ -1130,7 +1134,11 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
for (id, visibleItem) in self.visibleItems {
|
||||
if let view = visibleItem.view.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.discardMediaRecordingPreview(view: self)
|
||||
},
|
||||
attachmentAction: { [weak self] in
|
||||
attachmentAction: component.slice.peer.isService ? nil : { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -2078,11 +2086,11 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
self.navigateToPeer(peer: peer, chat: false)
|
||||
},
|
||||
openPeerStories: { [weak self] peer, sourceView in
|
||||
openPeerStories: { [weak self] peer, avatarNode in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openPeerStories(peer: peer, sourceView: sourceView)
|
||||
self.openPeerStories(peer: peer, avatarNode: avatarNode)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -2455,8 +2463,22 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
var currentCenterInfoItem: InfoItem?
|
||||
if focusedItem != nil {
|
||||
let centerInfoComponent = AnyComponent(StoryAuthorInfoComponent(context: component.context, peer: component.slice.peer, timestamp: component.slice.item.storyItem.timestamp, isEdited: component.slice.item.storyItem.isEdited))
|
||||
if let focusedItem {
|
||||
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 {
|
||||
currentCenterInfoItem = centerInfoItem
|
||||
} else {
|
||||
@ -2641,7 +2663,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: contentFrame.height)
|
||||
containerSize: CGSize(width: availableSize.width, height: contentFrame.height - 60.0)
|
||||
)
|
||||
captionItem.view.parentState = state
|
||||
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 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(
|
||||
transition: transition,
|
||||
component: AnyComponent(MediaNavigationStripComponent(
|
||||
index: max(0, min(index, component.slice.totalCount - 1)),
|
||||
count: component.slice.totalCount
|
||||
index: index,
|
||||
count: count
|
||||
)),
|
||||
environment: {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
guard let controller = component.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
let storyContent = StoryContentContextImpl(context: component.context, isHidden: false, focusedPeerId: peer.id, singlePeer: true)
|
||||
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)
|
||||
})
|
||||
StoryContainerScreen.openPeerStories(context: component.context, peerId: peer.id, parentController: controller, avatarNode: avatarNode)
|
||||
}
|
||||
|
||||
private func openStoryEditing() {
|
||||
@ -3655,7 +3624,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.requestSave()
|
||||
})))
|
||||
|
||||
if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) {
|
||||
if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) && component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) {
|
||||
items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
@ -3787,6 +3756,46 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) {
|
||||
items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] link in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if let link {
|
||||
UIPasteboard.general.string = link
|
||||
|
||||
component.presentController(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .linkCopied(text: "Link copied."),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
), nil)
|
||||
}
|
||||
})
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sendMessageContext.performShareAction(view: self)
|
||||
})))
|
||||
}
|
||||
|
||||
let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: component.slice.peer._asPeer(), peerSettings: settings._asNotificationSettings())
|
||||
|
||||
@ -3839,7 +3848,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
isHidden = storiesHidden
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: isHidden ? "Unhide \(component.slice.peer.compactDisplayTitle)" : "Hide \(component.slice.peer.compactDisplayTitle)", icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: isHidden ? "Unarchive" : "Archive", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/MoveToChats" : "Chat/Context Menu/MoveToContacts"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
@ -3879,6 +3888,25 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
component.controller()?.present(tooltipScreen, in: .current)
|
||||
})))
|
||||
|
||||
#if DEBUG
|
||||
let saveText: String
|
||||
if case .file = component.slice.item.storyItem.media {
|
||||
saveText = "Save Video"
|
||||
} else {
|
||||
saveText = "Save Image"
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.requestSave()
|
||||
})))
|
||||
#endif
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Report", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, a in
|
||||
|
@ -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 {
|
||||
return
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import ShimmerEffect
|
||||
import StoryFooterPanelComponent
|
||||
import PeerListItemComponent
|
||||
import AnimatedStickerComponent
|
||||
import AvatarNode
|
||||
|
||||
final class StoryItemSetViewListComponent: Component {
|
||||
final class AnimationHint {
|
||||
@ -56,7 +57,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
let deleteAction: () -> Void
|
||||
let moreAction: (UIView, ContextGesture?) -> Void
|
||||
let openPeer: (EnginePeer) -> Void
|
||||
let openPeerStories: (EnginePeer, UIView) -> Void
|
||||
let openPeerStories: (EnginePeer, AvatarNode) -> Void
|
||||
|
||||
init(
|
||||
externalState: ExternalState,
|
||||
@ -75,7 +76,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
deleteAction: @escaping () -> Void,
|
||||
moreAction: @escaping (UIView, ContextGesture?) -> Void,
|
||||
openPeer: @escaping (EnginePeer) -> Void,
|
||||
openPeerStories: @escaping (EnginePeer, UIView) -> Void
|
||||
openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
@ -499,11 +500,11 @@ final class StoryItemSetViewListComponent: Component {
|
||||
}
|
||||
component.openPeer(peer)
|
||||
},
|
||||
openStories: { [weak self] peer, sourceView in
|
||||
openStories: { [weak self] peer, avatarNode in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.openPeerStories(peer, sourceView)
|
||||
component.openPeerStories(peer, avatarNode)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
@ -338,6 +338,9 @@ public final class StoryPeerListComponent: Component {
|
||||
private var previewedItemDisposable: Disposable?
|
||||
private var previewedItemId: EnginePeer.Id?
|
||||
|
||||
private var loadingItemDisposable: Disposable?
|
||||
private var loadingItemId: EnginePeer.Id?
|
||||
|
||||
private var animationState: AnimationState?
|
||||
private var animator: ConstantDisplayLinkAnimator?
|
||||
|
||||
@ -403,6 +406,7 @@ public final class StoryPeerListComponent: Component {
|
||||
deinit {
|
||||
self.loadMoreDisposable.dispose()
|
||||
self.previewedItemDisposable?.dispose()
|
||||
self.loadingItemDisposable?.dispose()
|
||||
}
|
||||
|
||||
@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)? {
|
||||
return (self.collapsedButton, self.collapsedButton.bounds)
|
||||
}
|
||||
@ -869,8 +889,9 @@ public final class StoryPeerListComponent: Component {
|
||||
}
|
||||
|
||||
hasUnseenCloseFriendsItems = false
|
||||
} else if peer.id == self.loadingItemId {
|
||||
itemRingAnimation = .loading
|
||||
}
|
||||
//itemRingAnimation = .loading
|
||||
|
||||
let measuredItem = calculateItem(i)
|
||||
|
||||
|
@ -13,6 +13,7 @@ import ContextUI
|
||||
import AsyncDisplayKit
|
||||
import StoryContainerScreen
|
||||
import MultilineTextComponent
|
||||
import HierarchyTrackingLayer
|
||||
|
||||
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)
|
||||
@ -124,7 +125,7 @@ private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?,
|
||||
return path
|
||||
}
|
||||
|
||||
private final class StoryProgressLayer: SimpleLayer {
|
||||
private final class StoryProgressLayer: HierarchyTrackingLayer {
|
||||
enum Value: Equatable {
|
||||
case indefinite
|
||||
case progress(Float)
|
||||
@ -147,12 +148,10 @@ private final class StoryProgressLayer: SimpleLayer {
|
||||
|
||||
self.uploadProgressLayer.fillColor = nil
|
||||
self.uploadProgressLayer.strokeColor = UIColor.white.cgColor
|
||||
self.uploadProgressLayer.lineWidth = 2.0
|
||||
self.uploadProgressLayer.lineCap = .round
|
||||
|
||||
self.indefiniteDashLayer.fillColor = nil
|
||||
self.indefiniteDashLayer.strokeColor = UIColor.white.cgColor
|
||||
self.indefiniteDashLayer.lineWidth = 2.0
|
||||
self.indefiniteDashLayer.lineCap = .round
|
||||
self.indefiniteDashLayer.lineJoin = .round
|
||||
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.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) {
|
||||
@ -180,35 +186,12 @@ private final class StoryProgressLayer: SimpleLayer {
|
||||
self.uploadProgressLayer.path = nil
|
||||
}
|
||||
|
||||
func update(size: CGSize, lineWidth: CGFloat, value: Value, transition: Transition) {
|
||||
let params = Params(
|
||||
size: size,
|
||||
lineWidth: lineWidth,
|
||||
value: value
|
||||
)
|
||||
if self.currentParams == params {
|
||||
func updateAnimations(transition: Transition) {
|
||||
guard let params = self.currentParams else {
|
||||
return
|
||||
}
|
||||
self.currentParams = params
|
||||
|
||||
let lineWidth: CGFloat = 2.0
|
||||
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 {
|
||||
switch params.value {
|
||||
case let .progress(progress):
|
||||
if self.indefiniteReplicatorLayer.superlayer != nil {
|
||||
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?
|
||||
@ -845,9 +861,9 @@ public final class StoryPeerListItemComponent: Component {
|
||||
} else {
|
||||
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:
|
||||
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.indicatorShapeUnseenLayer.opacity = 0.0
|
||||
|
@ -92,6 +92,7 @@ public final class StorySetIndicatorComponent: Component {
|
||||
public let peer: EnginePeer
|
||||
public let items: [EngineStoryItem]
|
||||
public let hasUnseen: Bool
|
||||
public let hasUnseenPrivate: Bool
|
||||
public let totalCount: Int
|
||||
public let theme: PresentationTheme
|
||||
public let action: () -> Void
|
||||
@ -101,6 +102,7 @@ public final class StorySetIndicatorComponent: Component {
|
||||
peer: EnginePeer,
|
||||
items: [EngineStoryItem],
|
||||
hasUnseen: Bool,
|
||||
hasUnseenPrivate: Bool,
|
||||
totalCount: Int,
|
||||
theme: PresentationTheme,
|
||||
action: @escaping () -> Void
|
||||
@ -109,6 +111,7 @@ public final class StorySetIndicatorComponent: Component {
|
||||
self.peer = peer
|
||||
self.items = items
|
||||
self.hasUnseen = hasUnseen
|
||||
self.hasUnseenPrivate = hasUnseenPrivate
|
||||
self.totalCount = totalCount
|
||||
self.theme = theme
|
||||
self.action = action
|
||||
@ -121,6 +124,9 @@ public final class StorySetIndicatorComponent: Component {
|
||||
if lhs.hasUnseen != rhs.hasUnseen {
|
||||
return false
|
||||
}
|
||||
if lhs.hasUnseenPrivate != rhs.hasUnseenPrivate {
|
||||
return false
|
||||
}
|
||||
if lhs.totalCount != rhs.totalCount {
|
||||
return false
|
||||
}
|
||||
@ -349,7 +355,9 @@ public final class StorySetIndicatorComponent: Component {
|
||||
|
||||
let borderColors: [UInt32]
|
||||
|
||||
if component.hasUnseen {
|
||||
if component.hasUnseenPrivate {
|
||||
borderColors = [component.theme.chatList.storyUnseenPrivateColors.topColor.argb, component.theme.chatList.storyUnseenPrivateColors.bottomColor.argb]
|
||||
} else if component.hasUnseen {
|
||||
borderColors = [component.theme.chatList.storyUnseenColors.topColor.argb, component.theme.chatList.storyUnseenColors.bottomColor.argb]
|
||||
} else {
|
||||
borderColors = [UIColor(white: 1.0, alpha: 0.3).argb, UIColor(white: 1.0, alpha: 0.3).argb]
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "smoothGradient.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -26,6 +26,7 @@ import AccountUtils
|
||||
import ContextUI
|
||||
import TelegramCallsUI
|
||||
import AuthorizationUI
|
||||
import ChatListUI
|
||||
|
||||
final class UnauthorizedApplicationContext {
|
||||
let sharedContext: SharedAccountContextImpl
|
||||
@ -852,8 +853,56 @@ final class AuthorizedApplicationContext {
|
||||
}
|
||||
|
||||
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) {
|
||||
if let _ = storyId {
|
||||
self.rootController.chatListController?.openStories(peerId: peerId)
|
||||
if let storyId {
|
||||
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 {
|
||||
var visiblePeerId: PeerId?
|
||||
if let controller = self.rootController.topViewController as? ChatControllerImpl, controller.chatLocation.peerId == peerId, controller.chatLocation.threadId == threadId {
|
||||
|
@ -17033,110 +17033,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
private func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNode?, avatarNode: AvatarNode?) {
|
||||
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peerId, singlePeer: true)
|
||||
let _ = (storyContent.state
|
||||
|> 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)
|
||||
})
|
||||
if let avatarNode = avatarHeaderNode?.avatarNode ?? avatarNode {
|
||||
StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: avatarNode)
|
||||
}
|
||||
}
|
||||
|
||||
private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) {
|
||||
|
@ -2073,6 +2073,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = []
|
||||
var allVisibleAnchorMessageIds: [(MessageId, Int)] = []
|
||||
var visibleAdOpaqueIds: [Data] = []
|
||||
var peerIdsWithRefreshStories: [PeerId] = []
|
||||
|
||||
if 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] {
|
||||
case let .MessageEntry(message, _, _, _, _, _):
|
||||
if let author = message.author as? TelegramUser {
|
||||
peerIdsWithRefreshStories.append(author.id)
|
||||
}
|
||||
|
||||
var hasUnconsumedMention = false
|
||||
var hasUnconsumedContent = false
|
||||
if message.tags.contains(.unseenPersonalMessage) {
|
||||
@ -2187,6 +2192,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
allVisibleAnchorMessageIds.append((message.id, nodeIndex))
|
||||
}
|
||||
case let .MessageGroupEntry(_, messages, _):
|
||||
if let author = messages.first?.0.author as? TelegramUser {
|
||||
peerIdsWithRefreshStories.append(author.id)
|
||||
}
|
||||
|
||||
for (message, _, _, _, _) in messages {
|
||||
var hasUnconsumedMention = false
|
||||
var hasUnconsumedContent = false
|
||||
@ -2393,6 +2402,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
self.markAdAsSeen(opaqueId: opaqueId)
|
||||
}
|
||||
}
|
||||
if !peerIdsWithRefreshStories.isEmpty {
|
||||
self.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: peerIdsWithRefreshStories)
|
||||
}
|
||||
|
||||
self.currentEarlierPrefetchMessages = toEarlierMediaMessages
|
||||
self.currentLaterPrefetchMessages = toLaterMediaMessages
|
||||
|
@ -457,9 +457,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, action: { _, f in
|
||||
/*c.dismiss(completion: {
|
||||
controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context))
|
||||
})*/
|
||||
f(.dismissWithoutContent)
|
||||
controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context))
|
||||
})))
|
||||
@ -625,6 +622,14 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
}
|
||||
}
|
||||
|
||||
for media in messages[0].media {
|
||||
if let story = media as? TelegramMediaStory {
|
||||
if let story = message.associatedStories[story.storyId], story.data.isEmpty {
|
||||
canPin = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var loadStickerSaveStatusSignal: Signal<Bool?, NoError> = .single(nil)
|
||||
if let loadStickerSaveStatus = loadStickerSaveStatus {
|
||||
loadStickerSaveStatusSignal = context.engine.stickers.isStickerSaved(id: loadStickerSaveStatus)
|
||||
@ -1943,6 +1948,10 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
|
||||
}
|
||||
} else if let action = media as? TelegramMediaAction, case .phoneCall = action.action {
|
||||
optionsMap[id]!.insert(.rateCall)
|
||||
} else if let story = media as? TelegramMediaStory {
|
||||
if let story = message.associatedStories[story.storyId], story.data.isEmpty {
|
||||
isShareProtected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if id.namespace == Namespaces.Message.ScheduledCloud {
|
||||
@ -1962,7 +1971,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
|
||||
optionsMap[id]!.insert(.deleteLocally)
|
||||
}
|
||||
} else if id.peerId == accountPeerId {
|
||||
if !(message.flags.isSending || message.flags.contains(.Failed)) {
|
||||
if !(message.flags.isSending || message.flags.contains(.Failed)) && !isShareProtected {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
}
|
||||
optionsMap[id]!.insert(.deleteLocally)
|
||||
@ -2006,7 +2015,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
|
||||
banPeer = nil
|
||||
}
|
||||
}
|
||||
if !message.containsSecretMedia && !isAction {
|
||||
if !message.containsSecretMedia && !isAction && !isShareProtected {
|
||||
if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.isCopyProtected() {
|
||||
if !(message.flags.isSending || message.flags.contains(.Failed)) {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
@ -2023,7 +2032,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia {
|
||||
if !isAction && !message.isCopyProtected() {
|
||||
if !isAction && !message.isCopyProtected() && !isShareProtected {
|
||||
if !(message.flags.isSending || message.flags.contains(.Failed)) {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
}
|
||||
@ -2057,7 +2066,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
|
||||
}
|
||||
}
|
||||
} else if let user = peer as? TelegramUser {
|
||||
if !isScheduled && message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction && !message.id.peerId.isReplies && !message.isCopyProtected() {
|
||||
if !isScheduled && message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction && !message.id.peerId.isReplies && !message.isCopyProtected() && !isShareProtected {
|
||||
if !(message.flags.isSending || message.flags.contains(.Failed)) {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
}
|
||||
|
@ -536,7 +536,11 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
|
||||
return .openMessage
|
||||
if let item = self.item, item.message.media.contains(where: { $0 is TelegramMediaStory }) {
|
||||
return .none
|
||||
} else {
|
||||
return .openMessage
|
||||
}
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
|
@ -3903,7 +3903,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
case .placeholder:
|
||||
return nil
|
||||
}
|
||||
}, state.items.count, state.hasUnseen)
|
||||
}, state.items.count, state.hasUnseen, state.hasUnseenCloseFriends)
|
||||
}
|
||||
|
||||
self.requestLayout(animated: false)
|
||||
@ -4107,7 +4107,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
|
||||
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 fromAvatar {
|
||||
StoryContainerScreen.openPeerStories(context: self.context, peerId: self.peerId, parentController: controller, avatarNode: self.headerNode.avatarListNode.avatarContainerNode.avatarNode)
|
||||
return
|
||||
}
|
||||
|
||||
let _ = expiringStoryList
|
||||
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: self.peerId, singlePeer: true)
|
||||
let _ = (storyContent.state
|
||||
@ -7141,6 +7149,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
case .remove:
|
||||
data.members?.membersContext.removeMember(memberId: member.id)
|
||||
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.state
|
||||
|> filter { $0.slice != nil }
|
||||
|
Loading…
x
Reference in New Issue
Block a user