mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Stories UI
This commit is contained in:
parent
ddd7bd7457
commit
7d6b9c95e7
@ -92,7 +92,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case inlineForums(Bool)
|
||||
case localTranscription(Bool)
|
||||
case enableReactionOverrides(Bool)
|
||||
case playerEmbedding(Bool)
|
||||
case storiesExperiment(Bool)
|
||||
case playlistPlayback(Bool)
|
||||
case enableQuickReactionSwitch(Bool)
|
||||
case voiceConference
|
||||
@ -118,7 +118,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return DebugControllerSection.logging.rawValue
|
||||
case .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases:
|
||||
case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .logTranslationRecognition, .resetTranslationStates:
|
||||
return DebugControllerSection.translation.rawValue
|
||||
@ -213,7 +213,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 41
|
||||
case .resetTranslationStates:
|
||||
return 42
|
||||
case .playerEmbedding:
|
||||
case .storiesExperiment:
|
||||
return 43
|
||||
case .playlistPlayback:
|
||||
return 44
|
||||
@ -1220,12 +1220,12 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .playerEmbedding(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
case let .storiesExperiment(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Gallery X", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||
settings.playerEmbedding = value
|
||||
settings.storiesExperiment = value
|
||||
return PreferencesEntry(settings)
|
||||
})
|
||||
}).start()
|
||||
@ -1384,7 +1384,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
entries.append(.logTranslationRecognition(experimentalSettings.logLanguageRecognition))
|
||||
entries.append(.resetTranslationStates)
|
||||
|
||||
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
|
||||
entries.append(.storiesExperiment(experimentalSettings.storiesExperiment))
|
||||
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
|
||||
entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ swift_library(
|
||||
"//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI",
|
||||
"//submodules/MediaResources:MediaResources",
|
||||
"//submodules/WebsiteType:WebsiteType",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContentComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -14,6 +14,8 @@ import PeerAvatarGalleryUI
|
||||
import GalleryUI
|
||||
import MediaResources
|
||||
import WebsiteType
|
||||
import StoryContainerScreen
|
||||
import StoryContentComponent
|
||||
|
||||
public enum ChatMessageGalleryControllerData {
|
||||
case url(String)
|
||||
@ -28,6 +30,7 @@ public enum ChatMessageGalleryControllerData {
|
||||
case chatAvatars(AvatarGalleryController, Media)
|
||||
case theme(TelegramMediaFile)
|
||||
case other(Media)
|
||||
case story(Signal<StoryContainerScreen, NoError>)
|
||||
}
|
||||
|
||||
private func instantPageBlockMedia(pageId: MediaId, block: InstantPageBlock, media: [MediaId: Media], counter: inout Int) -> [InstantPageGalleryEntry] {
|
||||
@ -267,6 +270,21 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
||||
openChatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
||||
}
|
||||
|
||||
if context.sharedContext.immediateExperimentalUISettings.storiesExperiment {
|
||||
return .story(StoryChatContent.messages(
|
||||
context: context,
|
||||
messageId: message.id
|
||||
)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> map { initialContent in
|
||||
return StoryContainerScreen(
|
||||
context: context,
|
||||
initialContent: initialContent
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return .gallery(startState
|
||||
|> deliverOnMainQueue
|
||||
|> map { startState in
|
||||
|
@ -2057,8 +2057,8 @@ public func chatWebpageSnippetPhoto(account: Account, userLocation: MediaResourc
|
||||
}
|
||||
}
|
||||
|
||||
public func chatMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
return mediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: videoReference)
|
||||
public func chatMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, videoReference: FileMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
return mediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: videoReference, synchronousLoad: synchronousLoad)
|
||||
}
|
||||
|
||||
private func chatSecretMessageVideoData(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal<Data?, NoError> {
|
||||
|
@ -360,6 +360,8 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/SendInviteLinkScreen",
|
||||
"//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen",
|
||||
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContentComponent",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
@ -0,0 +1,20 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "MessageInputPanelComponent",
|
||||
module_name = "MessageInputPanelComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AppBundle",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,112 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AppBundle
|
||||
|
||||
public final class MessageInputPanelComponent: Component {
|
||||
public init() {
|
||||
|
||||
}
|
||||
|
||||
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let fieldBackgroundView: UIImageView
|
||||
private let fieldPlaceholder = ComponentView<Empty>()
|
||||
|
||||
private let attachmentIconView: UIImageView
|
||||
private let recordingIconView: UIImageView
|
||||
private let stickerIconView: UIImageView
|
||||
|
||||
private var component: MessageInputPanelComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.fieldBackgroundView = UIImageView()
|
||||
self.attachmentIconView = UIImageView()
|
||||
self.recordingIconView = UIImageView()
|
||||
self.stickerIconView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.fieldBackgroundView)
|
||||
|
||||
self.addSubview(self.fieldBackgroundView)
|
||||
self.addSubview(self.attachmentIconView)
|
||||
self.addSubview(self.recordingIconView)
|
||||
self.addSubview(self.stickerIconView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let insets = UIEdgeInsets(top: 5.0, left: 41.0, bottom: 5.0, right: 41.0)
|
||||
let fieldCornerRadius: CGFloat = 16.0
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if self.fieldBackgroundView.image == nil {
|
||||
self.fieldBackgroundView.image = generateStretchableFilledCircleImage(diameter: fieldCornerRadius * 2.0, color: nil, strokeColor: UIColor(white: 1.0, alpha: 0.16), strokeWidth: 1.0, backgroundColor: nil)
|
||||
}
|
||||
if self.attachmentIconView.image == nil {
|
||||
self.attachmentIconView.image = UIImage(bundleImageName: "Chat/Input/Text/IconAttachment")?.withRenderingMode(.alwaysTemplate)
|
||||
self.attachmentIconView.tintColor = .white
|
||||
}
|
||||
if self.recordingIconView.image == nil {
|
||||
self.recordingIconView.image = UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone")?.withRenderingMode(.alwaysTemplate)
|
||||
self.recordingIconView.tintColor = .white
|
||||
}
|
||||
if self.stickerIconView.image == nil {
|
||||
self.stickerIconView.image = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconStickers")?.withRenderingMode(.alwaysTemplate)
|
||||
self.stickerIconView.tintColor = .white
|
||||
}
|
||||
|
||||
let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: availableSize.height - insets.top - insets.bottom))
|
||||
transition.setFrame(view: self.fieldBackgroundView, frame: fieldFrame)
|
||||
|
||||
let rightFieldInset: CGFloat = 34.0
|
||||
|
||||
let placeholderSize = self.fieldPlaceholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: "Reply Privately...", font: Font.regular(17.0), color: UIColor(white: 1.0, alpha: 0.16))),
|
||||
environment: {},
|
||||
containerSize: fieldFrame.size
|
||||
)
|
||||
if let fieldPlaceholderView = self.fieldPlaceholder.view {
|
||||
if fieldPlaceholderView.superview == nil {
|
||||
fieldPlaceholderView.layer.anchorPoint = CGPoint()
|
||||
fieldPlaceholderView.isUserInteractionEnabled = false
|
||||
self.addSubview(fieldPlaceholderView)
|
||||
}
|
||||
fieldPlaceholderView.bounds = CGRect(origin: CGPoint(), size: placeholderSize)
|
||||
transition.setPosition(view: fieldPlaceholderView, position: CGPoint(x: fieldFrame.minX + 12.0, y: fieldFrame.minY + floor((fieldFrame.height - placeholderSize.height) * 0.5)))
|
||||
}
|
||||
|
||||
if let image = self.attachmentIconView.image {
|
||||
transition.setFrame(view: self.attachmentIconView, frame: CGRect(origin: CGPoint(x: floor((insets.left - image.size.width) * 0.5), y: floor((availableSize.height - image.size.height) * 0.5)), size: image.size))
|
||||
}
|
||||
if let image = self.recordingIconView.image {
|
||||
transition.setFrame(view: self.recordingIconView, frame: CGRect(origin: CGPoint(x: availableSize.width - insets.right + floor((insets.right - image.size.width) * 0.5), y: floor((availableSize.height - image.size.height) * 0.5)), size: image.size))
|
||||
}
|
||||
if let image = self.stickerIconView.image {
|
||||
transition.setFrame(view: self.stickerIconView, frame: CGRect(origin: CGPoint(x: fieldFrame.maxX - rightFieldInset + floor((rightFieldInset - image.size.width) * 0.5), y: fieldFrame.minY + floor((fieldFrame.height - image.size.height) * 0.5)), size: image.size))
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StoryContainerScreen",
|
||||
module_name = "StoryContainerScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AppBundle",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,139 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
final class MediaNavigationStripComponent: Component {
|
||||
let index: Int
|
||||
let count: Int
|
||||
|
||||
init(index: Int, count: Int) {
|
||||
self.index = index
|
||||
self.count = count
|
||||
}
|
||||
|
||||
static func ==(lhs: MediaNavigationStripComponent, rhs: MediaNavigationStripComponent) -> Bool {
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ItemLayer: SimpleLayer {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.cornerRadius = 1.5
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var visibleItems: [Int: ItemLayer] = [:]
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 1.0
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: MediaNavigationStripComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let spacing: CGFloat = 3.0
|
||||
let itemHeight: CGFloat = 2.0
|
||||
let minItemWidth: CGFloat = 3.0
|
||||
|
||||
var validIndices: [Int] = []
|
||||
if component.count != 0 {
|
||||
var idealItemWidth: CGFloat = (availableSize.width - CGFloat(component.count - 1) * spacing) / CGFloat(component.count)
|
||||
idealItemWidth = round(idealItemWidth)
|
||||
|
||||
let itemWidth: CGFloat
|
||||
if idealItemWidth < minItemWidth {
|
||||
itemWidth = minItemWidth
|
||||
} else {
|
||||
itemWidth = idealItemWidth
|
||||
}
|
||||
|
||||
let globalWidth: CGFloat = CGFloat(component.count) * itemWidth + CGFloat(component.count - 1) * spacing
|
||||
let globalFocusedFrame = CGRect(origin: CGPoint(x: CGFloat(component.index) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))
|
||||
var globalOffset: CGFloat = floor(globalFocusedFrame.midX - availableSize.width * 0.5)
|
||||
if globalOffset > globalWidth - availableSize.width {
|
||||
globalOffset = globalWidth - availableSize.width
|
||||
}
|
||||
if globalOffset < 0.0 {
|
||||
globalOffset = 0.0
|
||||
}
|
||||
|
||||
//itemWidth * itemCount + (itemCount - 1) * spacing = width
|
||||
//itemWidth * itemCount + itemCount * spacing - spacing = width
|
||||
//itemCount * (itemWidth + spacing) = width + spacing
|
||||
//itemCount = (width + spacing) / (itemWidth + spacing)
|
||||
let potentiallyVisibleCount = Int(ceil((availableSize.width + spacing) / (itemWidth + spacing)))
|
||||
for i in (component.index - potentiallyVisibleCount) ... (component.index + potentiallyVisibleCount) {
|
||||
if i < 0 {
|
||||
continue
|
||||
}
|
||||
if i >= component.count {
|
||||
continue
|
||||
}
|
||||
let itemFrame = CGRect(origin: CGPoint(x: -globalOffset + CGFloat(i) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))
|
||||
if itemFrame.maxY < 0.0 || itemFrame.minY >= availableSize.width {
|
||||
continue
|
||||
}
|
||||
|
||||
validIndices.append(i)
|
||||
|
||||
let itemLayer: ItemLayer
|
||||
if let current = self.visibleItems[i] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
itemLayer = ItemLayer()
|
||||
self.layer.addSublayer(itemLayer)
|
||||
self.visibleItems[i] = itemLayer
|
||||
itemLayer.cornerRadius = itemHeight * 0.5
|
||||
}
|
||||
|
||||
transition.setFrame(layer: itemLayer, frame: itemFrame)
|
||||
|
||||
itemLayer.backgroundColor = UIColor(white: 1.0, alpha: i == component.index ? 1.0 : 0.5).cgColor
|
||||
}
|
||||
}
|
||||
|
||||
var removedIndices: [Int] = []
|
||||
for (index, itemLayer) in self.visibleItems {
|
||||
if !validIndices.contains(index) {
|
||||
removedIndices.append(index)
|
||||
itemLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
for index in removedIndices {
|
||||
self.visibleItems.removeValue(forKey: index)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,544 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import AccountContext
|
||||
import SwiftSignalKit
|
||||
import AppBundle
|
||||
import MessageInputPanelComponent
|
||||
|
||||
private final class StoryContainerScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let initialContent: StoryContentItemSlice
|
||||
|
||||
init(
|
||||
initialContent: StoryContentItemSlice
|
||||
) {
|
||||
self.initialContent = initialContent
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryContainerScreenComponent, rhs: StoryContainerScreenComponent) -> Bool {
|
||||
if lhs.initialContent !== rhs.initialContent {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private struct ItemLayout {
|
||||
var size: CGSize
|
||||
|
||||
init(size: CGSize) {
|
||||
self.size = size
|
||||
}
|
||||
}
|
||||
|
||||
private final class VisibleItem {
|
||||
let view = ComponentView<Empty>()
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
private final class InfoItem {
|
||||
let component: AnyComponent<Empty>
|
||||
let view = ComponentView<Empty>()
|
||||
|
||||
init(component: AnyComponent<Empty>) {
|
||||
self.component = component
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private let contentContainerView: UIView
|
||||
private let topContentGradientLayer: SimpleGradientLayer
|
||||
|
||||
private let closeButton: HighlightableButton
|
||||
private let closeButtonIconView: UIImageView
|
||||
|
||||
private let navigationStrip = ComponentView<Empty>()
|
||||
|
||||
private var centerInfoItem: InfoItem?
|
||||
private var rightInfoItem: InfoItem?
|
||||
|
||||
private let inputPanel = ComponentView<Empty>()
|
||||
|
||||
private var component: StoryContainerScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
|
||||
private var itemLayout: ItemLayout?
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var focusedItemId: AnyHashable?
|
||||
private var currentSlice: StoryContentItemSlice?
|
||||
private var currentSliceDisposable: Disposable?
|
||||
|
||||
private var visibleItems: [AnyHashable: VisibleItem] = [:]
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
|
||||
self.contentContainerView = UIView()
|
||||
self.contentContainerView.clipsToBounds = true
|
||||
self.contentContainerView.isUserInteractionEnabled = false
|
||||
|
||||
self.topContentGradientLayer = SimpleGradientLayer()
|
||||
|
||||
self.closeButton = HighlightableButton()
|
||||
self.closeButtonIconView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .black
|
||||
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.alwaysBounceHorizontal = false
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
|
||||
self.addSubview(self.contentContainerView)
|
||||
self.layer.addSublayer(self.topContentGradientLayer)
|
||||
|
||||
self.closeButton.addSubview(self.closeButtonIconView)
|
||||
self.addSubview(self.closeButton)
|
||||
self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.currentSliceDisposable?.dispose()
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state, let currentSlice = self.currentSlice, let focusedItemId = self.focusedItemId, let currentIndex = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }), let itemLayout = self.itemLayout {
|
||||
let point = recognizer.location(in: self)
|
||||
|
||||
var nextIndex: Int
|
||||
if point.x < itemLayout.size.width * 0.5 {
|
||||
nextIndex = currentIndex + 1
|
||||
} else {
|
||||
nextIndex = currentIndex - 1
|
||||
}
|
||||
nextIndex = max(0, min(nextIndex, currentSlice.items.count - 1))
|
||||
if nextIndex != currentIndex {
|
||||
let focusedItemId = currentSlice.items[nextIndex].id
|
||||
self.focusedItemId = focusedItemId
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
self.currentSliceDisposable?.dispose()
|
||||
self.currentSliceDisposable = (currentSlice.update(
|
||||
currentSlice,
|
||||
focusedItemId
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] contentSlice in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentSlice = contentSlice
|
||||
self.state?.updated(transition: .immediate)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func closePressed() {
|
||||
guard let environment = self.environment, let controller = environment.controller() else {
|
||||
return
|
||||
}
|
||||
controller.dismiss()
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
if let focusedItemId = self.focusedItemId, let focusedItem = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) {
|
||||
validIds.append(focusedItemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: VisibleItem
|
||||
if let current = self.visibleItems[focusedItemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
itemTransition = .immediate
|
||||
visibleItem = VisibleItem()
|
||||
self.visibleItems[focusedItemId] = visibleItem
|
||||
}
|
||||
|
||||
let _ = visibleItem.view.update(
|
||||
transition: itemTransition,
|
||||
component: focusedItem.component,
|
||||
environment: {},
|
||||
containerSize: itemLayout.size
|
||||
)
|
||||
if let view = visibleItem.view.view {
|
||||
if view.superview == nil {
|
||||
self.contentContainerView.addSubview(view)
|
||||
}
|
||||
itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size))
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, visibleItem) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let view = visibleItem.view.view {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, completion: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.layer.allowsGroupOpacity = false
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func update(component: StoryContainerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
let isFirstTime = self.component == nil
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
|
||||
if self.component == nil {
|
||||
self.focusedItemId = component.initialContent.focusedItemId
|
||||
self.currentSlice = component.initialContent
|
||||
|
||||
self.currentSliceDisposable?.dispose()
|
||||
self.currentSliceDisposable = (component.initialContent.update(
|
||||
component.initialContent,
|
||||
component.initialContent.focusedItemId
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] contentSlice in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentSlice = contentSlice
|
||||
self.state?.updated(transition: .immediate)
|
||||
})
|
||||
}
|
||||
|
||||
if self.topContentGradientLayer.colors == nil {
|
||||
var locations: [NSNumber] = []
|
||||
var colors: [CGColor] = []
|
||||
let numStops = 4
|
||||
let baseAlpha: CGFloat = 0.5
|
||||
for i in 0 ..< numStops {
|
||||
let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1)
|
||||
locations.append((1.0 - step) as NSNumber)
|
||||
let alphaStep: CGFloat = pow(step, 1.5)
|
||||
colors.append(UIColor.black.withAlphaComponent(alphaStep * baseAlpha).cgColor)
|
||||
}
|
||||
|
||||
self.topContentGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
|
||||
self.topContentGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
|
||||
self.topContentGradientLayer.locations = locations
|
||||
self.topContentGradientLayer.colors = colors
|
||||
self.topContentGradientLayer.type = .axial
|
||||
}
|
||||
|
||||
if let focusedItemId = self.focusedItemId {
|
||||
if let currentSlice = self.currentSlice {
|
||||
if !currentSlice.items.contains(where: { $0.id == focusedItemId }) {
|
||||
self.focusedItemId = currentSlice.items.first?.id
|
||||
}
|
||||
} else {
|
||||
self.focusedItemId = nil
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
self.environment = environment
|
||||
|
||||
let bottomContentInset: CGFloat
|
||||
if !environment.safeInsets.bottom.isZero {
|
||||
bottomContentInset = environment.safeInsets.bottom + 5.0 + 44.0
|
||||
} else {
|
||||
bottomContentInset = 44.0
|
||||
}
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.statusBarHeight), size: CGSize(width: availableSize.width, height: availableSize.height - environment.statusBarHeight - bottomContentInset))
|
||||
transition.setFrame(view: self.contentContainerView, frame: contentFrame)
|
||||
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 14.0)
|
||||
|
||||
if self.closeButtonIconView.image == nil {
|
||||
self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate)
|
||||
self.closeButtonIconView.tintColor = .white
|
||||
}
|
||||
if let image = self.closeButtonIconView.image {
|
||||
let closeButtonFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: 50.0, height: 64.0))
|
||||
transition.setFrame(view: self.closeButton, frame: closeButtonFrame)
|
||||
transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size))
|
||||
}
|
||||
|
||||
var currentRightInfoItem: InfoItem?
|
||||
if let currentSlice = self.currentSlice, let item = currentSlice.items.first(where: { $0.id == self.focusedItemId }) {
|
||||
if let rightInfoComponent = item.rightInfoComponent {
|
||||
if let rightInfoItem = self.rightInfoItem, rightInfoItem.component == item.rightInfoComponent {
|
||||
currentRightInfoItem = rightInfoItem
|
||||
} else {
|
||||
currentRightInfoItem = InfoItem(component: rightInfoComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let rightInfoItem = self.rightInfoItem, currentRightInfoItem?.component != rightInfoItem.component {
|
||||
self.rightInfoItem = nil
|
||||
if let view = rightInfoItem.view.view {
|
||||
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
|
||||
view?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var currentCenterInfoItem: InfoItem?
|
||||
if let currentSlice = self.currentSlice, let item = currentSlice.items.first(where: { $0.id == self.focusedItemId }) {
|
||||
if let centerInfoComponent = item.centerInfoComponent {
|
||||
if let centerInfoItem = self.centerInfoItem, centerInfoItem.component == item.centerInfoComponent {
|
||||
currentCenterInfoItem = centerInfoItem
|
||||
} else {
|
||||
currentCenterInfoItem = InfoItem(component: centerInfoComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let centerInfoItem = self.centerInfoItem, currentCenterInfoItem?.component != centerInfoItem.component {
|
||||
self.centerInfoItem = nil
|
||||
if let view = centerInfoItem.view.view {
|
||||
view.removeFromSuperview()
|
||||
/*view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
|
||||
view?.removeFromSuperview()
|
||||
})*/
|
||||
}
|
||||
}
|
||||
|
||||
if let currentRightInfoItem {
|
||||
self.rightInfoItem = currentRightInfoItem
|
||||
|
||||
let rightInfoItemSize = currentRightInfoItem.view.update(
|
||||
transition: .immediate,
|
||||
component: currentRightInfoItem.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 36.0, height: 36.0)
|
||||
)
|
||||
if let view = currentRightInfoItem.view.view {
|
||||
var animateIn = false
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
animateIn = true
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 6.0 - rightInfoItemSize.width, y: contentFrame.minY + 14.0), size: rightInfoItemSize))
|
||||
|
||||
if animateIn, !isFirstTime {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let currentCenterInfoItem {
|
||||
self.centerInfoItem = currentCenterInfoItem
|
||||
|
||||
let centerInfoItemSize = currentCenterInfoItem.view.update(
|
||||
transition: .immediate,
|
||||
component: currentCenterInfoItem.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: contentFrame.width, height: 44.0)
|
||||
)
|
||||
if let view = currentCenterInfoItem.view.view {
|
||||
var animateIn = false
|
||||
if view.superview == nil {
|
||||
view.isUserInteractionEnabled = false
|
||||
self.addSubview(view)
|
||||
animateIn = true
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY + 10.0), size: centerInfoItemSize))
|
||||
|
||||
if animateIn, !isFirstTime {
|
||||
//view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let currentSlice = self.currentSlice {
|
||||
let navigationStripSideInset: CGFloat = 8.0
|
||||
let navigationStripTopInset: CGFloat = 8.0
|
||||
|
||||
let index = currentSlice.items.first(where: { $0.id == self.focusedItemId })?.position ?? 0
|
||||
|
||||
let _ = self.navigationStrip.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MediaNavigationStripComponent(
|
||||
index: max(0, min(currentSlice.totalCount - 1 - index, currentSlice.totalCount - 1)),
|
||||
count: currentSlice.totalCount
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)
|
||||
)
|
||||
if let navigationStripView = self.navigationStrip.view {
|
||||
if navigationStripView.superview == nil {
|
||||
self.addSubview(navigationStripView)
|
||||
}
|
||||
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: contentFrame.minX + navigationStripSideInset, y: contentFrame.minY + navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
|
||||
}
|
||||
}
|
||||
|
||||
let gradientHeight: CGFloat = 74.0
|
||||
transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: gradientHeight)))
|
||||
|
||||
let itemLayout = ItemLayout(size: contentFrame.size)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
let inputPanelSize = self.inputPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MessageInputPanelComponent(
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 44.0)
|
||||
)
|
||||
if let inputPanelView = self.inputPanel.view {
|
||||
if inputPanelView.superview == nil {
|
||||
self.addSubview(inputPanelView)
|
||||
}
|
||||
transition.setFrame(view: inputPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentFrame.maxY), size: inputPanelSize))
|
||||
}
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
||||
let contentSize = availableSize
|
||||
if contentSize != self.scrollView.contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
initialContent: StoryContentItemSlice
|
||||
) {
|
||||
super.init(context: context, component: StoryContainerScreenComponent(
|
||||
initialContent: initialContent
|
||||
), navigationBarAppearance: .none)
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
self.navigationPresentation = .flatModal
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
//self.automaticallyControlPresentationContextLayout = false
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View {
|
||||
componentView.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View {
|
||||
componentView.animateOut(completion: { [weak self] in
|
||||
completion?()
|
||||
self?.dismiss(animated: false)
|
||||
})
|
||||
} else {
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class StoryContentItem {
|
||||
public let id: AnyHashable
|
||||
public let position: Int
|
||||
public let component: AnyComponent<Empty>
|
||||
public let centerInfoComponent: AnyComponent<Empty>?
|
||||
public let rightInfoComponent: AnyComponent<Empty>?
|
||||
|
||||
public init(
|
||||
id: AnyHashable,
|
||||
position: Int,
|
||||
component: AnyComponent<Empty>,
|
||||
centerInfoComponent: AnyComponent<Empty>?,
|
||||
rightInfoComponent: AnyComponent<Empty>?
|
||||
) {
|
||||
self.id = id
|
||||
self.position = position
|
||||
self.component = component
|
||||
self.centerInfoComponent = centerInfoComponent
|
||||
self.rightInfoComponent = rightInfoComponent
|
||||
}
|
||||
}
|
||||
|
||||
public final class StoryContentItemSlice {
|
||||
public let focusedItemId: AnyHashable
|
||||
public let items: [StoryContentItem]
|
||||
public let totalCount: Int
|
||||
public let update: (StoryContentItemSlice, AnyHashable) -> Signal<StoryContentItemSlice, NoError>
|
||||
|
||||
public init(
|
||||
focusedItemId: AnyHashable,
|
||||
items: [StoryContentItem],
|
||||
totalCount: Int,
|
||||
update: @escaping (StoryContentItemSlice, AnyHashable) -> Signal<StoryContentItemSlice, NoError>
|
||||
) {
|
||||
self.focusedItemId = focusedItemId
|
||||
self.items = items
|
||||
self.totalCount = totalCount
|
||||
self.update = update
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StoryContentComponent",
|
||||
module_name = "StoryContentComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/TelegramUniversalVideoContent",
|
||||
"//submodules/AvatarNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,96 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import TelegramStringFormatting
|
||||
|
||||
final class StoryAuthorInfoComponent: Component {
|
||||
let context: AccountContext
|
||||
let message: EngineMessage
|
||||
|
||||
init(context: AccountContext, message: EngineMessage) {
|
||||
self.context = context
|
||||
self.message = message
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryAuthorInfoComponent, rhs: StoryAuthorInfoComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.message != rhs.message {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
|
||||
private var component: StoryAuthorInfoComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: StoryAuthorInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = availableSize
|
||||
let spacing: CGFloat = 0.0
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
let title = component.message.author?.debugDisplayTitle ?? ""
|
||||
let subtitle = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: component.message.timestamp).string
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: .white)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let subtitleSize = self.subtitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: subtitle, font: Font.regular(12.0), color: UIColor(white: 1.0, alpha: 0.8))),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
let contentHeight: CGFloat = titleSize.height + spacing + subtitleSize.height
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((availableSize.height - contentHeight) * 0.5)), size: titleSize)
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + spacing), size: subtitleSize)
|
||||
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
self.addSubview(subtitleView)
|
||||
}
|
||||
transition.setFrame(view: subtitleView, frame: subtitleFrame)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import AvatarNode
|
||||
|
||||
final class StoryAvatarInfoComponent: Component {
|
||||
let context: AccountContext
|
||||
let peer: EnginePeer
|
||||
|
||||
init(context: AccountContext, peer: EnginePeer) {
|
||||
self.context = context
|
||||
self.peer = peer
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryAvatarInfoComponent, rhs: StoryAvatarInfoComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
|
||||
private var component: StoryAvatarInfoComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: StoryAvatarInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: 36.0, height: 36.0)
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
|
||||
peer: component.peer
|
||||
)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import StoryContainerScreen
|
||||
|
||||
public enum StoryChatContent {
|
||||
public static func messages(
|
||||
context: AccountContext,
|
||||
messageId: EngineMessage.Id
|
||||
) -> Signal<StoryContentItemSlice, NoError> {
|
||||
return context.account.postbox.aroundIdMessageHistoryViewForLocation(
|
||||
.peer(peerId: messageId.peerId, threadId: nil),
|
||||
ignoreMessagesInTimestampRange: nil,
|
||||
count: 10,
|
||||
messageId: messageId,
|
||||
topTaggedMessageIdNamespaces: Set(),
|
||||
tagMask: .photoOrVideo,
|
||||
appendMessagesFromTheSameGroup: false,
|
||||
namespaces: .not(Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal])),
|
||||
orderStatistics: .combinedLocation
|
||||
)
|
||||
|> map { view -> StoryContentItemSlice in
|
||||
var items: [StoryContentItem] = []
|
||||
var totalCount = 0
|
||||
for entry in view.0.entries {
|
||||
if let location = entry.location {
|
||||
totalCount = location.count
|
||||
}
|
||||
items.append(StoryContentItem(
|
||||
id: AnyHashable(entry.message.id),
|
||||
position: entry.location?.index ?? 0,
|
||||
component: AnyComponent(StoryMessageContentComponent(
|
||||
context: context,
|
||||
message: EngineMessage(entry.message)
|
||||
)),
|
||||
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
|
||||
context: context,
|
||||
message: EngineMessage(entry.message)
|
||||
)),
|
||||
rightInfoComponent: entry.message.author.flatMap { author -> AnyComponent<Empty> in
|
||||
return AnyComponent(StoryAvatarInfoComponent(
|
||||
context: context,
|
||||
peer: EnginePeer(author)
|
||||
))
|
||||
}
|
||||
))
|
||||
}
|
||||
return StoryContentItemSlice(
|
||||
focusedItemId: AnyHashable(messageId),
|
||||
items: items,
|
||||
totalCount: totalCount,
|
||||
update: { _, itemId in
|
||||
if let id = itemId.base as? EngineMessage.Id {
|
||||
return StoryChatContent.messages(
|
||||
context: context,
|
||||
messageId: id
|
||||
)
|
||||
} else {
|
||||
return StoryChatContent.messages(
|
||||
context: context,
|
||||
messageId: messageId
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import PhotoResources
|
||||
import SwiftSignalKit
|
||||
import UniversalMediaPlayer
|
||||
import TelegramUniversalVideoContent
|
||||
|
||||
final class StoryMessageContentComponent: Component {
|
||||
let context: AccountContext
|
||||
let message: EngineMessage
|
||||
|
||||
init(context: AccountContext, message: EngineMessage) {
|
||||
self.context = context
|
||||
self.message = message
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryMessageContentComponent, rhs: StoryMessageContentComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.message != rhs.message {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let imageNode: TransformImageNode
|
||||
private var videoNode: UniversalVideoNode?
|
||||
|
||||
private var currentMessageMedia: EngineMedia?
|
||||
private var fetchDisposable: Disposable?
|
||||
|
||||
private var component: StoryMessageContentComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func performActionAfterImageContentLoaded(update: Bool) {
|
||||
guard let component = self.component, let currentMessageMedia = self.currentMessageMedia else {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .file(file) = currentMessageMedia {
|
||||
if self.videoNode == nil {
|
||||
let videoNode = UniversalVideoNode(
|
||||
postbox: component.context.account.postbox,
|
||||
audioSession: component.context.sharedContext.mediaManager.audioSession,
|
||||
manager: component.context.sharedContext.mediaManager.universalVideoManager,
|
||||
decoration: StoryVideoDecoration(),
|
||||
content: NativeVideoContent(
|
||||
id: .message(component.message.stableId, file.fileId),
|
||||
userLocation: .peer(component.message.id.peerId),
|
||||
fileReference: .message(message: MessageReference(component.message._asMessage()), media: file),
|
||||
imageReference: nil,
|
||||
loopVideo: true,
|
||||
enableSound: true,
|
||||
tempFilePath: nil,
|
||||
captureProtected: component.message._asMessage().isCopyProtected(),
|
||||
storeAfterDownload: nil
|
||||
),
|
||||
priority: .gallery
|
||||
)
|
||||
videoNode.ownsContentNodeUpdated = { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
self.videoNode?.play()
|
||||
}
|
||||
}
|
||||
videoNode.canAttachContent = true
|
||||
self.videoNode = videoNode
|
||||
self.addSubnode(videoNode)
|
||||
if update {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StoryMessageContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
var messageMedia: EngineMedia?
|
||||
for media in component.message.media {
|
||||
switch media {
|
||||
case let image as TelegramMediaImage:
|
||||
messageMedia = .image(image)
|
||||
case let file as TelegramMediaFile:
|
||||
messageMedia = .file(file)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var reloadMedia = false
|
||||
if self.currentMessageMedia?.id != messageMedia?.id {
|
||||
self.currentMessageMedia = messageMedia
|
||||
reloadMedia = true
|
||||
}
|
||||
|
||||
if reloadMedia, let messageMedia {
|
||||
var signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
var fetchSignal: Signal<Never, NoError>?
|
||||
switch messageMedia {
|
||||
case let .image(image):
|
||||
signal = chatMessagePhoto(
|
||||
postbox: component.context.account.postbox,
|
||||
userLocation: .peer(component.message.id.peerId),
|
||||
photoReference: .message(message: MessageReference(component.message._asMessage()), media: image),
|
||||
synchronousLoad: true,
|
||||
highQuality: true
|
||||
)
|
||||
if let representation = image.representations.last {
|
||||
fetchSignal = messageMediaImageInteractiveFetched(context: component.context, message: component.message._asMessage(), image: image, resource: representation.resource, userInitiated: true, storeToDownloadsPeerId: component.message.id.peerId)
|
||||
|> ignoreValues
|
||||
}
|
||||
case let .file(file):
|
||||
signal = chatMessageVideo(
|
||||
postbox: component.context.account.postbox,
|
||||
userLocation: .peer(component.message.id.peerId),
|
||||
videoReference: .message(message: MessageReference(component.message._asMessage()), media: file),
|
||||
synchronousLoad: true
|
||||
)
|
||||
fetchSignal = messageMediaFileInteractiveFetched(context: component.context, message: component.message._asMessage(), file: file, userInitiated: true, storeToDownloadsPeerId: component.message.id.peerId)
|
||||
|> ignoreValues
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let signal {
|
||||
var wasSynchronous = true
|
||||
self.imageNode.setSignal(signal |> afterCompleted { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.performActionAfterImageContentLoaded(update: !wasSynchronous)
|
||||
}
|
||||
}, attemptSynchronously: true)
|
||||
wasSynchronous = false
|
||||
}
|
||||
|
||||
self.fetchDisposable?.dispose()
|
||||
self.fetchDisposable = nil
|
||||
if let fetchSignal {
|
||||
self.fetchDisposable = fetchSignal.start()
|
||||
}
|
||||
}
|
||||
|
||||
if let messageMedia {
|
||||
var dimensions: CGSize?
|
||||
switch messageMedia {
|
||||
case let .image(image):
|
||||
dimensions = image.representations.last?.dimensions.cgSize
|
||||
case let .file(file):
|
||||
dimensions = file.dimensions?.cgSize
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let dimensions {
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(
|
||||
corners: ImageCorners(),
|
||||
imageSize: dimensions.aspectFilled(availableSize),
|
||||
boundingSize: availableSize,
|
||||
intrinsicInsets: UIEdgeInsets()
|
||||
))
|
||||
apply()
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
let videoSize = dimensions.aspectFilled(availableSize)
|
||||
videoNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) * 0.5), y: floor((availableSize.height - videoSize.height) * 0.5)), size: videoSize)
|
||||
videoNode.updateLayout(size: videoSize, transition: .immediate)
|
||||
}
|
||||
}
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import UniversalMediaPlayer
|
||||
import AccountContext
|
||||
import PhotoResources
|
||||
|
||||
public final class StoryVideoDecoration: UniversalVideoDecoration {
|
||||
public let backgroundNode: ASDisplayNode? = nil
|
||||
public let contentContainerNode: ASDisplayNode
|
||||
public let foregroundNode: ASDisplayNode? = nil
|
||||
|
||||
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
|
||||
|
||||
private var validLayoutSize: CGSize?
|
||||
|
||||
public init() {
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
}
|
||||
|
||||
public func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) {
|
||||
if self.contentNode !== contentNode {
|
||||
let previous = self.contentNode
|
||||
self.contentNode = contentNode
|
||||
|
||||
if let previous = previous {
|
||||
if previous.supernode === self.contentContainerNode {
|
||||
previous.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let contentNode = contentNode {
|
||||
if contentNode.supernode !== self.contentContainerNode {
|
||||
self.contentContainerNode.addSubnode(contentNode)
|
||||
if let validLayoutSize = self.validLayoutSize {
|
||||
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
|
||||
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateCorners(_ corners: ImageCorners) {
|
||||
self.contentContainerNode.clipsToBounds = true
|
||||
if isRoundEqualCorners(corners) {
|
||||
self.contentContainerNode.cornerRadius = corners.topLeft.radius
|
||||
} else {
|
||||
let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius))
|
||||
let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom)
|
||||
let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())
|
||||
guard let context = DrawingContext(size: size, clear: true) else {
|
||||
return
|
||||
}
|
||||
context.withContext { ctx in
|
||||
ctx.setFillColor(UIColor.black.cgColor)
|
||||
ctx.fill(arguments.drawingRect)
|
||||
}
|
||||
addCorners(context, arguments: arguments)
|
||||
|
||||
if let maskImage = context.generateImage() {
|
||||
let mask = CALayer()
|
||||
mask.contents = maskImage.cgImage
|
||||
mask.contentsScale = maskImage.scale
|
||||
mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height)
|
||||
|
||||
self.contentContainerNode.layer.mask = mask
|
||||
self.contentContainerNode.layer.mask?.frame = self.contentContainerNode.bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateClippingFrame(_ frame: CGRect, completion: (() -> Void)?) {
|
||||
self.contentContainerNode.layer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
})
|
||||
|
||||
if let maskLayer = self.contentContainerNode.layer.mask {
|
||||
maskLayer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
})
|
||||
|
||||
maskLayer.animate(from: NSValue(cgPoint: maskLayer.position), to: NSValue(cgPoint: frame.center), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
})
|
||||
}
|
||||
|
||||
if let contentNode = self.contentNode {
|
||||
contentNode.layer.animate(from: NSValue(cgPoint: contentNode.layer.position), to: NSValue(cgPoint: frame.center), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayoutSize = size
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
if let backgroundNode = self.backgroundNode {
|
||||
transition.updateFrame(node: backgroundNode, frame: bounds)
|
||||
}
|
||||
if let foregroundNode = self.foregroundNode {
|
||||
transition.updateFrame(node: foregroundNode, frame: bounds)
|
||||
}
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
|
||||
if let maskLayer = self.contentContainerNode.layer.mask {
|
||||
transition.updateFrame(layer: maskLayer, frame: bounds)
|
||||
}
|
||||
if let contentNode = self.contentNode {
|
||||
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
contentNode.updateLayout(size: size, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public func setStatus(_ status: Signal<MediaPlayerStatus?, NoError>) {
|
||||
}
|
||||
|
||||
public func tap() {
|
||||
}
|
||||
}
|
12
submodules/TelegramUI/Images.xcassets/Media Gallery/Close.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/Close.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
3
submodules/TelegramUI/Images.xcassets/Media Gallery/Close.imageset/icon.svg
vendored
Normal file
3
submodules/TelegramUI/Images.xcassets/Media Gallery/Close.imageset/icon.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2982 1.18268C13.7177 0.763164 14.3979 0.763164 14.8174 1.18268C15.2369 1.60219 15.2369 2.28236 14.8174 2.70187L9.51928 7.99997L14.8174 13.2981C15.2369 13.7176 15.2369 14.3977 14.8174 14.8173C14.3979 15.2368 13.7177 15.2368 13.2982 14.8173L8.00009 9.51916L2.70199 14.8173C2.28248 15.2368 1.60231 15.2368 1.1828 14.8173C0.763286 14.3977 0.763286 13.7176 1.1828 13.2981L6.4809 7.99997L1.1828 2.70187C0.763287 2.28236 0.763287 1.60219 1.1828 1.18268C1.60231 0.763164 2.28248 0.763164 2.70199 1.18268L8.00009 6.48077L13.2982 1.18268Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 701 B |
@ -22,6 +22,7 @@ import ShareController
|
||||
import UndoUI
|
||||
import WebsiteType
|
||||
import GalleryData
|
||||
import StoryContainerScreen
|
||||
|
||||
func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
||||
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) {
|
||||
@ -172,6 +173,12 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
||||
}
|
||||
params.context.sharedContext.mediaManager.setPlaylist((params.context.account, PeerMessagesMediaPlaylist(context: params.context, location: location, chatLocationContextHolder: params.chatLocationContextHolder)), type: playerType, control: control)
|
||||
return true
|
||||
case let .story(storyController):
|
||||
params.dismissInput()
|
||||
let _ = (storyController
|
||||
|> deliverOnMainQueue).start(next: { storyController in
|
||||
params.navigationController?.pushViewController(storyController)
|
||||
})
|
||||
case let .gallery(gallery):
|
||||
params.dismissInput()
|
||||
let _ = (gallery
|
||||
|
@ -50,6 +50,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
public var disableImageContentAnalysis: Bool
|
||||
public var disableBackgroundAnimation: Bool
|
||||
public var logLanguageRecognition: Bool
|
||||
public var storiesExperiment: Bool
|
||||
|
||||
public static var defaultSettings: ExperimentalUISettings {
|
||||
return ExperimentalUISettings(
|
||||
@ -77,7 +78,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
disableLanguageRecognition: false,
|
||||
disableImageContentAnalysis: false,
|
||||
disableBackgroundAnimation: false,
|
||||
logLanguageRecognition: false
|
||||
logLanguageRecognition: false,
|
||||
storiesExperiment: false
|
||||
)
|
||||
}
|
||||
|
||||
@ -106,7 +108,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
disableLanguageRecognition: Bool,
|
||||
disableImageContentAnalysis: Bool,
|
||||
disableBackgroundAnimation: Bool,
|
||||
logLanguageRecognition: Bool
|
||||
logLanguageRecognition: Bool,
|
||||
storiesExperiment: Bool
|
||||
) {
|
||||
self.keepChatNavigationStack = keepChatNavigationStack
|
||||
self.skipReadHistory = skipReadHistory
|
||||
@ -133,6 +136,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
self.disableImageContentAnalysis = disableImageContentAnalysis
|
||||
self.disableBackgroundAnimation = disableBackgroundAnimation
|
||||
self.logLanguageRecognition = logLanguageRecognition
|
||||
self.storiesExperiment = storiesExperiment
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -163,6 +167,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
self.disableImageContentAnalysis = try container.decodeIfPresent(Bool.self, forKey: "disableImageContentAnalysis") ?? false
|
||||
self.disableBackgroundAnimation = try container.decodeIfPresent(Bool.self, forKey: "disableBackgroundAnimation") ?? false
|
||||
self.logLanguageRecognition = try container.decodeIfPresent(Bool.self, forKey: "logLanguageRecognition") ?? false
|
||||
self.storiesExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesExperiment") ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -193,6 +198,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
try container.encode(self.disableImageContentAnalysis, forKey: "disableImageContentAnalysis")
|
||||
try container.encode(self.disableBackgroundAnimation, forKey: "disableBackgroundAnimation")
|
||||
try container.encode(self.logLanguageRecognition, forKey: "logLanguageRecognition")
|
||||
try container.encode(self.storiesExperiment, forKey: "storiesExperiment")
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user