[WIP] Stories UI

This commit is contained in:
Ali 2023-04-18 18:33:35 +04:00
parent ddd7bd7457
commit 7d6b9c95e7
21 changed files with 1548 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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