mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Stories
This commit is contained in:
parent
7150af3fe1
commit
ceda80c3a5
@ -263,6 +263,11 @@
|
|||||||
|
|
||||||
"PUSH_CHAT_REQ_JOINED" = "%2$@|%1$@ was accepted into the group";
|
"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_MESSAGE_FWDS" = "%1$@ forwarded you %2$d messages";
|
||||||
"LOCAL_CHANNEL_MESSAGE_FWDS" = "%1$@ posted %2$d forwarded messages";
|
"LOCAL_CHANNEL_MESSAGE_FWDS" = "%1$@ posted %2$d forwarded messages";
|
||||||
"LOCAL_CHAT_MESSAGE_FWDS" = "%1$@ forwarded %2$d messages";
|
"LOCAL_CHAT_MESSAGE_FWDS" = "%1$@ forwarded %2$d messages";
|
||||||
|
@ -2387,63 +2387,80 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var items: [ContextMenuItem] = []
|
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
|
||||||
|
}
|
||||||
|
|
||||||
//TODO:localize
|
var items: [ContextMenuItem] = []
|
||||||
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()
|
//TODO:localize
|
||||||
})
|
if peer.id == self.context.account.peerId {
|
||||||
})))
|
items.append(.action(ContextMenuActionItem(text: "Add Story", icon: { theme in
|
||||||
} else {
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
|
||||||
items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
|
}, action: { [weak self] c, _ in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
|
c.dismiss(completion: {
|
||||||
}, 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
|
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
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
|
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 {
|
let isMuted = notificationSettings.storiesMuted == true
|
||||||
return
|
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)
|
||||||
self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: true)
|
}, action: { [weak self] _, f in
|
||||||
})))
|
f(.default)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
guard let self else {
|
||||||
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3464,7 +3464,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||||||
return nil
|
return nil
|
||||||
case .links:
|
case .links:
|
||||||
var media: [EngineMedia] = []
|
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(
|
let message = EngineMessage(
|
||||||
stableId: 0,
|
stableId: 0,
|
||||||
stableVersion: 0,
|
stableVersion: 0,
|
||||||
|
@ -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 frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
|
||||||
let item: InstantPageItem
|
let item: InstantPageItem
|
||||||
if let url = url, let coverId = coverId, case let .image(image) = media[coverId] {
|
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)
|
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)
|
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)
|
||||||
|
@ -26,6 +26,9 @@ swift_library(
|
|||||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||||
"//submodules/AccountContext:AccountContext",
|
"//submodules/AccountContext:AccountContext",
|
||||||
"//submodules/AvatarVideoNode:AvatarVideoNode",
|
"//submodules/AvatarVideoNode:AvatarVideoNode",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/Components/ComponentDisplayAdapters",
|
||||||
|
"//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -16,6 +16,9 @@ import RadialStatusNode
|
|||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import AvatarNode
|
import AvatarNode
|
||||||
import AvatarVideoNode
|
import AvatarVideoNode
|
||||||
|
import ComponentFlow
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
import StorySetIndicatorComponent
|
||||||
|
|
||||||
private class PeerInfoAvatarListLoadingStripNode: ASImageNode {
|
private class PeerInfoAvatarListLoadingStripNode: ASImageNode {
|
||||||
private var currentInHierarchy = false
|
private var currentInHierarchy = false
|
||||||
@ -577,6 +580,9 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
public let topShadowNode: ASImageNode
|
public let topShadowNode: ASImageNode
|
||||||
public let bottomShadowNode: ASImageNode
|
public let bottomShadowNode: ASImageNode
|
||||||
|
|
||||||
|
public var storyParams: (peer: EnginePeer, items: [EngineStoryItem], count: Int, hasUnseen: Bool)?
|
||||||
|
private var expandedStorySetIndicator: ComponentView<Empty>?
|
||||||
|
|
||||||
public let contentNode: ASDisplayNode
|
public let contentNode: ASDisplayNode
|
||||||
let leftHighlightNode: ASDisplayNode
|
let leftHighlightNode: ASDisplayNode
|
||||||
let rightHighlightNode: ASDisplayNode
|
let rightHighlightNode: ASDisplayNode
|
||||||
@ -612,6 +618,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
public var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
|
public var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
|
||||||
public var currentIndexUpdated: (() -> Void)?
|
public var currentIndexUpdated: (() -> Void)?
|
||||||
|
|
||||||
|
public var openStories: (() -> Void)?
|
||||||
|
|
||||||
public let isReady = Promise<Bool>()
|
public let isReady = Promise<Bool>()
|
||||||
private var didSetReady = false
|
private var didSetReady = false
|
||||||
|
|
||||||
@ -914,6 +922,12 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
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)
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1228,6 +1242,45 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
self.updateItems(size: size, transition: transition, stripTransition: transition)
|
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) {
|
private func updateStrips(size: CGSize, itemsAdded: Bool, stripTransition: ContainedViewLayoutTransition) {
|
||||||
|
@ -961,6 +961,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-350980120] = { return Api.WebPage.parse_webPageEmpty($0) }
|
dict[-350980120] = { return Api.WebPage.parse_webPageEmpty($0) }
|
||||||
dict[1930545681] = { return Api.WebPage.parse_webPageNotModified($0) }
|
dict[1930545681] = { return Api.WebPage.parse_webPageNotModified($0) }
|
||||||
dict[-981018084] = { return Api.WebPage.parse_webPagePending($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[1421174295] = { return Api.WebPageAttribute.parse_webPageAttributeTheme($0) }
|
||||||
dict[211046684] = { return Api.WebViewMessageSent.parse_webViewMessageSent($0) }
|
dict[211046684] = { return Api.WebViewMessageSent.parse_webViewMessageSent($0) }
|
||||||
dict[202659196] = { return Api.WebViewResult.parse_webViewResultUrl($0) }
|
dict[202659196] = { return Api.WebViewResult.parse_webViewResultUrl($0) }
|
||||||
|
@ -187,11 +187,21 @@ public extension Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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?)
|
case webPageAttributeTheme(flags: Int32, documents: [Api.Document]?, settings: Api.ThemeSettings?)
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
switch self {
|
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):
|
case .webPageAttributeTheme(let flags, let documents, let settings):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(1421174295)
|
buffer.appendInt32(1421174295)
|
||||||
@ -209,11 +219,35 @@ public extension Api {
|
|||||||
|
|
||||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
switch self {
|
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):
|
case .webPageAttributeTheme(let flags, let documents, let settings):
|
||||||
return ("webPageAttributeTheme", [("flags", flags as Any), ("documents", documents as Any), ("settings", settings as Any)])
|
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? {
|
public static func parse_webPageAttributeTheme(_ reader: BufferReader) -> WebPageAttribute? {
|
||||||
var _1: Int32?
|
var _1: Int32?
|
||||||
_1 = reader.readInt32()
|
_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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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 {
|
public extension Api.account {
|
||||||
enum WallPapers: TypeConstructorDescription {
|
enum WallPapers: TypeConstructorDescription {
|
||||||
case wallPapers(hash: Int64, wallpapers: [Api.WallPaper])
|
case wallPapers(hash: Int64, wallpapers: [Api.WallPaper])
|
||||||
|
@ -6,12 +6,14 @@ import TelegramApi
|
|||||||
|
|
||||||
func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPageAttribute) -> TelegramMediaWebpageAttribute? {
|
func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPageAttribute) -> TelegramMediaWebpageAttribute? {
|
||||||
switch attribute {
|
switch attribute {
|
||||||
case let .webPageAttributeTheme(_, documents, settings):
|
case let .webPageAttributeTheme(_, documents, settings):
|
||||||
var files: [TelegramMediaFile] = []
|
var files: [TelegramMediaFile] = []
|
||||||
if let documents = documents {
|
if let documents = documents {
|
||||||
files = documents.compactMap { telegramMediaFileFromApiDocument($0) }
|
files = documents.compactMap { telegramMediaFileFromApiDocument($0) }
|
||||||
}
|
}
|
||||||
return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $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 {
|
if let document = document {
|
||||||
file = telegramMediaFileFromApiDocument(document)
|
file = telegramMediaFileFromApiDocument(document)
|
||||||
}
|
}
|
||||||
|
var story: TelegramMediaStory?
|
||||||
var webpageAttributes: [TelegramMediaWebpageAttribute] = []
|
var webpageAttributes: [TelegramMediaWebpageAttribute] = []
|
||||||
if let attributes = attributes {
|
if let attributes = attributes {
|
||||||
webpageAttributes = attributes.compactMap(telegramMediaWebpageAttributeFromApiWebpageAttribute)
|
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?
|
var instantPage: InstantPage?
|
||||||
if let cachedPage = cachedPage {
|
if let cachedPage = cachedPage {
|
||||||
instantPage = InstantPage(apiPage: 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:
|
case .webPageEmpty:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1254,6 +1254,8 @@ public final class AccountViewTracker {
|
|||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let storyMedia = media as? TelegramMediaStory {
|
if let storyMedia = media as? TelegramMediaStory {
|
||||||
result.insert(storyMedia.storyId)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,10 +85,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
|
|||||||
|
|
||||||
public let image: TelegramMediaImage?
|
public let image: TelegramMediaImage?
|
||||||
public let file: TelegramMediaFile?
|
public let file: TelegramMediaFile?
|
||||||
|
public let story: TelegramMediaStory?
|
||||||
public let attributes: [TelegramMediaWebpageAttribute]
|
public let attributes: [TelegramMediaWebpageAttribute]
|
||||||
public let instantPage: InstantPage?
|
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.url = url
|
||||||
self.displayUrl = displayUrl
|
self.displayUrl = displayUrl
|
||||||
self.hash = hash
|
self.hash = hash
|
||||||
@ -103,6 +104,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
|
|||||||
self.author = author
|
self.author = author
|
||||||
self.image = image
|
self.image = image
|
||||||
self.file = file
|
self.file = file
|
||||||
|
self.story = story
|
||||||
self.attributes = attributes
|
self.attributes = attributes
|
||||||
self.instantPage = instantPage
|
self.instantPage = instantPage
|
||||||
}
|
}
|
||||||
@ -141,6 +143,12 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
|
|||||||
self.file = nil
|
self.file = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let story = decoder.decodeObjectForKey("stry") as? TelegramMediaStory {
|
||||||
|
self.story = story
|
||||||
|
} else {
|
||||||
|
self.story = nil
|
||||||
|
}
|
||||||
|
|
||||||
var effectiveAttributes: [TelegramMediaWebpageAttribute] = []
|
var effectiveAttributes: [TelegramMediaWebpageAttribute] = []
|
||||||
if let attributes = decoder.decodeObjectArrayWithDecoderForKey("attr") as [TelegramMediaWebpageAttribute]? {
|
if let attributes = decoder.decodeObjectArrayWithDecoderForKey("attr") as [TelegramMediaWebpageAttribute]? {
|
||||||
effectiveAttributes.append(contentsOf: attributes)
|
effectiveAttributes.append(contentsOf: attributes)
|
||||||
@ -218,6 +226,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
|
|||||||
} else {
|
} else {
|
||||||
encoder.encodeNil(forKey: "fi")
|
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")
|
encoder.encodeObjectArray(self.attributes, forKey: "attr")
|
||||||
|
|
||||||
@ -261,6 +274,14 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage
|
|||||||
return false
|
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 {
|
if lhs.attributes.count != rhs.attributes.count {
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
@ -289,6 +310,14 @@ public final class TelegramMediaWebpage: Media, Equatable {
|
|||||||
}
|
}
|
||||||
public let peerIds: [PeerId] = []
|
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 webpageId: MediaId
|
||||||
public let content: TelegramMediaWebpageContent
|
public let content: TelegramMediaWebpageContent
|
||||||
|
|
||||||
|
@ -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> {
|
func _internal_updatePeerMuteSetting(account: Account, peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal<Void, NoError> {
|
||||||
return account.postbox.transaction { transaction -> Void in
|
return account.postbox.transaction { transaction -> Void in
|
||||||
_internal_updatePeerMuteSetting(account: account, transaction: transaction, peerId: peerId, threadId: threadId, muteInterval: muteInterval)
|
_internal_updatePeerMuteSetting(account: account, transaction: transaction, peerId: peerId, threadId: threadId, muteInterval: muteInterval)
|
||||||
|
@ -247,6 +247,11 @@ public extension TelegramEngine {
|
|||||||
return _internal_togglePeerMuted(account: self.account, peerId: peerId, threadId: threadId)
|
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> {
|
public func updatePeerMuteSetting(peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal<Void, NoError> {
|
||||||
return _internal_updatePeerMuteSetting(account: self.account, peerId: peerId, threadId: threadId, muteInterval: muteInterval)
|
return _internal_updatePeerMuteSetting(account: self.account, peerId: peerId, threadId: threadId, muteInterval: muteInterval)
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ public func actualizedWebpage(postbox: Postbox, network: Network, webpage: Teleg
|
|||||||
return updatedWebpage
|
return updatedWebpage
|
||||||
}
|
}
|
||||||
} else if let result = result, case let .webPageNotModified(_, viewsValue) = result, let views = viewsValue, case let .Loaded(content) = webpage.content {
|
} 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)
|
let updatedWebpage = TelegramMediaWebpage(webpageId: webpage.webpageId, content: updatedContent)
|
||||||
return postbox.transaction { transaction -> TelegramMediaWebpage in
|
return postbox.transaction { transaction -> TelegramMediaWebpage in
|
||||||
updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage)
|
updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage)
|
||||||
|
@ -374,6 +374,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
|
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
|
||||||
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
||||||
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||||
|
"//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent",
|
||||||
] + select({
|
] + select({
|
||||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||||
|
@ -198,7 +198,7 @@ public final class ChatListNavigationBar: Component {
|
|||||||
|
|
||||||
public func applyScroll(offset: CGFloat, allowAvatarsExpansion: Bool, forceUpdate: Bool = false, transition: Transition) {
|
public func applyScroll(offset: CGFloat, allowAvatarsExpansion: Bool, forceUpdate: Bool = false, transition: Transition) {
|
||||||
if self.currentAllowAvatarsExpansion != allowAvatarsExpansion, allowAvatarsExpansion {
|
if self.currentAllowAvatarsExpansion != allowAvatarsExpansion, allowAvatarsExpansion {
|
||||||
self.addStoriesUnlockedAnimation(duration: 0.3)
|
self.addStoriesUnlockedAnimation(duration: 0.3, animateScrollUnlocked: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
let transition = transition
|
let transition = transition
|
||||||
@ -501,13 +501,13 @@ public final class ChatListNavigationBar: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if storiesUnlockedUpdated, case let .curve(duration, _) = transition.animation {
|
if storiesUnlockedUpdated, case let .curve(duration, _) = transition.animation {
|
||||||
self.addStoriesUnlockedAnimation(duration: duration)
|
self.addStoriesUnlockedAnimation(duration: duration, animateScrollUnlocked: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addStoriesUnlockedAnimation(duration: Double) {
|
private func addStoriesUnlockedAnimation(duration: Double, animateScrollUnlocked: Bool) {
|
||||||
guard let component = self.component else {
|
guard let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -528,7 +528,9 @@ public final class ChatListNavigationBar: Component {
|
|||||||
|
|
||||||
let t = listViewAnimationCurveSystem(value)
|
let t = listViewAnimationCurveSystem(value)
|
||||||
self.applyScrollFraction = t
|
self.applyScrollFraction = t
|
||||||
self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t)
|
if animateScrollUnlocked {
|
||||||
|
self.applyScrollUnlockedFraction = storiesUnlocked ? t : (1.0 - t)
|
||||||
|
}
|
||||||
|
|
||||||
if let rawScrollOffset = self.rawScrollOffset {
|
if let rawScrollOffset = self.rawScrollOffset {
|
||||||
self.hasDeferredScrollOffset = true
|
self.hasDeferredScrollOffset = true
|
||||||
|
@ -6,17 +6,23 @@ import TelegramPresentationData
|
|||||||
|
|
||||||
public final class AvatarStoryIndicatorComponent: Component {
|
public final class AvatarStoryIndicatorComponent: Component {
|
||||||
public let hasUnseen: Bool
|
public let hasUnseen: Bool
|
||||||
|
public let isDarkTheme: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
hasUnseen: Bool
|
hasUnseen: Bool,
|
||||||
|
isDarkTheme: Bool
|
||||||
) {
|
) {
|
||||||
self.hasUnseen = hasUnseen
|
self.hasUnseen = hasUnseen
|
||||||
|
self.isDarkTheme = isDarkTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool {
|
public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool {
|
||||||
if lhs.hasUnseen != rhs.hasUnseen {
|
if lhs.hasUnseen != rhs.hasUnseen {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.isDarkTheme != rhs.isDarkTheme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +75,11 @@ public final class AvatarStoryIndicatorComponent: Component {
|
|||||||
if component.hasUnseen {
|
if component.hasUnseen {
|
||||||
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
|
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
|
||||||
} else {
|
} 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()
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
|
@ -1497,7 +1497,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
a(.default)
|
a(.default)
|
||||||
})))
|
})))
|
||||||
|
|
||||||
if component.slice.item.storyItem.isPublic {
|
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
|
items.append(.action(ContextMenuActionItem(text: "Copy link", icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||||
}, action: { [weak self] _, a in
|
}, action: { [weak self] _, a in
|
||||||
|
@ -340,9 +340,9 @@ final class StoryItemContentComponent: Component {
|
|||||||
var messageMedia: EngineMedia?
|
var messageMedia: EngineMedia?
|
||||||
switch component.item.media {
|
switch component.item.media {
|
||||||
case let .image(image):
|
case let .image(image):
|
||||||
messageMedia = .image(image)
|
messageMedia = .image(image)
|
||||||
case let .file(file):
|
case let .file(file):
|
||||||
messageMedia = .file(file)
|
messageMedia = .file(file)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -635,14 +635,18 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
|
|
||||||
self.indicatorShapeLayer.lineWidth = indicatorLineWidth
|
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 locations: [CGFloat] = [0.0, 1.0]
|
||||||
let colors: [CGColor]
|
let colors: [CGColor]
|
||||||
|
|
||||||
if component.hasUnseen || component.ringAnimation != nil {
|
if component.hasUnseen || component.ringAnimation != nil {
|
||||||
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
|
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
|
||||||
} else {
|
} 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 }
|
self.indicatorColorLayer.locations = locations.map { $0 as NSNumber }
|
||||||
|
@ -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",
|
||||||
|
],
|
||||||
|
)
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -2136,6 +2136,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
if message.associatedStories[story.storyId] == nil {
|
if message.associatedStories[story.storyId] == nil {
|
||||||
storiesRequiredValidation = true
|
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 {
|
if contentRequiredValidation {
|
||||||
|
@ -503,6 +503,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
isImage = true
|
isImage = true
|
||||||
} else if let _ = media as? WallpaperPreviewMedia {
|
} else if let _ = media as? WallpaperPreviewMedia {
|
||||||
isImage = true
|
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 {
|
if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme {
|
||||||
skipStandardStatus = true
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,12 +164,12 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
switch type {
|
switch type {
|
||||||
case .instagram, .twitter:
|
case .instagram, .twitter:
|
||||||
if automaticPlayback {
|
if automaticPlayback {
|
||||||
mainMedia = webpage.file ?? webpage.image
|
mainMedia = webpage.story ?? webpage.file ?? webpage.image
|
||||||
} else {
|
} else {
|
||||||
mainMedia = webpage.image ?? webpage.file
|
mainMedia = webpage.story ?? webpage.image ?? webpage.file
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
mainMedia = webpage.file ?? webpage.image
|
mainMedia = webpage.story ?? webpage.file ?? webpage.image
|
||||||
}
|
}
|
||||||
|
|
||||||
let themeMimeType = "application/x-tgtheme-ios"
|
let themeMimeType = "application/x-tgtheme-ios"
|
||||||
@ -216,6 +216,8 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
mediaAndFlags = (image, flags)
|
mediaAndFlags = (image, flags)
|
||||||
}
|
}
|
||||||
|
} else if let story = mainMedia as? TelegramMediaStory {
|
||||||
|
mediaAndFlags = (story, [])
|
||||||
} else if let type = webpage.type {
|
} else if let type = webpage.type {
|
||||||
if type == "telegram_background" {
|
if type == "telegram_background" {
|
||||||
var colors: [UInt32] = []
|
var colors: [UInt32] = []
|
||||||
@ -338,7 +340,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
for media in item.message.media {
|
for media in item.message.media {
|
||||||
switch media {
|
switch media {
|
||||||
case _ as TelegramMediaImage, _ as TelegramMediaFile:
|
case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaStory:
|
||||||
mediaAndFlags = (media, ChatMessageAttachedContentNodeMediaFlags())
|
mediaAndFlags = (media, ChatMessageAttachedContentNodeMediaFlags())
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
@ -26,32 +26,39 @@ import StoryContainerScreen
|
|||||||
import StoryContentComponent
|
import StoryContentComponent
|
||||||
|
|
||||||
func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
||||||
|
var story: TelegramMediaStory?
|
||||||
for media in params.message.media {
|
for media in params.message.media {
|
||||||
if let media = media as? TelegramMediaStory {
|
if let media = media as? TelegramMediaStory {
|
||||||
let navigationController = params.navigationController
|
story = media
|
||||||
let context = params.context
|
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.story != nil {
|
||||||
let storyContent = SingleStoryContentContextImpl(context: params.context, storyId: media.storyId)
|
story = content.story
|
||||||
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 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) {
|
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 {
|
switch mediaData {
|
||||||
case let .url(url):
|
case let .url(url):
|
||||||
|
@ -439,8 +439,6 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
|
|||||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||||
self.avatarNode.view.addGestureRecognizer(tapGestureRecognizer)
|
self.avatarNode.view.addGestureRecognizer(tapGestureRecognizer)
|
||||||
|
|
||||||
self.updateStoryView(transition: .immediate)
|
|
||||||
|
|
||||||
self.containerNode.activated = { [weak self] gesture, _ in
|
self.containerNode.activated = { [weak self] gesture, _ in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
@ -455,7 +453,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
|
|||||||
self.playbackStartDisposable.dispose()
|
self.playbackStartDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateStoryView(transition: ContainedViewLayoutTransition) {
|
func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) {
|
||||||
if let hasUnseenStories = self.hasUnseenStories {
|
if let hasUnseenStories = self.hasUnseenStories {
|
||||||
let avatarStoryView: ComponentView<Empty>
|
let avatarStoryView: ComponentView<Empty>
|
||||||
if let current = self.avatarStoryView {
|
if let current = self.avatarStoryView {
|
||||||
@ -468,7 +466,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
|
|||||||
let _ = avatarStoryView.update(
|
let _ = avatarStoryView.update(
|
||||||
transition: Transition(transition),
|
transition: Transition(transition),
|
||||||
component: AnyComponent(AvatarStoryIndicatorComponent(
|
component: AnyComponent(AvatarStoryIndicatorComponent(
|
||||||
hasUnseen: hasUnseenStories
|
hasUnseen: hasUnseenStories,
|
||||||
|
isDarkTheme: theme.overallDarkAppearance
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: self.avatarNode.bounds.size
|
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 itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
|
||||||
var animateOverlaysFadeIn: (() -> Void)?
|
var animateOverlaysFadeIn: (() -> Void)?
|
||||||
|
var openStories: (() -> Void)?
|
||||||
|
|
||||||
init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) {
|
init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) {
|
||||||
self.isSettings = isSettings
|
self.isSettings = isSettings
|
||||||
@ -1250,6 +1252,13 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
strongSelf.animateOverlaysFadeIn?()
|
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) {
|
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
|
let isForExpandedView: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
|
final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode {
|
||||||
private var presentationData: PresentationData?
|
private var presentationData: PresentationData?
|
||||||
private(set) var leftButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
|
private(set) var leftButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
|
||||||
private(set) var rightButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
|
private(set) var rightButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
|
||||||
|
@ -3044,54 +3044,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gallery, let expiringStoryList = strongSelf.expiringStoryList, let expiringStoryListState = strongSelf.expiringStoryListState, !expiringStoryListState.items.isEmpty {
|
if !gallery, let expiringStoryListState = strongSelf.expiringStoryListState, !expiringStoryListState.items.isEmpty {
|
||||||
let _ = expiringStoryList
|
strongSelf.openStories(fromAvatar: true)
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -3913,18 +3867,40 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
} else if peerId.namespace == Namespaces.Peer.CloudUser {
|
} else if peerId.namespace == Namespaces.Peer.CloudUser {
|
||||||
let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId)
|
let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId)
|
||||||
self.expiringStoryList = expiringStoryList
|
self.expiringStoryList = expiringStoryList
|
||||||
self.expiringStoryListDisposable = (expiringStoryList.state
|
self.expiringStoryListDisposable = (combineLatest(queue: .mainQueue(),
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
|
||||||
guard let self else {
|
expiringStoryList.state
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] peer, state in
|
||||||
|
guard let self, let peer else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.expiringStoryListState = state
|
self.expiringStoryListState = state
|
||||||
if state.items.isEmpty {
|
if state.items.isEmpty {
|
||||||
self.headerNode.avatarListNode.avatarContainerNode.hasUnseenStories = nil
|
self.headerNode.avatarListNode.avatarContainerNode.hasUnseenStories = nil
|
||||||
|
self.headerNode.avatarListNode.listContainerNode.storyParams = nil
|
||||||
} else {
|
} else {
|
||||||
self.headerNode.avatarListNode.avatarContainerNode.hasUnseenStories = state.hasUnseen
|
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)
|
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 {
|
private func openMessage(id: MessageId) -> Bool {
|
||||||
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else {
|
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else {
|
||||||
return false
|
return false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user