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

This commit is contained in:
Ilya Laktyushin 2023-07-11 18:35:31 +02:00
commit 2b40dc2989
50 changed files with 1916 additions and 719 deletions

View File

@ -6911,7 +6911,7 @@ Sorry for the inconvenience.";
"SponsoredMessageMenu.Info" = "What are sponsored\nmessages?";
"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";

View File

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

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -568,7 +568,17 @@ struct ChatListContactPeer {
}
}
func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer], accountPeerId: EnginePeer.Id) -> (entries: [ChatListNodeEntry], loading: Bool) {
func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer], accountPeerId: EnginePeer.Id, isMainTab: Bool) -> (entries: [ChatListNodeEntry], loading: Bool) {
var groupItems = view.groupItems
if isMainTab && state.archiveStoryState != nil && groupItems.isEmpty {
groupItems.append(EngineChatList.GroupItem(
id: .archive,
topMessage: nil,
items: [],
unreadCount: 0
))
}
var result: [ChatListNodeEntry] = []
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 {

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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 {

View File

@ -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(

View File

@ -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 {

View File

@ -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
}

View File

@ -362,7 +362,7 @@ public func stringForRelativeLiveLocationUpdateTimestamp(strings: PresentationSt
}
}
public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String {
public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String {
let difference = timestamp - relativeTimestamp
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
}

View File

@ -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",

View File

@ -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

View File

@ -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)
}
}
}
}

View File

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

View File

@ -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
}

View File

@ -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 {

View File

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

View File

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

View File

@ -8,15 +8,22 @@ import TelegramStringFormatting
import MultilineTextComponent
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
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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")

View File

@ -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

View File

@ -2462,7 +2462,7 @@ final class StoryItemSetContainerSendMessage {
})
}
func openPeerMention(view: StoryItemSetContainerComponent.View, name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) {
func openPeerMention(view: StoryItemSetContainerComponent.View, name: String, navigation: ChatControllerInteractionNavigateToPeer = .info, sourceMessageId: MessageId? = nil) {
guard let component = view.component, let parentController = component.controller() else {
return
}

View File

@ -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: {},

View File

@ -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)

View File

@ -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

View File

@ -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]

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -26,6 +26,7 @@ import AccountUtils
import ContextUI
import 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 {

View File

@ -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) {

View File

@ -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

View File

@ -457,9 +457,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
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)
}

View File

@ -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
}

View File

@ -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 }