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

This commit is contained in:
Ilya Laktyushin 2023-06-13 15:56:44 +04:00
commit 304fbbf8bc
35 changed files with 1519 additions and 294 deletions

View File

@ -263,6 +263,11 @@
"PUSH_CHAT_REQ_JOINED" = "%2$@|%1$@ was accepted into the group";
"PUSH_STORY_NOTEXT" = "%1$@|posted a story";
"PUSH_MESSAGE_STORY" = "%1$@|shared a story with you";
"PUSH_CHANNEL_MESSAGE_STORY" = "%1$@|shared a story";
"PUSH_CHAT_MESSAGE_STORY" = "%2$@|%1$@ shared a story to the group";
"LOCAL_MESSAGE_FWDS" = "%1$@ forwarded you %2$d messages";
"LOCAL_CHANNEL_MESSAGE_FWDS" = "%1$@ posted %2$d forwarded messages";
"LOCAL_CHAT_MESSAGE_FWDS" = "%1$@ forwarded %2$d messages";

View File

@ -2387,63 +2387,80 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
var items: [ContextMenuItem] = []
//TODO:localize
if peer.id == self.context.account.peerId {
items.append(.action(ContextMenuActionItem(text: "Add Story", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
self.openStoryCamera()
})
})))
} else {
items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
)
|> deliverOnMainQueue).start(next: { [weak self] peer in
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peer.id)
)
|> deliverOnMainQueue).start(next: { [weak self] notificationSettings in
guard let self else {
return
}
var items: [ContextMenuItem] = []
//TODO:localize
if peer.id == self.context.account.peerId {
items.append(.action(ContextMenuActionItem(text: "Add Story", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
self.openStoryCamera()
})
})))
} else {
items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
(self.navigationController as? NavigationController)?.pushViewController(controller)
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self else {
return
}
guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
return
}
(self.navigationController as? NavigationController)?.pushViewController(controller)
})
})
})
})))
/*items.append(.action(ContextMenuActionItem(text: "Mute", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
})))*/
items.append(.action(ContextMenuActionItem(text: "Archive", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
})))
guard let self else {
return
}
self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: true)
})))
}
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
let isMuted = notificationSettings.storiesMuted == true
items.append(.action(ContextMenuActionItem(text: isMuted ? "Unmute" : "Mute", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Muted": "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
let _ = self.context.engine.peers.togglePeerStoriesMuted(peerId: peer.id).start()
})))
items.append(.action(ContextMenuActionItem(text: "Archive", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self else {
return
}
self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: true)
})))
}
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
})
}
}
}

View File

@ -1957,8 +1957,13 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
offset = 0.0
}
var allowAvatarsExpansion: Bool = true
if !self.mainContainerNode.currentItemNode.startedScrollingAtUpperBound && !self.mainContainerNode.storiesUnlocked {
allowAvatarsExpansion = false
}
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.applyScroll(offset: offset, transition: Transition(transition))
navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, transition: Transition(transition))
}
}
@ -2209,7 +2214,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
func willScrollToTop() {
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.applyScroll(offset: 0.0, transition: Transition(animation: .curve(duration: 0.3, curve: .slide)))
navigationBarComponentView.applyScroll(offset: 0.0, allowAvatarsExpansion: false, transition: Transition(animation: .curve(duration: 0.3, curve: .slide)))
}
}

View File

@ -3464,7 +3464,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
return nil
case .links:
var media: [EngineMedia] = []
media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, attributes: [], instantPage: nil)))))
media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))))
let message = EngineMessage(
stableId: 0,
stableVersion: 0,

View File

@ -1193,6 +1193,8 @@ public final class ChatListNode: ListView {
}
}
public private(set) var startedScrollingAtUpperBound: Bool = false
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) {
self.context = context
self.location = location
@ -2719,7 +2721,6 @@ public final class ChatListNode: ListView {
}
}
}
var startedScrollingAtUpperBound = false
var startedScrollingWithCanExpandHiddenItems = false
self.beganInteractiveDragging = { [weak self] _ in
@ -2727,10 +2728,10 @@ public final class ChatListNode: ListView {
return
}
switch strongSelf.visibleContentOffset() {
case .none, .unknown:
startedScrollingAtUpperBound = false
case let .known(value):
startedScrollingAtUpperBound = value <= 0.0
case .none, .unknown:
strongSelf.startedScrollingAtUpperBound = false
case let .known(value):
strongSelf.startedScrollingAtUpperBound = value <= 0.0
}
if let canExpandHiddenItems = strongSelf.canExpandHiddenItems {
@ -2752,7 +2753,7 @@ public final class ChatListNode: ListView {
guard let strongSelf = self else {
return
}
startedScrollingAtUpperBound = false
strongSelf.startedScrollingAtUpperBound = false
let _ = strongSelf.contentScrollingEnded?(strongSelf)
let revealHiddenItems: Bool
switch strongSelf.visibleContentOffset() {
@ -2795,7 +2796,7 @@ public final class ChatListNode: ListView {
atTop = false
case let .known(value):
atTop = value <= 0.0
if startedScrollingAtUpperBound && startedScrollingWithCanExpandHiddenItems && strongSelf.isTracking {
if strongSelf.startedScrollingAtUpperBound && startedScrollingWithCanExpandHiddenItems && strongSelf.isTracking {
revealHiddenItems = value <= -60.0
}
}

View File

@ -435,7 +435,7 @@ final class ContactsControllerNode: ASDisplayNode {
}
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.applyScroll(offset: offset, transition: Transition(transition))
navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: true, transition: Transition(transition))
}
}

View File

@ -632,7 +632,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
let item: InstantPageItem
if let url = url, let coverId = coverId, case let .image(image) = media[coverId] {
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, attributes: [], instantPage: nil)
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil)
let content = TelegramMediaWebpageContent.Loaded(loadedContent)
item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: .webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content)), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false)

View File

@ -26,6 +26,9 @@ swift_library(
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/AccountContext:AccountContext",
"//submodules/AvatarVideoNode:AvatarVideoNode",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent",
],
visibility = [
"//visibility:public",

View File

@ -16,6 +16,9 @@ import RadialStatusNode
import TelegramUIPreferences
import AvatarNode
import AvatarVideoNode
import ComponentFlow
import ComponentDisplayAdapters
import StorySetIndicatorComponent
private class PeerInfoAvatarListLoadingStripNode: ASImageNode {
private var currentInHierarchy = false
@ -577,6 +580,9 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
public let topShadowNode: ASImageNode
public let bottomShadowNode: ASImageNode
public var storyParams: (peer: EnginePeer, items: [EngineStoryItem], count: Int, hasUnseen: Bool)?
private var expandedStorySetIndicator: ComponentView<Empty>?
public let contentNode: ASDisplayNode
let leftHighlightNode: ASDisplayNode
let rightHighlightNode: ASDisplayNode
@ -612,6 +618,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
public var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
public var currentIndexUpdated: (() -> Void)?
public var openStories: (() -> Void)?
public let isReady = Promise<Bool>()
private var didSetReady = false
@ -914,6 +922,12 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isExpanded, let expandedStorySetIndicatorView = self.expandedStorySetIndicator?.view {
if let result = expandedStorySetIndicatorView.hitTest(self.view.convert(point, to: expandedStorySetIndicatorView), with: event) {
return result
}
}
return super.hitTest(point, with: event)
}
@ -1228,6 +1242,45 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
}))
}
self.updateItems(size: size, transition: transition, stripTransition: transition)
if let storyParams = self.storyParams {
var indicatorTransition = Transition(transition)
let expandedStorySetIndicator: ComponentView<Empty>
if let current = self.expandedStorySetIndicator {
expandedStorySetIndicator = current
} else {
indicatorTransition = .immediate
expandedStorySetIndicator = ComponentView()
self.expandedStorySetIndicator = expandedStorySetIndicator
}
let expandedStorySetSize = expandedStorySetIndicator.update(
transition: indicatorTransition,
component: AnyComponent(StorySetIndicatorComponent(
context: self.context,
peer: storyParams.peer,
items: storyParams.items,
hasUnseen: storyParams.hasUnseen,
totalCount: storyParams.count,
theme: defaultDarkPresentationTheme,
action: { [weak self] in
self?.openStories?()
}
)),
environment: {},
containerSize: CGSize(width: 300.0, height: 100.0)
)
let expandedStorySetFrame = CGRect(origin: CGPoint(x: floor((size.width - expandedStorySetSize.width) * 0.5), y: 10.0), size: expandedStorySetSize)
if let expandedStorySetIndicatorView = expandedStorySetIndicator.view {
if expandedStorySetIndicatorView.superview == nil {
self.stripContainerNode.view.addSubview(expandedStorySetIndicatorView)
}
indicatorTransition.setFrame(view: expandedStorySetIndicatorView, frame: expandedStorySetFrame)
}
} else if let expandedStorySetIndicator = self.expandedStorySetIndicator {
self.expandedStorySetIndicator = nil
expandedStorySetIndicator.view?.removeFromSuperview()
}
}
private func updateStrips(size: CGSize, itemsAdded: Bool, stripTransition: ContainedViewLayoutTransition) {

View File

@ -961,6 +961,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-350980120] = { return Api.WebPage.parse_webPageEmpty($0) }
dict[1930545681] = { return Api.WebPage.parse_webPageNotModified($0) }
dict[-981018084] = { return Api.WebPage.parse_webPagePending($0) }
dict[-1818605967] = { return Api.WebPageAttribute.parse_webPageAttributeStory($0) }
dict[1421174295] = { return Api.WebPageAttribute.parse_webPageAttributeTheme($0) }
dict[211046684] = { return Api.WebViewMessageSent.parse_webViewMessageSent($0) }
dict[202659196] = { return Api.WebViewResult.parse_webViewResultUrl($0) }

View File

@ -187,11 +187,21 @@ public extension Api {
}
}
public extension Api {
enum WebPageAttribute: TypeConstructorDescription {
indirect enum WebPageAttribute: TypeConstructorDescription {
case webPageAttributeStory(flags: Int32, userId: Int64, id: Int32, story: Api.StoryItem?)
case webPageAttributeTheme(flags: Int32, documents: [Api.Document]?, settings: Api.ThemeSettings?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .webPageAttributeStory(let flags, let userId, let id, let story):
if boxed {
buffer.appendInt32(-1818605967)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(userId, buffer: buffer, boxed: false)
serializeInt32(id, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {story!.serialize(buffer, true)}
break
case .webPageAttributeTheme(let flags, let documents, let settings):
if boxed {
buffer.appendInt32(1421174295)
@ -209,11 +219,35 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .webPageAttributeStory(let flags, let userId, let id, let story):
return ("webPageAttributeStory", [("flags", flags as Any), ("userId", userId as Any), ("id", id as Any), ("story", story as Any)])
case .webPageAttributeTheme(let flags, let documents, let settings):
return ("webPageAttributeTheme", [("flags", flags as Any), ("documents", documents as Any), ("settings", settings as Any)])
}
}
public static func parse_webPageAttributeStory(_ reader: BufferReader) -> WebPageAttribute? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int64?
_2 = reader.readInt64()
var _3: Int32?
_3 = reader.readInt32()
var _4: Api.StoryItem?
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.StoryItem
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.WebPageAttribute.webPageAttributeStory(flags: _1!, userId: _2!, id: _3!, story: _4)
}
else {
return nil
}
}
public static func parse_webPageAttributeTheme(_ reader: BufferReader) -> WebPageAttribute? {
var _1: Int32?
_1 = reader.readInt32()
@ -1292,43 +1326,3 @@ public extension Api.account {
}
}
public extension Api.account {
enum TmpPassword: TypeConstructorDescription {
case tmpPassword(tmpPassword: Buffer, validUntil: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .tmpPassword(let tmpPassword, let validUntil):
if boxed {
buffer.appendInt32(-614138572)
}
serializeBytes(tmpPassword, buffer: buffer, boxed: false)
serializeInt32(validUntil, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .tmpPassword(let tmpPassword, let validUntil):
return ("tmpPassword", [("tmpPassword", tmpPassword as Any), ("validUntil", validUntil as Any)])
}
}
public static func parse_tmpPassword(_ reader: BufferReader) -> TmpPassword? {
var _1: Buffer?
_1 = parseBytes(reader)
var _2: Int32?
_2 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.account.TmpPassword.tmpPassword(tmpPassword: _1!, validUntil: _2!)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,43 @@
public extension Api.account {
enum TmpPassword: TypeConstructorDescription {
case tmpPassword(tmpPassword: Buffer, validUntil: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .tmpPassword(let tmpPassword, let validUntil):
if boxed {
buffer.appendInt32(-614138572)
}
serializeBytes(tmpPassword, buffer: buffer, boxed: false)
serializeInt32(validUntil, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .tmpPassword(let tmpPassword, let validUntil):
return ("tmpPassword", [("tmpPassword", tmpPassword as Any), ("validUntil", validUntil as Any)])
}
}
public static func parse_tmpPassword(_ reader: BufferReader) -> TmpPassword? {
var _1: Buffer?
_1 = parseBytes(reader)
var _2: Int32?
_2 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.account.TmpPassword.tmpPassword(tmpPassword: _1!, validUntil: _2!)
}
else {
return nil
}
}
}
}
public extension Api.account {
enum WallPapers: TypeConstructorDescription {
case wallPapers(hash: Int64, wallpapers: [Api.WallPaper])

View File

@ -6,12 +6,14 @@ import TelegramApi
func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPageAttribute) -> TelegramMediaWebpageAttribute? {
switch attribute {
case let .webPageAttributeTheme(_, documents, settings):
var files: [TelegramMediaFile] = []
if let documents = documents {
files = documents.compactMap { telegramMediaFileFromApiDocument($0) }
}
return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $0) }))
case let .webPageAttributeTheme(_, documents, settings):
var files: [TelegramMediaFile] = []
if let documents = documents {
files = documents.compactMap { telegramMediaFileFromApiDocument($0) }
}
return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $0) }))
case .webPageAttributeStory:
return nil
}
}
@ -38,15 +40,22 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) ->
if let document = document {
file = telegramMediaFileFromApiDocument(document)
}
var story: TelegramMediaStory?
var webpageAttributes: [TelegramMediaWebpageAttribute] = []
if let attributes = attributes {
webpageAttributes = attributes.compactMap(telegramMediaWebpageAttributeFromApiWebpageAttribute)
for attribute in attributes {
if case let .webPageAttributeStory(_, userId, id, _) = attribute {
story = TelegramMediaStory(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: id))
}
}
}
var instantPage: InstantPage?
if let cachedPage = cachedPage {
instantPage = InstantPage(apiPage: cachedPage)
}
return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, attributes: webpageAttributes, instantPage: instantPage)))
return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage)))
case .webPageEmpty:
return nil
}

View File

@ -1254,6 +1254,8 @@ public final class AccountViewTracker {
for media in message.media {
if let storyMedia = media as? TelegramMediaStory {
result.insert(storyMedia.storyId)
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let story = content.story {
result.insert(story.storyId)
}
}
}

View File

@ -85,10 +85,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
public let image: TelegramMediaImage?
public let file: TelegramMediaFile?
public let story: TelegramMediaStory?
public let attributes: [TelegramMediaWebpageAttribute]
public let instantPage: InstantPage?
public init(url: String, displayUrl: String, hash: Int32, type: String?, websiteName: String?, title: String?, text: String?, embedUrl: String?, embedType: String?, embedSize: PixelDimensions?, duration: Int?, author: String?, image: TelegramMediaImage?, file: TelegramMediaFile?, attributes: [TelegramMediaWebpageAttribute], instantPage: InstantPage?) {
public init(url: String, displayUrl: String, hash: Int32, type: String?, websiteName: String?, title: String?, text: String?, embedUrl: String?, embedType: String?, embedSize: PixelDimensions?, duration: Int?, author: String?, image: TelegramMediaImage?, file: TelegramMediaFile?, story: TelegramMediaStory?, attributes: [TelegramMediaWebpageAttribute], instantPage: InstantPage?) {
self.url = url
self.displayUrl = displayUrl
self.hash = hash
@ -103,6 +104,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
self.author = author
self.image = image
self.file = file
self.story = story
self.attributes = attributes
self.instantPage = instantPage
}
@ -141,6 +143,12 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
self.file = nil
}
if let story = decoder.decodeObjectForKey("stry") as? TelegramMediaStory {
self.story = story
} else {
self.story = nil
}
var effectiveAttributes: [TelegramMediaWebpageAttribute] = []
if let attributes = decoder.decodeObjectArrayWithDecoderForKey("attr") as [TelegramMediaWebpageAttribute]? {
effectiveAttributes.append(contentsOf: attributes)
@ -218,6 +226,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "fi")
}
if let story = self.story {
encoder.encodeObject(story, forKey: "stry")
} else {
encoder.encodeNil(forKey: "stry")
}
encoder.encodeObjectArray(self.attributes, forKey: "attr")
@ -261,6 +274,14 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage
return false
}
if let lhsStory = lhs.story, let rhsStory = rhs.story {
if !lhsStory.isEqual(to: rhsStory) {
return false
}
} else if (lhs.story == nil) != (rhs.story == nil) {
return false
}
if lhs.attributes.count != rhs.attributes.count {
return false
} else {
@ -289,6 +310,14 @@ public final class TelegramMediaWebpage: Media, Equatable {
}
public let peerIds: [PeerId] = []
public var storyIds: [StoryId] {
if case let .Loaded(content) = self.content, let story = content.story {
return story.storyIds
} else {
return []
}
}
public let webpageId: MediaId
public let content: TelegramMediaWebpageContent

View File

@ -662,6 +662,8 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
}
}
flags |= 1 << 3
return account.network.request(Api.functions.stories.sendStory(
flags: flags,
media: inputMedia,
@ -1312,6 +1314,9 @@ public final class EngineStoryViewListContext {
}
func loadMore() {
if !self.state.canLoadMore {
return
}
if self.isLoadingMore {
return
}

View File

@ -62,6 +62,36 @@ func _internal_togglePeerMuted(account: Account, peerId: PeerId, threadId: Int64
}
}
func _internal_togglePeerStoriesMuted(account: Account, peerId: PeerId) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Void in
guard let peer = transaction.getPeer(peerId) else {
return
}
var notificationPeerId = peerId
if let associatedPeerId = peer.associatedPeerId {
notificationPeerId = associatedPeerId
}
let currentSettings = transaction.getPeerNotificationSettings(id: notificationPeerId) as? TelegramPeerNotificationSettings
let previousSettings: TelegramPeerNotificationSettings
if let currentSettings = currentSettings {
previousSettings = currentSettings
} else {
previousSettings = TelegramPeerNotificationSettings.defaultSettings
}
let updatedSettings: TelegramPeerNotificationSettings
if let previousStoriesMuted = previousSettings.storiesMuted {
updatedSettings = previousSettings.withUpdatedStoriesMuted(!previousStoriesMuted)
} else {
updatedSettings = previousSettings.withUpdatedStoriesMuted(true)
}
transaction.updatePendingPeerNotificationSettings(peerId: notificationPeerId, settings: updatedSettings)
}
}
func _internal_updatePeerMuteSetting(account: Account, peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Void in
_internal_updatePeerMuteSetting(account: account, transaction: transaction, peerId: peerId, threadId: threadId, muteInterval: muteInterval)

View File

@ -246,6 +246,11 @@ public extension TelegramEngine {
public func togglePeerMuted(peerId: PeerId, threadId: Int64?) -> Signal<Void, NoError> {
return _internal_togglePeerMuted(account: self.account, peerId: peerId, threadId: threadId)
}
public func togglePeerStoriesMuted(peerId: EnginePeer.Id) -> Signal<Never, NoError> {
return _internal_togglePeerStoriesMuted(account: self.account, peerId: peerId)
|> ignoreValues
}
public func updatePeerMuteSetting(peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal<Void, NoError> {
return _internal_updatePeerMuteSetting(account: self.account, peerId: peerId, threadId: threadId, muteInterval: muteInterval)

View File

@ -88,7 +88,7 @@ public func actualizedWebpage(postbox: Postbox, network: Network, webpage: Teleg
return updatedWebpage
}
} else if let result = result, case let .webPageNotModified(_, viewsValue) = result, let views = viewsValue, case let .Loaded(content) = webpage.content {
let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(url: content.url, displayUrl: content.displayUrl, hash: content.hash, type: content.type, websiteName: content.websiteName, title: content.title, text: content.text, embedUrl: content.embedUrl, embedType: content.embedType, embedSize: content.embedSize, duration: content.duration, author: content.author, image: content.image, file: content.file, attributes: content.attributes, instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) })))
let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(url: content.url, displayUrl: content.displayUrl, hash: content.hash, type: content.type, websiteName: content.websiteName, title: content.title, text: content.text, embedUrl: content.embedUrl, embedType: content.embedType, embedSize: content.embedSize, duration: content.duration, author: content.author, image: content.image, file: content.file, story: content.story, attributes: content.attributes, instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) })))
let updatedWebpage = TelegramMediaWebpage(webpageId: webpage.webpageId, content: updatedContent)
return postbox.transaction { transaction -> TelegramMediaWebpage in
updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage)

View File

@ -374,6 +374,7 @@ swift_library(
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
"//submodules/TelegramUI/Components/MoreHeaderButton",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -142,6 +142,7 @@ public final class ChatListNavigationBar: Component {
private var currentLayout: CurrentLayout?
private var rawScrollOffset: CGFloat?
private var currentAllowAvatarsExpansion: Bool = false
public private(set) var clippedScrollOffset: CGFloat?
public var deferScrollApplication: Bool = false
@ -191,14 +192,19 @@ public final class ChatListNavigationBar: Component {
public func applyCurrentScroll(transition: Transition) {
if let rawScrollOffset = self.rawScrollOffset, self.hasDeferredScrollOffset {
self.applyScroll(offset: rawScrollOffset, transition: transition)
self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, transition: transition)
}
}
public func applyScroll(offset: CGFloat, forceUpdate: Bool = false, transition: Transition) {
public func applyScroll(offset: CGFloat, allowAvatarsExpansion: Bool, forceUpdate: Bool = false, transition: Transition) {
if self.currentAllowAvatarsExpansion != allowAvatarsExpansion, allowAvatarsExpansion {
self.addStoriesUnlockedAnimation(duration: 0.3, animateScrollUnlocked: false)
}
let transition = transition
self.rawScrollOffset = offset
self.currentAllowAvatarsExpansion = allowAvatarsExpansion
if self.deferScrollApplication && !forceUpdate {
self.hasDeferredScrollOffset = true
@ -286,7 +292,7 @@ public final class ChatListNavigationBar: Component {
let clippedStoriesOffset = max(0.0, min(clippedScrollOffset, defaultStoriesOffsetDistance))
var storiesOffsetFraction: CGFloat
var storiesUnlockedOffsetFraction: CGFloat
if !component.isSearchActive, component.secondaryTransition == 0.0, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty {
if !component.isSearchActive, component.secondaryTransition == 0.0, let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty, allowAvatarsExpansion {
if component.storiesUnlocked {
storiesOffsetFraction = clippedStoriesOffset / defaultStoriesOffsetDistance
storiesUnlockedOffsetFraction = 1.0
@ -490,45 +496,54 @@ public final class ChatListNavigationBar: Component {
if uploadProgressUpdated {
if let rawScrollOffset = self.rawScrollOffset {
self.applyScroll(offset: rawScrollOffset, forceUpdate: true, transition: transition)
self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, forceUpdate: true, transition: transition)
}
}
if storiesUnlockedUpdated, case let .curve(duration, _) = transition.animation {
self.applyScrollFractionAnimator?.invalidate()
self.applyScrollFractionAnimator = nil
self.storiesOffsetStartFraction = self.storiesOffsetFraction
self.storiesUnlockedStartFraction = self.storiesUnlockedFraction
let storiesUnlocked = component.storiesUnlocked
self.applyScrollFraction = 0.0
self.applyScrollUnlockedFraction = 0.0
self.applyScrollFractionAnimator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in
guard let self else {
return
}
let t = listViewAnimationCurveSystem(value)
self.applyScrollFraction = t
self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t)
if let rawScrollOffset = self.rawScrollOffset {
self.hasDeferredScrollOffset = true
self.applyScroll(offset: rawScrollOffset, transition: transition)
}
}, completion: { [weak self] in
guard let self else {
return
}
self.applyScrollFractionAnimator?.invalidate()
self.applyScrollFractionAnimator = nil
})
self.addStoriesUnlockedAnimation(duration: duration, animateScrollUnlocked: true)
}
return size
}
private func addStoriesUnlockedAnimation(duration: Double, animateScrollUnlocked: Bool) {
guard let component = self.component else {
return
}
self.applyScrollFractionAnimator?.invalidate()
self.applyScrollFractionAnimator = nil
self.storiesOffsetStartFraction = self.storiesOffsetFraction
self.storiesUnlockedStartFraction = self.storiesUnlockedFraction
let storiesUnlocked = component.storiesUnlocked
self.applyScrollFraction = 0.0
self.applyScrollUnlockedFraction = 0.0
self.applyScrollFractionAnimator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in
guard let self else {
return
}
let t = listViewAnimationCurveSystem(value)
self.applyScrollFraction = t
if animateScrollUnlocked {
self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t)
}
if let rawScrollOffset = self.rawScrollOffset {
self.hasDeferredScrollOffset = true
self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, transition: .immediate)
}
}, completion: { [weak self] in
guard let self else {
return
}
self.applyScrollFractionAnimator?.invalidate()
self.applyScrollFractionAnimator = nil
})
}
}
public func makeView() -> View {

View File

@ -6,17 +6,23 @@ import TelegramPresentationData
public final class AvatarStoryIndicatorComponent: Component {
public let hasUnseen: Bool
public let isDarkTheme: Bool
public init(
hasUnseen: Bool
hasUnseen: Bool,
isDarkTheme: Bool
) {
self.hasUnseen = hasUnseen
self.isDarkTheme = isDarkTheme
}
public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool {
if lhs.hasUnseen != rhs.hasUnseen {
return false
}
if lhs.isDarkTheme != rhs.isDarkTheme {
return false
}
return true
}
@ -69,7 +75,11 @@ public final class AvatarStoryIndicatorComponent: Component {
if component.hasUnseen {
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
} else {
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
if component.isDarkTheme {
colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor]
} else {
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
}
}
let colorSpace = CGColorSpaceCreateDeviceRGB()

View File

@ -593,8 +593,16 @@ public final class StoryItemSetContainerComponent: Component {
}
func activateInput() {
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View {
inputPanelView.activateInput()
guard let component = self.component else {
return
}
if component.slice.peer.id == component.context.account.peerId {
self.displayViewList = true
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View {
inputPanelView.activateInput()
}
}
}
@ -621,6 +629,16 @@ public final class StoryItemSetContainerComponent: Component {
)
footerPanelView.layer.animateAlpha(from: 0.0, to: footerPanelView.alpha, duration: 0.28)
}
if let viewListView = self.viewList?.view.view {
viewListView.layer.animatePosition(
from: CGPoint(x: 0.0, y: self.bounds.height - self.contentContainerView.frame.maxY),
to: CGPoint(),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
additive: true
)
viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28)
}
if let captionItemView = self.captionItem?.view.view {
captionItemView.layer.animatePosition(
from: CGPoint(x: 0.0, y: self.bounds.height - captionItemView.frame.minY),
@ -704,6 +722,17 @@ public final class StoryItemSetContainerComponent: Component {
)
footerPanelView.layer.animateAlpha(from: footerPanelView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let viewListView = self.viewList?.view.view {
viewListView.layer.animatePosition(
from: CGPoint(),
to: CGPoint(x: 0.0, y: self.bounds.height - self.contentContainerView.frame.maxY),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
removeOnCompletion: false,
additive: true
)
viewListView.layer.animateAlpha(from: viewListView.alpha, to: 0.0, duration: 0.28, removeOnCompletion: false)
}
if let captionItemView = self.captionItem?.view.view {
captionItemView.layer.animatePosition(
from: CGPoint(),
@ -1006,10 +1035,7 @@ public final class StoryItemSetContainerComponent: Component {
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)
)
var currentItem: StoryContentItem?
currentItem = component.slice.item
let footerPanelSize = self.footerPanel.update(
/*let footerPanelSize = self.footerPanel.update(
transition: transition,
component: AnyComponent(StoryFooterPanelComponent(
context: component.context,
@ -1239,7 +1265,7 @@ public final class StoryItemSetContainerComponent: Component {
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 200.0)
)
)*/
let bottomContentInsetWithoutInput = bottomContentInset
var viewListInset: CGFloat = 0.0
@ -1260,7 +1286,7 @@ public final class StoryItemSetContainerComponent: Component {
inputPanelIsOverlay = true
}
if self.displayViewList {
if component.slice.peer.id == component.context.account.peerId {
let viewList: ViewList
var viewListTransition = transition
if let current = self.viewList {
@ -1273,6 +1299,14 @@ public final class StoryItemSetContainerComponent: Component {
self.viewList = viewList
}
let outerExpansionFraction: CGFloat
if self.displayViewList {
outerExpansionFraction = 1.0
} else {
outerExpansionFraction = component.verticalPanFraction
}
viewList.view.parentState = state
let viewListSize = viewList.view.update(
transition: viewListTransition,
component: AnyComponent(StoryItemSetViewListComponent(
@ -1282,19 +1316,242 @@ public final class StoryItemSetContainerComponent: Component {
strings: component.strings,
safeInsets: component.safeInsets,
storyItem: component.slice.item.storyItem,
outerExpansionFraction: outerExpansionFraction,
close: { [weak self] in
guard let self else {
return
}
self.displayViewList = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
},
expandViewStats: { [weak self] in
guard let self else {
return
}
if !self.displayViewList {
self.displayViewList = true
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
},
deleteAction: { [weak self] in
guard let self, let component = self.component else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component else {
return
}
component.delete()
/*if let currentSlice = self.currentSlice, let index = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }) {
let item = currentSlice.items[index]
if currentSlice.items.count == 1 {
component.navigateToItemSet(.next)
} else {
var nextIndex: Int = index + 1
if nextIndex >= currentSlice.items.count {
nextIndex = currentSlice.items.count - 1
}
self.focusedItemId = currentSlice.items[nextIndex].id
currentSlice.items[nextIndex].markAsSeen?()
self.state?.updated(transition: .immediate)
}
item.delete?()
}*/
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
actionSheet.dismissed = { [weak self] _ in
guard let self else {
return
}
self.actionSheet = nil
self.updateIsProgressPaused()
}
self.actionSheet = actionSheet
self.updateIsProgressPaused()
component.presentController(actionSheet)
},
moreAction: { [weak self] sourceView, gesture in
guard let self, let component = self.component, let controller = component.controller() else {
return
}
var items: [ContextMenuItem] = []
let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0
let privacyText: String
switch component.slice.item.storyItem.privacy?.base {
case .closeFriends:
if additionalCount != 0 {
privacyText = "Close Friends (+\(additionalCount)"
} else {
privacyText = "Close Friends"
}
case .contacts:
if additionalCount != 0 {
privacyText = "Contacts (+\(additionalCount)"
} else {
privacyText = "Contacts"
}
case .nobody:
if additionalCount != 0 {
if additionalCount == 1 {
privacyText = "\(additionalCount) Person"
} else {
privacyText = "\(additionalCount) People"
}
} else {
privacyText = "Only Me"
}
default:
privacyText = "Everyone"
}
items.append(.action(ContextMenuActionItem(text: "Who can see", textLayout: .secondLineWithValue(privacyText), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
self.openItemPrivacySettings()
})))
items.append(.action(ContextMenuActionItem(text: "Edit Story", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
self.openStoryEditing()
})))
items.append(.separator)
component.controller()?.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), 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.updateStoriesArePinned(ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start()
if component.slice.item.storyItem.isPinned {
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
self.component?.presentController(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "Story removed from your profile", timeout: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
))
} else {
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
self.component?.presentController(UndoOverlayController(
presentationData: presentationData,
content: .info(title: "Story saved to your profile", text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
))
}
})))
items.append(.action(ContextMenuActionItem(text: "Save image", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor)
}, action: { _, a in
a(.default)
})))
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
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
component.presentController(UndoOverlayController(
presentationData: presentationData,
content: .linkCopied(text: "Link copied."),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
))
}
})
})))
items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { _, a in
a(.default)
})))
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
contextController.dismissed = { [weak self] in
guard let self else {
return
}
self.contextController = nil
self.updateIsProgressPaused()
}
self.contextController = contextController
self.updateIsProgressPaused()
controller.present(contextController, in: .window(.root))
}
)),
environment: {},
containerSize: availableSize
)
let viewListFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - viewListSize.height), size: viewListSize)
if let viewListView = viewList.view.view {
if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View {
var animateIn = false
if viewListView.superview == nil {
self.addSubview(viewListView)
@ -1303,15 +1560,15 @@ public final class StoryItemSetContainerComponent: Component {
viewListTransition.setFrame(view: viewListView, frame: viewListFrame)
if animateIn, !transition.animation.isImmediate {
transition.animatePosition(view: viewListView, from: CGPoint(x: 0.0, y: viewListFrame.height), to: CGPoint(), additive: true)
viewListView.animateIn(transition: transition)
}
}
viewListInset = viewListFrame.height
viewListInset = viewList.externalState.effectiveHeight
inputPanelBottomInset = viewListInset
} else if let viewList = self.viewList {
self.viewList = nil
if let viewListView = viewList.view.view {
transition.setPosition(view: viewListView, position: CGPoint(x: viewListView.center.x, y: availableSize.height + viewListView.bounds.height * 0.5), completion: { [weak viewListView] _ in
if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View {
viewListView.animateOut(transition: transition, completion: { [weak viewListView] in
viewListView?.removeFromSuperview()
})
}
@ -1320,12 +1577,8 @@ public final class StoryItemSetContainerComponent: Component {
let contentDefaultBottomInset: CGFloat = bottomContentInset
let contentSize = CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - contentDefaultBottomInset)
let contentVisualBottomInset: CGFloat
if self.displayViewList {
contentVisualBottomInset = viewListInset + 12.0
} else {
contentVisualBottomInset = contentDefaultBottomInset
}
let contentVisualBottomInset: CGFloat = max(contentDefaultBottomInset, viewListInset)
let contentVisualHeight = availableSize.height - component.containerInsets.top - contentVisualBottomInset
let contentVisualScale = contentVisualHeight / contentSize.height
@ -1721,7 +1974,7 @@ public final class StoryItemSetContainerComponent: Component {
}
}
var footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize)
/*var footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize)
var footerPanelAlpha: CGFloat = (focusedItem?.isMy == true && !self.displayViewList) ? 1.0 : 0.0
if case .regular = component.metrics.widthClass {
footerPanelAlpha *= component.visibilityFraction
@ -1735,7 +1988,7 @@ public final class StoryItemSetContainerComponent: Component {
}
transition.setFrame(view: footerPanelView, frame: footerPanelFrame)
transition.setAlpha(view: footerPanelView, alpha: footerPanelAlpha)
}
}*/
let bottomGradientHeight = inputPanelSize.height + 32.0
transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - component.inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight)))

View File

@ -10,9 +10,13 @@ import AccountContext
import SwiftSignalKit
import TelegramStringFormatting
import ShimmerEffect
import StoryFooterPanelComponent
final class StoryItemSetViewListComponent: Component {
final class ExternalState {
fileprivate(set) var minimizedHeight: CGFloat = 0.0
fileprivate(set) var effectiveHeight: CGFloat = 0.0
init() {
}
}
@ -23,7 +27,11 @@ final class StoryItemSetViewListComponent: Component {
let strings: PresentationStrings
let safeInsets: UIEdgeInsets
let storyItem: EngineStoryItem
let outerExpansionFraction: CGFloat
let close: () -> Void
let expandViewStats: () -> Void
let deleteAction: () -> Void
let moreAction: (UIView, ContextGesture?) -> Void
init(
externalState: ExternalState,
@ -32,7 +40,11 @@ final class StoryItemSetViewListComponent: Component {
strings: PresentationStrings,
safeInsets: UIEdgeInsets,
storyItem: EngineStoryItem,
close: @escaping () -> Void
outerExpansionFraction: CGFloat,
close: @escaping () -> Void,
expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void
) {
self.externalState = externalState
self.context = context
@ -40,7 +52,11 @@ final class StoryItemSetViewListComponent: Component {
self.strings = strings
self.safeInsets = safeInsets
self.storyItem = storyItem
self.outerExpansionFraction = outerExpansionFraction
self.close = close
self.expandViewStats = expandViewStats
self.deleteAction = deleteAction
self.moreAction = moreAction
}
static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool {
@ -56,6 +72,9 @@ final class StoryItemSetViewListComponent: Component {
if lhs.storyItem != rhs.storyItem {
return false
}
if lhs.outerExpansionFraction != rhs.outerExpansionFraction {
return false
}
return true
}
@ -110,11 +129,30 @@ final class StoryItemSetViewListComponent: Component {
return true
}
}
private final class PanState {
var startContentOffsetY: CGFloat = 0.0
var fraction: CGFloat = 0.0
var accumulatedOffset: CGFloat = 0.0
init() {
}
}
private final class EventCycleState {
var ignoreScrolling: Bool = false
init() {
}
}
final class View: UIView, UIScrollViewDelegate {
final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
private let navigationBarBackground: BlurredBackgroundView
private let navigationSeparator: SimpleLayer
private let navigationTitle = ComponentView<Empty>()
private let navigationPanel = ComponentView<Empty>()
private let navigationLeftButton = ComponentView<Empty>()
private let backgroundView: UIView
@ -138,6 +176,9 @@ final class StoryItemSetViewListComponent: Component {
private var viewListState: EngineStoryViewListContext.State?
private var requestedLoadMoreToken: EngineStoryViewListContext.LoadMoreToken?
private var dismissPanState: PanState?
private var eventCycleState: EventCycleState?
override init(frame: CGRect) {
self.navigationBarBackground = BlurredBackgroundView(color: .clear, enableBlur: true)
self.navigationSeparator = SimpleLayer()
@ -153,12 +194,18 @@ final class StoryItemSetViewListComponent: Component {
self.scrollView.indicatorStyle = .white
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.backgroundView)
self.addSubview(self.scrollView)
self.addSubview(self.navigationBarBackground)
self.layer.addSublayer(self.navigationSeparator)
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self
self.addGestureRecognizer(panRecognizer)
}
required init?(coder: NSCoder) {
@ -169,16 +216,131 @@ final class StoryItemSetViewListComponent: Component {
self.viewListDisposable?.dispose()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
if case .began = recognizer.state {
let dismissPanState = PanState()
dismissPanState.startContentOffsetY = 0.0
self.dismissPanState = dismissPanState
}
if let dismissPanState = self.dismissPanState {
let relativeTranslationY = recognizer.translation(in: self).y - dismissPanState.startContentOffsetY
let overflowY = self.scrollView.contentOffset.y - relativeTranslationY
dismissPanState.accumulatedOffset += -overflowY
dismissPanState.accumulatedOffset = max(0.0, dismissPanState.accumulatedOffset)
if dismissPanState.accumulatedOffset > 0.0 {
self.scrollView.contentOffset = CGPoint()
let eventCycleState = EventCycleState()
eventCycleState.ignoreScrolling = true
self.eventCycleState = eventCycleState
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
self.eventCycleState = nil
}
}
dismissPanState.startContentOffsetY = recognizer.translation(in: self).y
self.state?.updated(transition: .immediate)
}
case .cancelled, .ended:
if let dismissPanState = self.dismissPanState {
self.dismissPanState = nil
let relativeTranslationY = recognizer.translation(in: self).y - dismissPanState.startContentOffsetY
let overflowY = self.scrollView.contentOffset.y - relativeTranslationY
dismissPanState.accumulatedOffset += -overflowY
dismissPanState.accumulatedOffset = max(0.0, dismissPanState.accumulatedOffset)
if dismissPanState.accumulatedOffset > 0.0 {
self.scrollView.contentOffset = CGPoint()
let eventCycleState = EventCycleState()
eventCycleState.ignoreScrolling = true
self.eventCycleState = eventCycleState
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
self.eventCycleState = nil
}
}
let velocityY = recognizer.velocity(in: self).y
if dismissPanState.accumulatedOffset > 150.0 || (dismissPanState.accumulatedOffset > 0.0 && velocityY > 300.0) {
self.component?.close()
} else {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
}
default:
break
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let navigationPanelView = self.navigationPanel.view {
if let result = navigationPanelView.hitTest(self.convert(point, to: navigationPanelView), with: event) {
return result
}
}
if !self.backgroundView.frame.contains(point) {
return nil
}
return super.hitTest(point, with: event)
}
func animateIn(transition: Transition) {
let offset = self.bounds.height - self.navigationBarBackground.frame.minY
Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset))
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: 0.0))
}
func animateOut(transition: Transition, completion: @escaping () -> Void) {
let offset = self.bounds.height - self.navigationBarBackground.frame.minY
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset), completion: { _ in
completion()
})
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
if let eventCycleState = self.eventCycleState {
if eventCycleState.ignoreScrolling {
self.ignoreScrolling = true
scrollView.contentOffset = CGPoint()
self.ignoreScrolling = false
return
}
}
self.updateScrolling(transition: .immediate)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if let eventCycleState = self.eventCycleState {
if eventCycleState.ignoreScrolling {
targetContentOffset.pointee.y = 0.0
}
}
}
private func updateScrolling(transition: Transition) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
@ -315,7 +477,7 @@ final class StoryItemSetViewListComponent: Component {
self.visiblePlaceholderViews.removeValue(forKey: id)
}
if let viewList = self.viewList, let viewListState = self.viewListState, visibleBounds.maxY >= self.scrollView.contentSize.height - 200.0 {
if let viewList = self.viewList, let viewListState = self.viewListState, viewListState.loadMoreToken != nil, visibleBounds.maxY >= self.scrollView.contentSize.height - 200.0 {
if self.requestedLoadMoreToken != viewListState.loadMoreToken {
self.requestedLoadMoreToken = viewListState.loadMoreToken
viewList.loadMore()
@ -330,7 +492,7 @@ final class StoryItemSetViewListComponent: Component {
self.component = component
self.state = state
let size = CGSize(width: availableSize.width, height: min(availableSize.height, 500.0))
let minimizedHeight = min(availableSize.height, 500.0)
if themeUpdated {
self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
@ -364,11 +526,11 @@ final class StoryItemSetViewListComponent: Component {
let sideInset: CGFloat = 16.0
let navigationHeight: CGFloat = 56.0
let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: navigationHeight))
let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - minimizedHeight + 12.0), size: CGSize(width: availableSize.width, height: navigationHeight))
transition.setFrame(view: self.navigationBarBackground, frame: navigationBarFrame)
self.navigationBarBackground.update(size: navigationBarFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.navigationSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: size.width, height: UIScreenPixel)))
transition.setFrame(layer: self.navigationSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
let navigationLeftButtonSize = self.navigationLeftButton.update(
transition: transition,
@ -384,7 +546,7 @@ final class StoryItemSetViewListComponent: Component {
environment: {},
containerSize: CGSize(width: 120.0, height: 100.0)
)
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: navigationLeftButtonSize)
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: navigationBarFrame.minY), size: navigationLeftButtonSize)
if let navigationLeftButtonView = self.navigationLeftButton.view {
if navigationLeftButtonView.superview == nil {
self.addSubview(navigationLeftButtonView)
@ -392,36 +554,58 @@ final class StoryItemSetViewListComponent: Component {
transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame)
}
let titleText: String
let expansionOffset = availableSize.height - self.navigationBarBackground.frame.minY
let viewCount = self.viewListState?.totalCount ?? component.storyItem.views?.seenCount
if let viewCount {
if viewCount == 1 {
titleText = "1 View"
} else {
titleText = "\(viewCount) Views"
}
} else {
titleText = "No Views"
var dismissOffsetY: CGFloat = 0.0
if let dismissPanState = self.dismissPanState {
dismissOffsetY = -dismissPanState.accumulatedOffset
}
let navigationTitleSize = self.navigationTitle.update(
transition: .immediate,
component: AnyComponent(Text(
text: titleText, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor
dismissOffsetY -= (1.0 - component.outerExpansionFraction) * expansionOffset
let dismissFraction: CGFloat = 1.0 - max(0.0, min(1.0, -dismissOffsetY / expansionOffset))
let navigationPanelSize = self.navigationPanel.update(
transition: transition,
component: AnyComponent(StoryFooterPanelComponent(
context: component.context,
storyItem: component.storyItem,
expandFraction: dismissFraction,
expandViewStats: { [weak self] in
guard let self, let component = self.component else {
return
}
component.expandViewStats()
},
deleteAction: { [weak self] in
guard let self, let component = self.component else {
return
}
component.deleteAction()
},
moreAction: { [weak self] sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.moreAction(sourceView, gesture)
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: navigationHeight)
containerSize: CGSize(width: availableSize.width, height: 200.0)
)
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - navigationTitleSize.width) * 0.5), y: floor((navigationBarFrame.height - navigationTitleSize.height) * 0.5)), size: navigationTitleSize)
if let navigationTitleView = self.navigationTitle.view {
if navigationTitleView.superview == nil {
self.addSubview(navigationTitleView)
if let navigationPanelView = self.navigationPanel.view {
if navigationPanelView.superview == nil {
self.addSubview(navigationPanelView)
}
transition.setPosition(view: navigationTitleView, position: navigationTitleFrame.center)
transition.setBounds(view: navigationTitleView, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size))
let expandedNavigationPanelFrame = CGRect(origin: CGPoint(x: navigationBarFrame.minX, y: navigationBarFrame.minY + 4.0), size: navigationPanelSize)
let collapsedNavigationPanelFrame = CGRect(origin: CGPoint(x: navigationBarFrame.minX, y: navigationBarFrame.minY - navigationPanelSize.height - component.safeInsets.bottom - 1.0), size: navigationPanelSize)
transition.setFrame(view: navigationPanelView, frame: collapsedNavigationPanelFrame.interpolate(to: expandedNavigationPanelFrame, amount: dismissFraction))
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: size.width, height: size.height - navigationBarFrame.maxY)))
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height)))
let measureItemSize = self.measureItem.update(
transition: .immediate,
@ -439,7 +623,7 @@ final class StoryItemSetViewListComponent: Component {
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: 1000.0)
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
if self.placeholderImage == nil || themeUpdated {
@ -467,9 +651,9 @@ final class StoryItemSetViewListComponent: Component {
}
let itemLayout = ItemLayout(
containerSize: size,
containerSize: CGSize(width: availableSize.width, height: minimizedHeight),
bottomInset: component.safeInsets.bottom,
topInset: 0.0,
topInset: navigationHeight,
sideInset: sideInset,
itemHeight: measureItemSize.height,
itemCount: self.viewListState?.items.count ?? 0
@ -480,8 +664,8 @@ final class StoryItemSetViewListComponent: Component {
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
let scrollContentInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.minY), size: CGSize(width: availableSize.width, height: minimizedHeight)))
let scrollContentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
let scrollIndicatorInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: component.safeInsets.bottom, right: 0.0)
if self.scrollView.contentInset != scrollContentInsets {
self.scrollView.contentInset = scrollContentInsets
@ -496,7 +680,14 @@ final class StoryItemSetViewListComponent: Component {
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
return size
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: dismissOffsetY))
component.externalState.minimizedHeight = minimizedHeight
let effectiveHeight: CGFloat = minimizedHeight * dismissFraction + (1.0 - dismissFraction) * (60.0 + component.safeInsets.bottom + 1.0)
component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, effectiveHeight))
return availableSize
}
}

View File

@ -340,9 +340,9 @@ final class StoryItemContentComponent: Component {
var messageMedia: EngineMedia?
switch component.item.media {
case let .image(image):
messageMedia = .image(image)
messageMedia = .image(image)
case let .file(file):
messageMedia = .file(file)
messageMedia = .file(file)
default:
break
}

View File

@ -12,6 +12,7 @@ import MoreHeaderButton
public final class StoryFooterPanelComponent: Component {
public let context: AccountContext
public let storyItem: EngineStoryItem?
public let expandFraction: CGFloat
public let expandViewStats: () -> Void
public let deleteAction: () -> Void
public let moreAction: (UIView, ContextGesture?) -> Void
@ -19,6 +20,7 @@ public final class StoryFooterPanelComponent: Component {
public init(
context: AccountContext,
storyItem: EngineStoryItem?,
expandFraction: CGFloat,
expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void
@ -26,6 +28,7 @@ public final class StoryFooterPanelComponent: Component {
self.context = context
self.storyItem = storyItem
self.expandViewStats = expandViewStats
self.expandFraction = expandFraction
self.deleteAction = deleteAction
self.moreAction = moreAction
}
@ -37,12 +40,16 @@ public final class StoryFooterPanelComponent: Component {
if lhs.storyItem != rhs.storyItem {
return false
}
if lhs.expandFraction != rhs.expandFraction {
return false
}
return true
}
public final class View: UIView {
private let viewStatsButton: HighlightableButton
private let viewStatsText = ComponentView<Empty>()
private let viewStatsExpandedText = ComponentView<Empty>()
private let deleteButton = ComponentView<Empty>()
private var moreButton: MoreHeaderButton?
@ -98,6 +105,8 @@ public final class StoryFooterPanelComponent: Component {
let avatarsNodeFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize)
self.avatarsNode.frame = avatarsNodeFrame
//transition.setScale(view: self.avatarsNode.view, scale: CGFloat(1.0).interpolate(to: 0.001, amount: component.expandFraction))
transition.setAlpha(view: self.avatarsNode.view, alpha: pow(1.0 - component.expandFraction, 1.0))
if !avatarsSize.width.isZero {
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
}
@ -124,18 +133,45 @@ public final class StoryFooterPanelComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width, height: size.height)
)
let viewStatsTextFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize)
let viewStatsExpandedTextSize = self.viewStatsExpandedText.update(
transition: .immediate,
component: AnyComponent(Text(text: viewsText, font: Font.semibold(17.0), color: .white)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: size.height)
)
let viewStatsCollapsedFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize)
let viewStatsExpandedFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - viewStatsExpandedTextSize.width) * 0.5), y: floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize)
let viewStatsCurrentFrame = viewStatsCollapsedFrame.interpolate(to: viewStatsExpandedFrame, amount: component.expandFraction)
let viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction)
let viewStatsTextFrame = viewStatsCollapsedFrame.size.centered(around: viewStatsTextCenter)
if let viewStatsTextView = self.viewStatsText.view {
if viewStatsTextView.superview == nil {
viewStatsTextView.layer.anchorPoint = CGPoint()
viewStatsTextView.isUserInteractionEnabled = false
self.viewStatsButton.addSubview(viewStatsTextView)
}
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin)
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.center)
transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size))
transition.setAlpha(view: viewStatsTextView, alpha: pow(1.0 - component.expandFraction, 1.2))
transition.setScale(view: viewStatsTextView, scale: viewStatsCurrentFrame.width / viewStatsTextFrame.width)
}
let viewStatsExpandedTextFrame = viewStatsExpandedFrame.size.centered(around: viewStatsTextCenter)
if let viewStatsExpandedTextView = self.viewStatsExpandedText.view {
if viewStatsExpandedTextView.superview == nil {
viewStatsExpandedTextView.isUserInteractionEnabled = false
self.viewStatsButton.addSubview(viewStatsExpandedTextView)
}
transition.setPosition(view: viewStatsExpandedTextView, position: viewStatsExpandedTextFrame.center)
transition.setBounds(view: viewStatsExpandedTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsExpandedTextFrame.size))
transition.setAlpha(view: viewStatsExpandedTextView, alpha: pow(component.expandFraction, 1.2))
transition.setScale(view: viewStatsExpandedTextView, scale: viewStatsCurrentFrame.width / viewStatsExpandedTextFrame.width)
}
transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: viewStatsTextFrame.maxX, height: viewStatsTextFrame.maxY + 8.0)))
self.viewStatsButton.isUserInteractionEnabled = component.expandFraction == 0.0
var rightContentOffset: CGFloat = availableSize.width - 12.0
@ -162,6 +198,8 @@ public final class StoryFooterPanelComponent: Component {
}
transition.setFrame(view: deleteButtonView, frame: CGRect(origin: CGPoint(x: rightContentOffset - deleteButtonSize.width, y: floor((size.height - deleteButtonSize.height) * 0.5)), size: deleteButtonSize))
rightContentOffset -= deleteButtonSize.width + 8.0
transition.setAlpha(view: deleteButtonView, alpha: pow(1.0 - component.expandFraction, 1.0))
}
let moreButton: MoreHeaderButton
@ -197,6 +235,7 @@ public final class StoryFooterPanelComponent: Component {
let buttonSize = CGSize(width: 32.0, height: 44.0)
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white)))
transition.setFrame(view: moreButton.view, frame: CGRect(origin: CGPoint(x: rightContentOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize))
transition.setAlpha(view: moreButton.view, alpha: pow(1.0 - component.expandFraction, 1.0))
return size
}

View File

@ -635,14 +635,18 @@ public final class StoryPeerListItemComponent: Component {
self.indicatorShapeLayer.lineWidth = indicatorLineWidth
if hadUnseen != component.hasUnseen || hadProgress != (component.ringAnimation != nil) {
if hadUnseen != component.hasUnseen || themeUpdated || hadProgress != (component.ringAnimation != nil) {
let locations: [CGFloat] = [0.0, 1.0]
let colors: [CGColor]
if component.hasUnseen || component.ringAnimation != nil {
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
} else {
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
if component.theme.overallDarkAppearance {
colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor]
} else {
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
}
}
self.indicatorColorLayer.locations = locations.map { $0 as NSNumber }

View File

@ -0,0 +1,26 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StorySetIndicatorComponent",
module_name = "StorySetIndicatorComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/AvatarNode",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/PhotoResources",
"//submodules/AccountContext",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,415 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import TelegramCore
import Postbox
import SwiftSignalKit
import AccountContext
import PhotoResources
private final class ShapeImageView: UIView {
struct Item: Equatable {
var position: CGPoint
var diameter: CGFloat
var image: UIImage?
}
struct Params: Equatable {
var items: [Item]
var innerSpacing: CGFloat
var lineWidth: CGFloat
var borderColors: [UInt32]
}
var params: Params?
override func draw(_ rect: CGRect) {
guard let params = self.params else {
return
}
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(rect)
context.setFillColor(UIColor.black.cgColor)
for item in params.items {
context.fillEllipse(in: CGRect(origin: CGPoint(x: item.position.x - item.diameter * 0.5, y: item.position.y - item.diameter * 0.5), size: CGSize(width: item.diameter, height: item.diameter)))
}
context.setFillColor(UIColor.clear.cgColor)
for item in params.items {
context.fillEllipse(in: CGRect(origin: CGPoint(x: item.position.x - item.diameter * 0.5, y: item.position.y - item.diameter * 0.5), size: CGSize(width: item.diameter, height: item.diameter)).insetBy(dx: params.lineWidth, dy: params.lineWidth))
}
context.setBlendMode(.sourceIn)
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: params.borderColors.map {
UIColor(rgb: $0).cgColor
} as CFArray, locations: nil)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 50.0), options: [])
context.setBlendMode(.copy)
for i in (0 ..< params.items.count).reversed() {
let item = params.items[i]
if i != params.items.count - 1 {
let previousItem = params.items[i]
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
context.fillEllipse(in: CGRect(origin: CGPoint(x: previousItem.position.x - previousItem.diameter * 0.5, y: previousItem.position.y - previousItem.diameter * 0.5), size: CGSize(width: previousItem.diameter, height: previousItem.diameter)).insetBy(dx: params.lineWidth, dy: params.lineWidth))
}
context.setBlendMode(.normal)
let imageRect = CGRect(origin: CGPoint(x: item.position.x - item.diameter * 0.5, y: item.position.y - item.diameter * 0.5), size: CGSize(width: item.diameter, height: item.diameter)).insetBy(dx: params.lineWidth + params.innerSpacing, dy: params.lineWidth + params.innerSpacing)
if let image = item.image {
context.draw(image.cgImage!, in: imageRect)
} else {
context.setFillColor(UIColor.black.cgColor)
context.fillEllipse(in: imageRect)
}
}
}
}
public final class StorySetIndicatorComponent: Component {
public let context: AccountContext
public let peer: EnginePeer
public let items: [EngineStoryItem]
public let hasUnseen: Bool
public let totalCount: Int
public let theme: PresentationTheme
public let action: () -> Void
public init(
context: AccountContext,
peer: EnginePeer,
items: [EngineStoryItem],
hasUnseen: Bool,
totalCount: Int,
theme: PresentationTheme,
action: @escaping () -> Void
) {
self.context = context
self.peer = peer
self.items = items
self.hasUnseen = hasUnseen
self.totalCount = totalCount
self.theme = theme
self.action = action
}
public static func ==(lhs: StorySetIndicatorComponent, rhs: StorySetIndicatorComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.hasUnseen != rhs.hasUnseen {
return false
}
if lhs.totalCount != rhs.totalCount {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
private final class ImageContext {
private var fetchDisposable: Disposable?
private var imageDisposable: Disposable?
private let updated: () -> Void
private(set) var image: UIImage?
init(context: AccountContext, peer: EnginePeer, item: EngineStoryItem, updated: @escaping () -> Void) {
self.updated = updated
let peerReference = PeerReference(peer._asPeer())
var messageMedia: EngineMedia?
switch item.media {
case let .image(image):
messageMedia = .image(image)
case let .file(file):
messageMedia = .file(file)
default:
break
}
let reloadMedia = true
if reloadMedia, let messageMedia, let peerReference {
var signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var fetchSignal: Signal<Never, NoError>?
switch messageMedia {
case let .image(image):
signal = chatMessagePhoto(
postbox: context.account.postbox,
userLocation: .other,
photoReference: .story(peer: peerReference, id: item.id, media: image),
synchronousLoad: false,
highQuality: true
)
if let representation = image.representations.last {
fetchSignal = fetchedMediaResource(
mediaBox: context.account.postbox.mediaBox,
userLocation: .other,
userContentType: .image,
reference: ImageMediaReference.story(peer: peerReference, id: item.id, media: image).resourceReference(representation.resource)
)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
}
case let .file(file):
signal = mediaGridMessageVideo(
postbox: context.account.postbox,
userLocation: .other,
videoReference: .story(peer: peerReference, id: item.id, media: file),
onlyFullSize: false,
useLargeThumbnail: true,
synchronousLoad: false,
autoFetchFullSizeThumbnail: true,
overlayColor: nil,
nilForEmptyResult: false,
useMiniThumbnailIfAvailable: false,
blurred: false
)
fetchSignal = fetchedMediaResource(
mediaBox: context.account.postbox.mediaBox,
userLocation: .other,
userContentType: .image,
reference: FileMediaReference.story(peer: peerReference, id: item.id, media: file).resourceReference(file.resource)
)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
default:
break
}
if let signal {
var wasSynchronous = true
self.imageDisposable = (signal
|> deliverOnMainQueue).start(next: { [weak self] process in
guard let self else {
return
}
let outerSize = CGSize(width: 1080.0, height: 1920.0)
let innerSize = CGSize(width: 26.0, height: 26.0)
let result = process(TransformImageArguments(corners: ImageCorners(radius: innerSize.width * 0.5), imageSize: outerSize.aspectFilled(innerSize), boundingSize: innerSize, intrinsicInsets: UIEdgeInsets()))
if let result {
self.image = result.generateImage()
if !wasSynchronous {
self.updated()
}
}
})
wasSynchronous = false
}
self.fetchDisposable?.dispose()
self.fetchDisposable = nil
if let fetchSignal {
self.fetchDisposable = (fetchSignal |> deliverOnMainQueue).start()
}
}
}
deinit {
self.fetchDisposable?.dispose()
self.imageDisposable?.dispose()
}
}
public final class View: UIView {
private let button: HighlightTrackingButton
private let imageView: ShapeImageView
private let text = ComponentView<Empty>()
private var imageContexts: [Int32: ImageContext] = [:]
private var component: StorySetIndicatorComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.button = HighlightTrackingButton()
self.imageView = ShapeImageView(frame: CGRect())
self.imageView.isUserInteractionEnabled = false
self.imageView.backgroundColor = nil
self.imageView.isOpaque = false
super.init(frame: frame)
self.button.addSubview(self.imageView)
self.addSubview(self.button)
self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.button.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
let transition = Transition(animation: .curve(duration: 0.16, curve: .easeInOut))
transition.setSublayerTransform(view: self.button, transform: CATransform3DMakeScale(0.8, 0.8, 1.0))
} else {
let transition = Transition(animation: .curve(duration: 0.24, curve: .easeInOut))
transition.setSublayerTransform(view: self.button, transform: CATransform3DIdentity)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.action()
}
func update(component: StorySetIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let innerDiameter: CGFloat = 26.0
let innerSpacing: CGFloat = 1.33
let lineWidth: CGFloat = 1.33
let outerDiameter: CGFloat = innerDiameter + innerSpacing * 2.0 + lineWidth * 2.0
let overflow: CGFloat = 14.0
var validIds: [Int32] = []
var items: [ShapeImageView.Item] = []
for i in 0 ..< min(3, component.items.count) {
validIds.append(component.items[i].id)
let imageContext: ImageContext
if let current = self.imageContexts[component.items[i].id] {
imageContext = current
} else {
var update = false
imageContext = ImageContext(context: component.context, peer: component.peer, item: component.items[i], updated: { [weak self] in
guard let self else {
return
}
if update {
self.state?.updated(transition: .immediate)
}
})
self.imageContexts[component.items[i].id] = imageContext
update = true
}
items.append(ShapeImageView.Item(
position: CGPoint(x: outerDiameter * 0.5 + CGFloat(i) * (outerDiameter - overflow), y: outerDiameter * 0.5),
diameter: outerDiameter,
image: imageContext.image
))
}
var removeIds: [Int32] = []
for (id, _) in self.imageContexts {
if !validIds.contains(id) {
removeIds.append(id)
}
}
for id in removeIds {
self.imageContexts.removeValue(forKey: id)
}
let maxItemsWidth: CGFloat = outerDiameter * 0.5 + CGFloat(max(0, 3 - 1)) * (outerDiameter - overflow) + outerDiameter * 0.5
let effectiveItemsWidth: CGFloat = outerDiameter * 0.5 + CGFloat(max(0, items.count - 1)) * (outerDiameter - overflow) + outerDiameter * 0.5
let borderColors: [UInt32]
if component.theme.overallDarkAppearance {
if component.hasUnseen {
borderColors = [
0x34C76F,
0x3DA1FD
]
} else {
borderColors = [
0x48484A,
0x48484A
]
}
} else {
if component.hasUnseen {
borderColors = [
0x34C76F,
0x3DA1FD
]
} else {
borderColors = [
0xD8D8E1,
0xD8D8E1
]
}
}
let imageSize = CGSize(width: maxItemsWidth, height: outerDiameter)
let params = ShapeImageView.Params(
items: items,
innerSpacing: innerSpacing,
lineWidth: lineWidth,
borderColors: borderColors
)
if self.imageView.params != params || self.imageView.bounds.size != imageSize {
self.imageView.params = params
self.imageView.frame = CGRect(origin: CGPoint(), size: imageSize)
self.imageView.setNeedsDisplay()
}
//TODO:localize
let textValue: String
if component.totalCount == 0 {
textValue = ""
} else if component.totalCount == 1 {
textValue = "1 story"
} else {
textValue = "\(component.totalCount) stories"
}
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(Text(text: textValue, font: Font.semibold(17.0), color: .white)),
environment: {},
containerSize: CGSize(width: 300.0, height: 100.0)
)
let textFrame = CGRect(origin: CGPoint(x: effectiveItemsWidth + 6.0, y: 5.0), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
textView.layer.anchorPoint = CGPoint()
textView.isUserInteractionEnabled = false
self.button.addSubview(textView)
}
transition.setPosition(view: textView, position: textFrame.origin)
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
}
let size = CGSize(width: effectiveItemsWidth + 6.0 + textSize.width, height: outerDiameter)
transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size))
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -2136,6 +2136,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if message.associatedStories[story.storyId] == nil {
storiesRequiredValidation = true
}
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let story = content.story {
if message.associatedStories[story.storyId] == nil {
storiesRequiredValidation = true
}
}
}
if contentRequiredValidation {

View File

@ -503,6 +503,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
isImage = true
} else if let _ = media as? WallpaperPreviewMedia {
isImage = true
} else if let _ = media as? TelegramMediaStory {
isImage = true
}
}
@ -677,6 +679,20 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme {
skipStandardStatus = true
}
} else if let story = media as? TelegramMediaStory {
var media: Media?
if let storyValue = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyValue {
media = item.media
}
var automaticDownload = false
if let media {
automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: media)
}
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, story, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout
}
}

View File

@ -164,12 +164,12 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
switch type {
case .instagram, .twitter:
if automaticPlayback {
mainMedia = webpage.file ?? webpage.image
mainMedia = webpage.story ?? webpage.file ?? webpage.image
} else {
mainMedia = webpage.image ?? webpage.file
mainMedia = webpage.story ?? webpage.image ?? webpage.file
}
default:
mainMedia = webpage.file ?? webpage.image
mainMedia = webpage.story ?? webpage.file ?? webpage.image
}
let themeMimeType = "application/x-tgtheme-ios"
@ -216,6 +216,8 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
}
mediaAndFlags = (image, flags)
}
} else if let story = mainMedia as? TelegramMediaStory {
mediaAndFlags = (story, [])
} else if let type = webpage.type {
if type == "telegram_background" {
var colors: [UInt32] = []
@ -338,7 +340,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
}
for media in item.message.media {
switch media {
case _ as TelegramMediaImage, _ as TelegramMediaFile:
case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaStory:
mediaAndFlags = (media, ChatMessageAttachedContentNodeMediaFlags())
default:
break

View File

@ -26,32 +26,39 @@ import StoryContainerScreen
import StoryContentComponent
func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
var story: TelegramMediaStory?
for media in params.message.media {
if let media = media as? TelegramMediaStory {
let navigationController = params.navigationController
let context = params.context
let storyContent = SingleStoryContentContextImpl(context: params.context, storyId: media.storyId)
let _ = (storyContent.state
|> take(1)
|> deliverOnMainQueue).start(next: { [weak navigationController] _ in
let transitionIn: StoryContainerScreen.TransitionIn? = nil
let storyContainerScreen = StoryContainerScreen(
context: context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { _, _ in
let transitionOut: StoryContainerScreen.TransitionOut? = nil
return transitionOut
}
)
navigationController?.pushViewController(storyContainerScreen)
})
return true
story = media
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.story != nil {
story = content.story
}
}
if let story {
let navigationController = params.navigationController
let context = params.context
let storyContent = SingleStoryContentContextImpl(context: params.context, storyId: story.storyId)
let _ = (storyContent.state
|> take(1)
|> deliverOnMainQueue).start(next: { [weak navigationController] _ in
let transitionIn: StoryContainerScreen.TransitionIn? = nil
let storyContainerScreen = StoryContainerScreen(
context: context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { _, _ in
let transitionOut: StoryContainerScreen.TransitionOut? = nil
return transitionOut
}
)
navigationController?.pushViewController(storyContainerScreen)
})
return true
}
if let mediaData = chatMessageGalleryControllerData(context: params.context, chatLocation: params.chatLocation, chatLocationContextHolder: params.chatLocationContextHolder, message: params.message, navigationController: params.navigationController, standalone: params.standalone, reverseMessageGalleryOrder: params.reverseMessageGalleryOrder, mode: params.mode, source: params.gallerySource, synchronousLoad: false, actionInteraction: params.actionInteraction) {
switch mediaData {
case let .url(url):

View File

@ -438,8 +438,6 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.avatarNode.view.addGestureRecognizer(tapGestureRecognizer)
self.updateStoryView(transition: .immediate)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
@ -455,7 +453,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
self.playbackStartDisposable.dispose()
}
func updateStoryView(transition: ContainedViewLayoutTransition) {
func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) {
if let hasUnseenStories = self.hasUnseenStories {
let avatarStoryView: ComponentView<Empty>
if let current = self.avatarStoryView {
@ -468,7 +466,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
let _ = avatarStoryView.update(
transition: Transition(transition),
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: hasUnseenStories
hasUnseen: hasUnseenStories,
isDarkTheme: theme.overallDarkAppearance
)),
environment: {},
containerSize: self.avatarNode.bounds.size
@ -783,6 +782,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
}
}
}
self.updateStoryView(transition: .immediate, theme: theme)
}
}
@ -1161,6 +1162,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
var animateOverlaysFadeIn: (() -> Void)?
var openStories: (() -> Void)?
init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) {
self.isSettings = isSettings
@ -1250,6 +1252,13 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
}
strongSelf.animateOverlaysFadeIn?()
}
self.listContainerNode.openStories = { [weak self] in
guard let self else {
return
}
self.openStories?()
}
}
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
@ -1591,7 +1600,7 @@ struct PeerInfoHeaderNavigationButtonSpec: Equatable {
let isForExpandedView: Bool
}
final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode {
private var presentationData: PresentationData?
private(set) var leftButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
private(set) var rightButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]

View File

@ -3044,54 +3044,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return
}
if !gallery, let expiringStoryList = strongSelf.expiringStoryList, let expiringStoryListState = strongSelf.expiringStoryListState, !expiringStoryListState.items.isEmpty {
let _ = expiringStoryList
let storyContent = StoryContentContextImpl(context: strongSelf.context, includeHidden: false, focusedPeerId: strongSelf.peerId, singlePeer: true)
let _ = (storyContent.state
|> take(1)
|> deliverOnMainQueue).start(next: { storyContentState in
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
transitionIn = nil
let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.view
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
sourceCornerRadius: transitionView.bounds.height * 0.5
)
self.headerNode.avatarListNode.avatarContainerNode.avatarNode.isHidden = true
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { [weak self] peerId, _ in
guard let self else {
return nil
}
let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.view
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
transitionView: nil,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5,
destinationIsAvatar: true,
completed: { [weak self] in
guard let self else {
return
}
self.headerNode.avatarListNode.avatarContainerNode.avatarNode.isHidden = false
}
)
}
)
self.controller?.push(storyContainerScreen)
})
if !gallery, let expiringStoryListState = strongSelf.expiringStoryListState, !expiringStoryListState.items.isEmpty {
strongSelf.openStories(fromAvatar: true)
return
}
@ -3913,18 +3867,40 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} else if peerId.namespace == Namespaces.Peer.CloudUser {
let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId)
self.expiringStoryList = expiringStoryList
self.expiringStoryListDisposable = (expiringStoryList.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
self.expiringStoryListDisposable = (combineLatest(queue: .mainQueue(),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
expiringStoryList.state
)
|> deliverOnMainQueue).start(next: { [weak self] peer, state in
guard let self, let peer else {
return
}
self.expiringStoryListState = state
if state.items.isEmpty {
self.headerNode.avatarListNode.avatarContainerNode.hasUnseenStories = nil
self.headerNode.avatarListNode.listContainerNode.storyParams = nil
} else {
self.headerNode.avatarListNode.avatarContainerNode.hasUnseenStories = state.hasUnseen
self.headerNode.avatarListNode.listContainerNode.storyParams = (peer, state.items.prefix(3).compactMap { item -> EngineStoryItem? in
switch item {
case let .item(item):
return item
case .placeholder:
return nil
}
}, state.items.count, state.hasUnseen)
}
self.requestLayout(animated: false)
if self.headerNode.avatarListNode.openStories == nil {
self.headerNode.avatarListNode.openStories = { [weak self] in
guard let self else {
return
}
self.openStories(fromAvatar: false)
}
}
self.headerNode.avatarListNode.avatarContainerNode.updateStoryView(transition: .immediate)
})
}
}
@ -4115,6 +4091,64 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.headerNode.navigationButtonContainer.performAction?(.cancel, nil, nil)
}
private func openStories(fromAvatar: Bool) {
if let expiringStoryList = self.expiringStoryList, let expiringStoryListState = self.expiringStoryListState, !expiringStoryListState.items.isEmpty {
let _ = expiringStoryList
let storyContent = StoryContentContextImpl(context: self.context, includeHidden: false, focusedPeerId: self.peerId, singlePeer: true)
let _ = (storyContent.state
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] storyContentState in
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
if fromAvatar {
let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.view
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
sourceCornerRadius: transitionView.bounds.height * 0.5
)
}
self.headerNode.avatarListNode.avatarContainerNode.avatarNode.isHidden = true
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { [weak self] peerId, _ in
guard let self else {
return nil
}
if !fromAvatar {
return nil
}
let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.view
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
transitionView: nil,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5,
destinationIsAvatar: true,
completed: { [weak self] in
guard let self else {
return
}
self.headerNode.avatarListNode.avatarContainerNode.avatarNode.isHidden = false
}
)
}
)
self.controller?.push(storyContainerScreen)
})
return
}
}
private func openMessage(id: MessageId) -> Bool {
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else {
return false