diff --git a/Random.txt b/Random.txt index 86eb369131..6eb2db792a 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -4f0d2d13a70664d3029d9b97935089df0426fe53745965d175408752838b80dd \ No newline at end of file +61c0e29ede9b63175583b4609216b9c6083192c87d0e6ee0a42a5ff263b627dc diff --git a/buildbox/build-telegram.sh b/buildbox/build-telegram.sh index f2bc29956c..9a31feed47 100644 --- a/buildbox/build-telegram.sh +++ b/buildbox/build-telegram.sh @@ -4,8 +4,8 @@ set -e BUILD_TELEGRAM_VERSION="1" -MACOS_VERSION="10.15" -XCODE_VERSION="12.4" +MACOS_VERSION="11" +XCODE_VERSION="12.5.1" GUEST_SHELL="bash" VM_BASE_NAME="macos$(echo $MACOS_VERSION | sed -e 's/\.'/_/g)_Xcode$(echo $XCODE_VERSION | sed -e 's/\.'/_/g)" diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index d0919530dd..af7c095c8e 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -130,7 +130,7 @@ public enum ChatControllerInteractionNavigateToPeer { case withBotStartPayload(ChatControllerInitialBotStart) } -public struct ChatTextInputState: PostboxCoding, Equatable { +public struct ChatTextInputState: Codable, Equatable { public let inputText: NSAttributedString public let selectionRange: Range @@ -153,29 +153,40 @@ public struct ChatTextInputState: PostboxCoding, Equatable { let length = inputText.length self.selectionRange = length ..< length } - - public init(decoder: PostboxDecoder) { - self.inputText = ((decoder.decodeObjectForKey("at", decoder: { ChatTextInputStateText(decoder: $0) }) as? ChatTextInputStateText) ?? ChatTextInputStateText()).attributedText() - self.selectionRange = Int(decoder.decodeInt32ForKey("as0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("as1", orElse: 0)) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + self.inputText = ((try? container.decode(ChatTextInputStateText.self, forKey: "at")) ?? ChatTextInputStateText()).attributedText() + let rangeFrom = (try? container.decode(Int32.self, forKey: "as0")) ?? 0 + let rangeTo = (try? container.decode(Int32.self, forKey: "as1")) ?? 0 + if rangeFrom <= rangeTo { + self.selectionRange = Int(rangeFrom) ..< Int(rangeTo) + } else { + let length = self.inputText.length + self.selectionRange = length ..< length + } } - - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeObject(ChatTextInputStateText(attributedText: self.inputText), forKey: "at") - - encoder.encodeInt32(Int32(self.selectionRange.lowerBound), forKey: "as0") - encoder.encodeInt32(Int32(self.selectionRange.upperBound), forKey: "as1") + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(ChatTextInputStateText(attributedText: self.inputText), forKey: "at") + try container.encode(Int32(self.selectionRange.lowerBound), forKey: "as0") + try container.encode(Int32(self.selectionRange.upperBound), forKey: "as1") } } -public enum ChatTextInputStateTextAttributeType: PostboxCoding, Equatable { +public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case bold case italic case monospace case textMention(EnginePeer.Id) case textUrl(String) - - public init(decoder: PostboxDecoder) { - switch decoder.decodeInt32ForKey("t", orElse: 0) { + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + switch (try? container.decode(Int32.self, forKey: "t")) ?? 0 { case 0: self = .bold case 1: @@ -183,69 +194,37 @@ public enum ChatTextInputStateTextAttributeType: PostboxCoding, Equatable { case 2: self = .monospace case 3: - self = .textMention(EnginePeer.Id(decoder.decodeInt64ForKey("peerId", orElse: 0))) + let peerId = (try? container.decode(Int64.self, forKey: "peerId")) ?? 0 + self = .textMention(EnginePeer.Id(peerId)) case 4: - self = .textUrl(decoder.decodeStringForKey("url", orElse: "")) + let url = (try? container.decode(String.self, forKey: "url")) ?? "" + self = .textUrl(url) default: assertionFailure() self = .bold } } - - public func encode(_ encoder: PostboxEncoder) { + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) switch self { case .bold: - encoder.encodeInt32(0, forKey: "t") + try container.encode(0 as Int32, forKey: "t") case .italic: - encoder.encodeInt32(1, forKey: "t") + try container.encode(1 as Int32, forKey: "t") case .monospace: - encoder.encodeInt32(2, forKey: "t") + try container.encode(2 as Int32, forKey: "t") case let .textMention(id): - encoder.encodeInt32(3, forKey: "t") - encoder.encodeInt64(id.toInt64(), forKey: "peerId") + try container.encode(3 as Int32, forKey: "t") + try container.encode(id.toInt64(), forKey: "peerId") case let .textUrl(url): - encoder.encodeInt32(4, forKey: "t") - encoder.encodeString(url, forKey: "url") - } - } - - public static func ==(lhs: ChatTextInputStateTextAttributeType, rhs: ChatTextInputStateTextAttributeType) -> Bool { - switch lhs { - case .bold: - if case .bold = rhs { - return true - } else { - return false - } - case .italic: - if case .italic = rhs { - return true - } else { - return false - } - case .monospace: - if case .monospace = rhs { - return true - } else { - return false - } - case let .textMention(id): - if case .textMention(id) = rhs { - return true - } else { - return false - } - case let .textUrl(url): - if case .textUrl(url) = rhs { - return true - } else { - return false - } + try container.encode(4 as Int32, forKey: "t") + try container.encode(url, forKey: "url") } } } -public struct ChatTextInputStateTextAttribute: PostboxCoding, Equatable { +public struct ChatTextInputStateTextAttribute: Codable, Equatable { public let type: ChatTextInputStateTextAttributeType public let range: Range @@ -253,16 +232,23 @@ public struct ChatTextInputStateTextAttribute: PostboxCoding, Equatable { self.type = type self.range = range } - - public init(decoder: PostboxDecoder) { - self.type = decoder.decodeObjectForKey("type", decoder: { ChatTextInputStateTextAttributeType(decoder: $0) }) as! ChatTextInputStateTextAttributeType - self.range = Int(decoder.decodeInt32ForKey("range0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("range1", orElse: 0)) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + self.type = try container.decode(ChatTextInputStateTextAttributeType.self, forKey: "type") + let rangeFrom = (try? container.decode(Int32.self, forKey: "range0")) ?? 0 + let rangeTo = (try? container.decode(Int32.self, forKey: "range1")) ?? 0 + + self.range = Int(rangeFrom) ..< Int(rangeTo) } - - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeObject(self.type, forKey: "type") - encoder.encodeInt32(Int32(self.range.lowerBound), forKey: "range0") - encoder.encodeInt32(Int32(self.range.upperBound), forKey: "range1") + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.type, forKey: "type") + + try container.encode(Int32(self.range.lowerBound), forKey: "range0") + try container.encode(Int32(self.range.upperBound), forKey: "range1") } public static func ==(lhs: ChatTextInputStateTextAttribute, rhs: ChatTextInputStateTextAttribute) -> Bool { @@ -270,7 +256,7 @@ public struct ChatTextInputStateTextAttribute: PostboxCoding, Equatable { } } -public struct ChatTextInputStateText: PostboxCoding, Equatable { +public struct ChatTextInputStateText: Codable, Equatable { public let text: String public let attributes: [ChatTextInputStateTextAttribute] @@ -304,15 +290,17 @@ public struct ChatTextInputStateText: PostboxCoding, Equatable { }) self.attributes = parsedAttributes } - - public init(decoder: PostboxDecoder) { - self.text = decoder.decodeStringForKey("text", orElse: "") - self.attributes = decoder.decodeObjectArrayWithDecoderForKey("attributes") + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + self.text = (try? container.decode(String.self, forKey: "text")) ?? "" + self.attributes = (try? container.decode([ChatTextInputStateTextAttribute].self, forKey: "attributes")) ?? [] } - - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeString(self.text, forKey: "text") - encoder.encodeObjectArray(self.attributes, forKey: "attributes") + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + try container.encode(self.text, forKey: "text") + try container.encode(self.attributes, forKey: "attributes") } static public func ==(lhs: ChatTextInputStateText, rhs: ChatTextInputStateText) -> Bool { @@ -359,15 +347,18 @@ public final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceStat self.timestamp = timestamp self.text = text } - - public init(decoder: PostboxDecoder) { - self.timestamp = decoder.decodeInt32ForKey("d", orElse: 0) - self.text = ((decoder.decodeObjectForKey("at", decoder: { ChatTextInputStateText(decoder: $0) }) as? ChatTextInputStateText) ?? ChatTextInputStateText()).attributedText() + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + self.timestamp = (try? container.decode(Int32.self, forKey: "d")) ?? 0 + self.text = ((try? container.decode(ChatTextInputStateText.self, forKey: "at")) ?? ChatTextInputStateText()).attributedText() } - - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.timestamp, forKey: "d") - encoder.encodeObject(ChatTextInputStateText(attributedText: self.text), forKey: "at") + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.timestamp, forKey: "d") + try container.encode(ChatTextInputStateText(attributedText: self.text), forKey: "at") } public func isEqual(to: PeerChatListEmbeddedInterfaceState) -> Bool { diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift index 2daae7aa39..9d14791fbc 100644 --- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift @@ -10,7 +10,7 @@ public enum ChatTextInputMediaRecordingButtonMode: Int32 { case video = 1 } -public struct ChatInterfaceSelectionState: PostboxCoding, Equatable { +public struct ChatInterfaceSelectionState: Codable, Equatable { public let selectedIds: Set public static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool { @@ -20,8 +20,28 @@ public struct ChatInterfaceSelectionState: PostboxCoding, Equatable { public init(selectedIds: Set) { self.selectedIds = selectedIds } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + if let data = try? container.decodeIfPresent(Data.self, forKey: "i") { + self.selectedIds = Set(MessageId.decodeArrayFromBuffer(ReadBuffer(data: data))) + } else { + self.selectedIds = Set() + } + } + + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + let buffer = WriteBuffer() + MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer) + + try container.encode(buffer.makeData(), forKey: "i") + } - public init(decoder: PostboxDecoder) { + /*public init(decoder: PostboxDecoder) { if let data = decoder.decodeBytesForKeyNoCopy("i") { self.selectedIds = Set(MessageId.decodeArrayFromBuffer(data)) } else { @@ -33,10 +53,10 @@ public struct ChatInterfaceSelectionState: PostboxCoding, Equatable { let buffer = WriteBuffer() MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer) encoder.encodeBytes(buffer, forKey: "i") - } + }*/ } -public struct ChatEditMessageState: PostboxCoding, Equatable { +public struct ChatEditMessageState: Codable, Equatable { public let messageId: MessageId public let inputState: ChatTextInputState public let disableUrlPreview: String? @@ -48,8 +68,41 @@ public struct ChatEditMessageState: PostboxCoding, Equatable { self.disableUrlPreview = disableUrlPreview self.inputTextMaxLength = inputTextMaxLength } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.messageId = MessageId( + peerId: PeerId((try? container.decode(Int64.self, forKey: "mp")) ?? 0), + namespace: (try? container.decode(Int32.self, forKey: "mn")) ?? 0, + id: (try? container.decode(Int32.self, forKey: "mi")) ?? 0 + ) + + if let inputState = try? container.decode(ChatTextInputState.self, forKey: "is") { + self.inputState = inputState + } else { + self.inputState = ChatTextInputState() + } + + self.disableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") + self.inputTextMaxLength = try? container.decodeIfPresent(Int32.self, forKey: "tl") + } + + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.messageId.peerId.toInt64(), forKey: "mp") + try container.encode(self.messageId.namespace, forKey: "mn") + try container.encode(self.messageId.id, forKey: "mi") + + try container.encode(self.inputState, forKey: "is") + + try container.encodeIfPresent(self.disableUrlPreview, forKey: "dup") + try container.encodeIfPresent(self.inputTextMaxLength, forKey: "tl") + } - public init(decoder: PostboxDecoder) { + /*public init(decoder: PostboxDecoder) { self.messageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("mp", orElse: 0)), namespace: decoder.decodeInt32ForKey("mn", orElse: 0), id: decoder.decodeInt32ForKey("mi", orElse: 0)) if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { self.inputState = inputState @@ -75,7 +128,7 @@ public struct ChatEditMessageState: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "ml") } - } + }*/ public static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool { return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreview == rhs.disableUrlPreview && lhs.inputTextMaxLength == rhs.inputTextMaxLength @@ -322,7 +375,7 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata public init(decoder: PostboxDecoder) { self.timestamp = decoder.decodeInt32ForKey("ts", orElse: 0) - if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { + if let inputState = decoder.decode(ChatTextInputState.self, forKey: "is") { self.composeInputState = inputState } else { self.composeInputState = ChatTextInputState() @@ -345,12 +398,12 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata } else { self.forwardMessageIds = nil } - if let editMessage = decoder.decodeObjectForKey("em", decoder: { ChatEditMessageState(decoder: $0) }) as? ChatEditMessageState { + if let editMessage = decoder.decode(ChatEditMessageState.self, forKey: "em") { self.editMessage = editMessage } else { self.editMessage = nil } - if let selectionState = decoder.decodeObjectForKey("ss", decoder: { return ChatInterfaceSelectionState(decoder: $0) }) as? ChatInterfaceSelectionState { + if let selectionState = decoder.decode(ChatInterfaceSelectionState.self, forKey: "ss") { self.selectionState = selectionState } else { self.selectionState = nil @@ -372,7 +425,7 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.timestamp, forKey: "ts") - encoder.encodeObject(self.composeInputState, forKey: "is") + encoder.encode(self.composeInputState, forKey: "is") if let composeDisableUrlPreview = self.composeDisableUrlPreview { encoder.encodeString(composeDisableUrlPreview, forKey: "dup") } else { @@ -395,12 +448,12 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata encoder.encodeNil(forKey: "fm") } if let editMessage = self.editMessage { - encoder.encodeObject(editMessage, forKey: "em") + encoder.encode(editMessage, forKey: "em") } else { encoder.encodeNil(forKey: "em") } if let selectionState = self.selectionState { - encoder.encodeObject(selectionState, forKey: "ss") + encoder.encode(selectionState, forKey: "ss") } else { encoder.encodeNil(forKey: "ss") } diff --git a/submodules/ComponentFlow/BUILD b/submodules/ComponentFlow/BUILD new file mode 100644 index 0000000000..d69165e200 --- /dev/null +++ b/submodules/ComponentFlow/BUILD @@ -0,0 +1,14 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ComponentFlow", + module_name = "ComponentFlow", + srcs = glob([ + "Source/**/*.swift", + ]), + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift new file mode 100644 index 0000000000..881c14d844 --- /dev/null +++ b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift @@ -0,0 +1,78 @@ +import Foundation +import UIKit + +public extension Transition.Appear { + static func `default`(scale: Bool = false, alpha: Bool = false) -> Transition.Appear { + return Transition.Appear { component, view, transition in + if scale { + transition.animateScale(view: view, from: 0.01, to: 1.0) + } + if alpha { + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + } + } + } + + static func scaleIn() -> Transition.Appear { + return Transition.Appear { component, view, transition in + transition.animateScale(view: view, from: 0.01, to: 1.0) + } + } +} + +public extension Transition.AppearWithGuide { + static func `default`(scale: Bool = false, alpha: Bool = false) -> Transition.AppearWithGuide { + return Transition.AppearWithGuide { component, view, guide, transition in + if scale { + transition.animateScale(view: view, from: 0.01, to: 1.0) + } + if alpha { + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + } + transition.animatePosition(view: view, from: CGPoint(x: guide.x - view.center.x, y: guide.y - view.center.y), to: CGPoint(), additive: true) + } + } +} + +public extension Transition.Disappear { + static let `default` = Transition.Disappear { view, transition, completion in + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + } +} + +public extension Transition.DisappearWithGuide { + static func `default`(alpha: Bool = true) -> Transition.DisappearWithGuide { + return Transition.DisappearWithGuide { stage, view, guide, transition, completion in + switch stage { + case .begin: + if alpha { + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + } + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: guide.x - view.bounds.width / 2.0, y: guide.y - view.bounds.height / 2.0), size: view.bounds.size), completion: { _ in + if !alpha { + completion() + } + }) + case .update: + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: guide.x - view.bounds.width / 2.0, y: guide.y - view.bounds.height / 2.0), size: view.bounds.size)) + } + } + } +} + +public extension Transition.Update { + static let `default` = Transition.Update { component, view, transition in + let frame = component.size.centered(around: component._position ?? CGPoint()) + if view.frame != frame { + transition.setFrame(view: view, frame: frame) + } + let opacity = component._opacity ?? 1.0 + if view.alpha != opacity { + transition.setAlpha(view: view, alpha: opacity) + } + } +} diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift new file mode 100644 index 0000000000..ead71c9b1f --- /dev/null +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -0,0 +1,798 @@ +import Foundation +import UIKit + +private func updateChildAnyComponent( + id: _AnyChildComponent.Id, + component: AnyComponent, + view: UIView, + availableSize: CGSize, + transition: Transition +) -> _UpdatedChildComponent { + let parentContext = _AnyCombinedComponentContext.current + + if !parentContext.updateContext.updatedViews.insert(id).inserted { + preconditionFailure("Child component can only be processed once") + } + + let context = view.context(component: component) + var isEnvironmentUpdated = false + var isStateUpdated = false + var isComponentUpdated = false + var availableSizeUpdated = false + + if context.environment.calculateIsUpdated() { + context.environment._isUpdated = false + isEnvironmentUpdated = true + } + + if context.erasedState.isUpdated { + context.erasedState.isUpdated = false + isStateUpdated = true + } + + if context.erasedComponent != component { + isComponentUpdated = true + } + context.erasedComponent = component + + if context.layoutResult.availableSize != availableSize { + context.layoutResult.availableSize = availableSize + availableSizeUpdated = true + } + + let isUpdated = isEnvironmentUpdated || isStateUpdated || isComponentUpdated || availableSizeUpdated + + if !isUpdated, let size = context.layoutResult.size { + return _UpdatedChildComponent( + id: id, + component: component, + view: view, + context: context, + size: size + ) + } else { + let size = component._update( + view: view, + availableSize: availableSize, + transition: transition + ) + context.layoutResult.size = size + + return _UpdatedChildComponent( + id: id, + component: component, + view: view, + context: context, + size: size + ) + } +} + +public class _AnyChildComponent { + fileprivate enum Id: Hashable { + case direct(Int) + case mapped(Int, AnyHashable) + } + + fileprivate var directId: Int { + return Int(bitPattern: Unmanaged.passUnretained(self).toOpaque()) + } +} + +public final class _ConcreteChildComponent: _AnyChildComponent { + fileprivate var id: Id { + return .direct(self.directId) + } + + public func update(component: ComponentType, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + let parentContext = _AnyCombinedComponentContext.current + if !parentContext.updateContext.configuredViews.insert(self.id).inserted { + preconditionFailure("Child component can only be configured once") + } + + var transition = transition + + let view: ComponentType.View + if let current = parentContext.childViews[self.id] { + // TODO: Check if the type is the same + view = current.view as! ComponentType.View + } else { + view = component.makeView() + transition = .immediate + } + + let context = view.context(component: component) + EnvironmentBuilder._environment = context.erasedEnvironment + let _ = environment() + EnvironmentBuilder._environment = nil + + return updateChildAnyComponent( + id: self.id, + component: AnyComponent(component), + view: view, + availableSize: availableSize, + transition: transition + ) + } +} + +public extension _ConcreteChildComponent where ComponentType.EnvironmentType == Empty { + func update(component: ComponentType, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition) + } +} + +public final class _UpdatedChildComponentGuide { + fileprivate let instance: _ChildComponentGuide + + fileprivate init(instance: _ChildComponentGuide) { + self.instance = instance + } +} + +public final class _ChildComponentGuide { + fileprivate var directId: Int { + return Int(bitPattern: Unmanaged.passUnretained(self).toOpaque()) + } + + fileprivate var id: _AnyChildComponent.Id { + return .direct(self.directId) + } + + public func update(position: CGPoint, transition: Transition) -> _UpdatedChildComponentGuide { + let parentContext = _AnyCombinedComponentContext.current + + let previousPosition = parentContext.guides[self.id] + + if parentContext.updateContext.configuredGuides.updateValue(_AnyCombinedComponentContext.UpdateContext.ConfiguredGuide(previousPosition: previousPosition ?? position, position: position), forKey: self.id) != nil { + preconditionFailure("Child guide can only be configured once") + } + + for disappearingView in parentContext.disappearingChildViews { + if disappearingView.guideId == self.id { + disappearingView.transitionWithGuide?( + stage: .update, + view: disappearingView.view, + guide: position, + transition: transition, + completion: disappearingView.completion + ) + } + } + + return _UpdatedChildComponentGuide(instance: self) + } +} + +public final class _UpdatedChildComponent { + fileprivate let id: _AnyChildComponent.Id + fileprivate let component: _TypeErasedComponent + fileprivate let view: UIView + fileprivate let context: _TypeErasedComponentContext + + public let size: CGSize + + var _removed: Bool = false + var _position: CGPoint? + var _opacity: CGFloat? + + fileprivate var transitionAppear: Transition.Appear? + fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)? + fileprivate var transitionDisappear: Transition.Disappear? + fileprivate var transitionDisappearWithGuide: (Transition.DisappearWithGuide, _AnyChildComponent.Id)? + fileprivate var transitionUpdate: Transition.Update? + fileprivate var gestures: [Gesture] = [] + + fileprivate init( + id: _AnyChildComponent.Id, + component: _TypeErasedComponent, + view: UIView, + context: _TypeErasedComponentContext, + size: CGSize + ) { + self.id = id + self.component = component + self.view = view + self.context = context + self.size = size + } + + @discardableResult public func appear(_ transition: Transition.Appear) -> _UpdatedChildComponent { + self.transitionAppear = transition + self.transitionAppearWithGuide = nil + return self + } + + @discardableResult public func appear(_ transition: Transition.AppearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent { + self.transitionAppear = nil + self.transitionAppearWithGuide = (transition, guide.instance.id) + return self + } + + @discardableResult public func disappear(_ transition: Transition.Disappear) -> _UpdatedChildComponent { + self.transitionDisappear = transition + self.transitionDisappearWithGuide = nil + return self + } + + @discardableResult public func disappear(_ transition: Transition.DisappearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent { + self.transitionDisappear = nil + self.transitionDisappearWithGuide = (transition, guide.instance.id) + return self + } + + @discardableResult public func update(_ transition: Transition.Update) -> _UpdatedChildComponent { + self.transitionUpdate = transition + return self + } + + @discardableResult public func removed(_ removed: Bool) -> _UpdatedChildComponent { + self._removed = removed + return self + } + + @discardableResult public func position(_ position: CGPoint) -> _UpdatedChildComponent { + self._position = position + return self + } + + @discardableResult public func opacity(_ opacity: CGFloat) -> _UpdatedChildComponent { + self._opacity = opacity + return self + } + + @discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent { + self.gestures.append(gesture) + return self + } +} + +public final class _EnvironmentChildComponent: _AnyChildComponent { + fileprivate var id: Id { + return .direct(self.directId) + } + + func update(component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + let parentContext = _AnyCombinedComponentContext.current + if !parentContext.updateContext.configuredViews.insert(self.id).inserted { + preconditionFailure("Child component can only be configured once") + } + + var transition = transition + + let view: UIView + if let current = parentContext.childViews[self.id] { + // Check if the type is the same + view = current.view + } else { + view = component._makeView() + transition = .immediate + } + + EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment + let _ = environment() + EnvironmentBuilder._environment = nil + + return updateChildAnyComponent( + id: self.id, + component: component, + view: view, + availableSize: availableSize, + transition: transition + ) + } +} + +public extension _EnvironmentChildComponent where EnvironmentType == Empty { + func update(component: AnyComponent, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition) + } +} + +public extension _EnvironmentChildComponent { + func update(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType { + return self.update(component: AnyComponent(component), environment: environment, availableSize: availableSize, transition: transition) + } + + func update(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType, EnvironmentType == Empty { + return self.update(component: AnyComponent(component), environment: {}, availableSize: availableSize, transition: transition) + } +} + +public final class _EnvironmentChildComponentFromMap: _AnyChildComponent { + private let id: Id + + fileprivate init(id: Id) { + self.id = id + } + + public func update(component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + let parentContext = _AnyCombinedComponentContext.current + if !parentContext.updateContext.configuredViews.insert(self.id).inserted { + preconditionFailure("Child component can only be configured once") + } + + var transition = transition + + let view: UIView + if let current = parentContext.childViews[self.id] { + // Check if the type is the same + view = current.view + } else { + view = component._makeView() + transition = .immediate + } + + EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment + let _ = environment() + EnvironmentBuilder._environment = nil + + return updateChildAnyComponent( + id: self.id, + component: component, + view: view, + availableSize: availableSize, + transition: transition + ) + } +} + +public extension _EnvironmentChildComponentFromMap where EnvironmentType == Empty { + func update(component: AnyComponent, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent { + return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition) + } +} + +public final class _EnvironmentChildComponentMap { + private var directId: Int { + return Int(bitPattern: Unmanaged.passUnretained(self).toOpaque()) + } + + public subscript(_ key: Key) -> _EnvironmentChildComponentFromMap { + get { + return _EnvironmentChildComponentFromMap(id: .mapped(self.directId, key)) + } + } +} + +public final class CombinedComponentContext { + fileprivate let escapeGuard = EscapeGuard() + + private let context: ComponentContext + public let view: UIView + + public let component: ComponentType + public let availableSize: CGSize + public let transition: Transition + private let addImpl: (_ updatedComponent: _UpdatedChildComponent) -> Void + + public var environment: Environment { + return self.context.environment + } + public var state: ComponentType.State { + return self.context.state + } + + fileprivate init( + context: ComponentContext, + view: UIView, + component: ComponentType, + availableSize: CGSize, + transition: Transition, + add: @escaping (_ updatedComponent: _UpdatedChildComponent) -> Void + ) { + self.context = context + self.view = view + self.component = component + self.availableSize = availableSize + self.transition = transition + self.addImpl = add + } + + public func add(_ updatedComponent: _UpdatedChildComponent) { + self.addImpl(updatedComponent) + } +} + +public protocol CombinedComponent: Component { + typealias Body = (CombinedComponentContext) -> CGSize + + static var body: Body { get } +} + +private class _AnyCombinedComponentContext { + class UpdateContext { + struct ConfiguredGuide { + var previousPosition: CGPoint + var position: CGPoint + } + + var configuredViews: Set<_AnyChildComponent.Id> = Set() + var updatedViews: Set<_AnyChildComponent.Id> = Set() + var configuredGuides: [_AnyChildComponent.Id: ConfiguredGuide] = [:] + } + + private static var _current: _AnyCombinedComponentContext? + static var current: _AnyCombinedComponentContext { + return self._current! + } + + static func push(_ context: _AnyCombinedComponentContext) -> _AnyCombinedComponentContext? { + let previous = self._current + + precondition(context._updateContext == nil) + context._updateContext = UpdateContext() + self._current = context + + return previous + } + + static func pop(_ context: _AnyCombinedComponentContext, stack: _AnyCombinedComponentContext?) { + precondition(context._updateContext != nil) + context._updateContext = nil + + self._current = stack + } + + class ChildView { + let view: UIView + var index: Int + var transition: Transition.Disappear? + var transitionWithGuide: (Transition.DisappearWithGuide, _AnyChildComponent.Id)? + + var gestures: [UInt: UIGestureRecognizer] = [:] + + init(view: UIView, index: Int) { + self.view = view + self.index = index + } + + func updateGestures(_ gestures: [Gesture]) { + var validIds: [UInt] = [] + for gesture in gestures { + validIds.append(gesture.id.id) + if let current = self.gestures[gesture.id.id] { + gesture.update(gesture: current) + } else { + let gestureInstance = gesture.create() + self.gestures[gesture.id.id] = gestureInstance + self.view.addGestureRecognizer(gestureInstance) + } + } + var removeIds: [UInt] = [] + for id in self.gestures.keys { + if !validIds.contains(id) { + removeIds.append(id) + } + } + for id in removeIds { + if let gestureInstance = self.gestures.removeValue(forKey: id) { + self.view.removeGestureRecognizer(gestureInstance) + } + } + } + } + + class DisappearingChildView { + let view: UIView + let guideId: _AnyChildComponent.Id? + let transition: Transition.Disappear? + let transitionWithGuide: Transition.DisappearWithGuide? + let completion: () -> Void + + init( + view: UIView, + guideId: _AnyChildComponent.Id?, + transition: Transition.Disappear?, + transitionWithGuide: Transition.DisappearWithGuide?, + completion: @escaping () -> Void + ) { + self.view = view + self.guideId = guideId + self.transition = transition + self.transitionWithGuide = transitionWithGuide + self.completion = completion + } + } + + var childViews: [_AnyChildComponent.Id: ChildView] = [:] + var childViewIndices: [_AnyChildComponent.Id] = [] + var guides: [_AnyChildComponent.Id: CGPoint] = [:] + var disappearingChildViews: [DisappearingChildView] = [] + + private var _updateContext: UpdateContext? + var updateContext: UpdateContext { + return self._updateContext! + } +} + +private final class _CombinedComponentContext: _AnyCombinedComponentContext { + var body: ComponentType.Body? +} + +private var UIView_CombinedComponentContextKey: Int? + +private extension UIView { + func getCombinedComponentContext(_ type: ComponentType.Type) -> _CombinedComponentContext { + if let context = objc_getAssociatedObject(self, &UIView_CombinedComponentContextKey) as? _CombinedComponentContext { + return context + } else { + let context = _CombinedComponentContext() + objc_setAssociatedObject(self, &UIView_CombinedComponentContextKey, context, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return context + } + } +} + +public extension Transition { + final class Appear { + private let f: (_UpdatedChildComponent, UIView, Transition) -> Void + + public init(_ f: @escaping (_UpdatedChildComponent, UIView, Transition) -> Void) { + self.f = f + } + + public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: Transition) { + self.f(component, view, transition) + } + } + + final class AppearWithGuide { + private let f: (_UpdatedChildComponent, UIView, CGPoint, Transition) -> Void + + public init(_ f: @escaping (_UpdatedChildComponent, UIView, CGPoint, Transition) -> Void) { + self.f = f + } + + public func callAsFunction(component: _UpdatedChildComponent, view: UIView, guide: CGPoint, transition: Transition) { + self.f(component, view, guide, transition) + } + } + + final class Disappear { + private let f: (UIView, Transition, @escaping () -> Void) -> Void + + public init(_ f: @escaping (UIView, Transition, @escaping () -> Void) -> Void) { + self.f = f + } + + public func callAsFunction(view: UIView, transition: Transition, completion: @escaping () -> Void) { + self.f(view, transition, completion) + } + } + + final class DisappearWithGuide { + public enum Stage { + case begin + case update + } + + private let f: (Stage, UIView, CGPoint, Transition, @escaping () -> Void) -> Void + + public init(_ f: @escaping (Stage, UIView, CGPoint, Transition, @escaping () -> Void) -> Void + ) { + self.f = f + } + + public func callAsFunction(stage: Stage, view: UIView, guide: CGPoint, transition: Transition, completion: @escaping () -> Void) { + self.f(stage, view, guide, transition, completion) + } + } + + final class Update { + private let f: (_UpdatedChildComponent, UIView, Transition) -> Void + + public init(_ f: @escaping (_UpdatedChildComponent, UIView, Transition) -> Void) { + self.f = f + } + + public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: Transition) { + self.f(component, view, transition) + } + } +} + +public extension CombinedComponent { + func makeView() -> UIView { + return UIView() + } + + func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize { + let context = view.getCombinedComponentContext(Self.self) + + let storedBody: Body + if let current = context.body { + storedBody = current + } else { + storedBody = Self.body + context.body = storedBody + } + + let viewContext = view.context(component: self) + + var nextChildIndex = 0 + var addedChildIds = Set<_AnyChildComponent.Id>() + + let contextStack = _AnyCombinedComponentContext.push(context) + + let escapeStatus: EscapeGuard.Status + let size: CGSize + do { + let bodyContext = CombinedComponentContext( + context: viewContext, + view: view, + component: self, + availableSize: availableSize, + transition: transition, + add: { updatedChild in + if !addedChildIds.insert(updatedChild.id).inserted { + preconditionFailure("Child component can only be added once") + } + + let index = nextChildIndex + nextChildIndex += 1 + + if let previousView = context.childViews[updatedChild.id] { + precondition(updatedChild.view === previousView.view) + + if index != previousView.index { + assert(index < previousView.index) + for i in index ..< previousView.index { + if let moveView = context.childViews[context.childViewIndices[i]] { + moveView.index += 1 + } + } + context.childViewIndices.remove(at: previousView.index) + context.childViewIndices.insert(updatedChild.id, at: index) + previousView.index = index + view.insertSubview(previousView.view, at: index) + } + + previousView.updateGestures(updatedChild.gestures) + previousView.transition = updatedChild.transitionDisappear + previousView.transitionWithGuide = updatedChild.transitionDisappearWithGuide + + (updatedChild.transitionUpdate ?? Transition.Update.default)(component: updatedChild, view: updatedChild.view, transition: transition) + } else { + for i in index ..< context.childViewIndices.count { + if let moveView = context.childViews[context.childViewIndices[i]] { + moveView.index += 1 + } + } + + context.childViewIndices.insert(updatedChild.id, at: index) + let childView = _AnyCombinedComponentContext.ChildView(view: updatedChild.view, index: index) + context.childViews[updatedChild.id] = childView + + childView.updateGestures(updatedChild.gestures) + childView.transition = updatedChild.transitionDisappear + childView.transitionWithGuide = updatedChild.transitionDisappearWithGuide + + view.insertSubview(updatedChild.view, at: index) + + updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint()) + updatedChild.view.alpha = updatedChild._opacity ?? 1.0 + updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in + guard let viewContext = viewContext else { + return + } + viewContext.state.updated(transition: transition) + } + + if let transitionAppearWithGuide = updatedChild.transitionAppearWithGuide { + guard let guide = context.updateContext.configuredGuides[transitionAppearWithGuide.1] else { + preconditionFailure("Guide should be configured before using") + } + transitionAppearWithGuide.0( + component: updatedChild, + view: updatedChild.view, + guide: guide.previousPosition, + transition: transition + ) + } else if let transitionAppear = updatedChild.transitionAppear { + transitionAppear( + component: updatedChild, + view: updatedChild.view, + transition: transition + ) + } + } + } + ) + + escapeStatus = bodyContext.escapeGuard.status + size = storedBody(bodyContext) + } + + assert(escapeStatus.isDeallocated, "Body context should not be stored for later use") + + if nextChildIndex < context.childViewIndices.count { + for i in nextChildIndex ..< context.childViewIndices.count { + let id = context.childViewIndices[i] + if let childView = context.childViews.removeValue(forKey: id) { + let view = childView.view + let completion: () -> Void = { [weak context, weak view] in + view?.removeFromSuperview() + + if let context = context { + for i in 0 ..< context.disappearingChildViews.count { + if context.disappearingChildViews[i].view === view { + context.disappearingChildViews.remove(at: i) + break + } + } + } + } + if let transitionWithGuide = childView.transitionWithGuide { + guard let guide = context.updateContext.configuredGuides[transitionWithGuide.1] else { + preconditionFailure("Guide should be configured before using") + } + context.disappearingChildViews.append(_AnyCombinedComponentContext.DisappearingChildView( + view: view, + guideId: transitionWithGuide.1, + transition: nil, + transitionWithGuide: transitionWithGuide.0, + completion: completion + )) + view.isUserInteractionEnabled = false + transitionWithGuide.0( + stage: .begin, + view: view, + guide: guide.position, + transition: transition, + completion: completion + ) + } else if let simpleTransition = childView.transition { + context.disappearingChildViews.append(_AnyCombinedComponentContext.DisappearingChildView( + view: view, + guideId: nil, + transition: simpleTransition, + transitionWithGuide: nil, + completion: completion + )) + view.isUserInteractionEnabled = false + simpleTransition( + view: view, + transition: transition, + completion: completion + ) + } else { + childView.view.removeFromSuperview() + } + } + } + context.childViewIndices.removeSubrange(nextChildIndex...) + } + + if addedChildIds != context.updateContext.updatedViews { + preconditionFailure("Updated and added child lists do not match") + } + + context.guides.removeAll() + for (id, guide) in context.updateContext.configuredGuides { + context.guides[id] = guide.position + } + + _AnyCombinedComponentContext.pop(context, stack: contextStack) + + return size + } +} + +public extension CombinedComponent { + static func Child(environment: Environment.Type) -> _EnvironmentChildComponent { + return _EnvironmentChildComponent() + } + + static func ChildMap(environment: Environment.Type, keyedBy keyType: Key.Type) -> _EnvironmentChildComponentMap { + return _EnvironmentChildComponentMap() + } + + static func Child(_ type: ComponentType.Type) -> _ConcreteChildComponent { + return _ConcreteChildComponent() + } + + static func Guide() -> _ChildComponentGuide { + return _ChildComponentGuide() + } +} diff --git a/submodules/ComponentFlow/Source/Base/Component.swift b/submodules/ComponentFlow/Source/Base/Component.swift new file mode 100644 index 0000000000..7e150609c7 --- /dev/null +++ b/submodules/ComponentFlow/Source/Base/Component.swift @@ -0,0 +1,203 @@ +import Foundation +import UIKit +import ObjectiveC + +public class ComponentLayoutResult { + var availableSize: CGSize? + var size: CGSize? +} + +public protocol _TypeErasedComponentContext: AnyObject { + var erasedEnvironment: _Environment { get } + var erasedState: ComponentState { get } + + var layoutResult: ComponentLayoutResult { get } +} + +class AnyComponentContext: _TypeErasedComponentContext { + var erasedComponent: AnyComponent { + get { + preconditionFailure() + } set(value) { + preconditionFailure() + } + } + var erasedState: ComponentState { + preconditionFailure() + } + var erasedEnvironment: _Environment { + return self.environment + } + + let layoutResult: ComponentLayoutResult + var environment: Environment + + init(environment: Environment) { + self.layoutResult = ComponentLayoutResult() + self.environment = environment + } +} + +class ComponentContext: AnyComponentContext { + override var erasedComponent: AnyComponent { + get { + return AnyComponent(self.component) + } set(value) { + self.component = value.wrapped as! ComponentType + } + } + + var component: ComponentType + let state: ComponentType.State + + override var erasedState: ComponentState { + return self.state + } + + init(component: ComponentType, environment: Environment, state: ComponentType.State) { + self.component = component + self.state = state + + super.init(environment: environment) + } +} + +private var UIView_TypeErasedComponentContextKey: Int? + +extension UIView { + func context(component: AnyComponent) -> AnyComponentContext { + return self.context(typeErasedComponent: component) as! AnyComponentContext + } + + func context(component: ComponentType) -> ComponentContext { + return self.context(typeErasedComponent: component) as! ComponentContext + } + + func context(typeErasedComponent component: _TypeErasedComponent) -> _TypeErasedComponentContext{ + if let context = objc_getAssociatedObject(self, &UIView_TypeErasedComponentContextKey) as? _TypeErasedComponentContext { + return context + } else { + let context = component._makeContext() + objc_setAssociatedObject(self, &UIView_TypeErasedComponentContextKey, context, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return context + } + } +} + +public class ComponentState { + var _updated: ((Transition) -> Void)? + var isUpdated: Bool = false + + public final func updated(transition: Transition = .immediate) { + self.isUpdated = true + self._updated?(transition) + } +} + +public final class EmptyComponentState: ComponentState { +} + +public protocol _TypeErasedComponent { + func _makeView() -> UIView + func _makeContext() -> _TypeErasedComponentContext + func _update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize + func _isEqual(to other: _TypeErasedComponent) -> Bool +} + +public protocol Component: _TypeErasedComponent, Equatable { + associatedtype EnvironmentType = Empty + associatedtype View: UIView = UIView + associatedtype State: ComponentState = EmptyComponentState + + func makeView() -> View + func makeState() -> State + func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize +} + +public extension Component { + func _makeView() -> UIView { + return self.makeView() + } + + func _makeContext() -> _TypeErasedComponentContext { + return ComponentContext(component: self, environment: Environment(), state: self.makeState()) + } + + func _update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize { + return self.update(view: view as! Self.View, availableSize: availableSize, transition: transition) + } + + func _isEqual(to other: _TypeErasedComponent) -> Bool { + if let other = other as? Self { + return self == other + } else { + return false + } + } +} + +public extension Component where Self.View == UIView { + func makeView() -> UIView { + return UIView() + } +} + +public extension Component where Self.State == EmptyComponentState { + func makeState() -> State { + return EmptyComponentState() + } +} + +public class ComponentGesture { + public static func tap(action: @escaping() -> Void) -> ComponentGesture { + preconditionFailure() + } +} + +public class AnyComponent: _TypeErasedComponent, Equatable { + fileprivate let wrapped: _TypeErasedComponent + + public init(_ component: ComponentType) where ComponentType.EnvironmentType == EnvironmentType { + self.wrapped = component + } + + public static func ==(lhs: AnyComponent, rhs: AnyComponent) -> Bool { + return lhs.wrapped._isEqual(to: rhs.wrapped) + } + + public func _makeView() -> UIView { + return self.wrapped._makeView() + } + + public func _makeContext() -> _TypeErasedComponentContext { + return self.wrapped._makeContext() + } + + public func _update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize { + return self.wrapped._update(view: view, availableSize: availableSize, transition: transition) + } + + public func _isEqual(to other: _TypeErasedComponent) -> Bool { + return self.wrapped._isEqual(to: other) + } +} + +public final class AnyComponentWithIdentity: Equatable { + public let id: AnyHashable + public let component: AnyComponent + + public init(id: IdType, component: AnyComponent) { + self.id = AnyHashable(id) + self.component = component + } + + public static func == (lhs: AnyComponentWithIdentity, rhs: AnyComponentWithIdentity) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.component != rhs.component { + return false + } + return true + } +} diff --git a/submodules/ComponentFlow/Source/Base/Environment.swift b/submodules/ComponentFlow/Source/Base/Environment.swift new file mode 100644 index 0000000000..d3582772ae --- /dev/null +++ b/submodules/ComponentFlow/Source/Base/Environment.swift @@ -0,0 +1,219 @@ +import Foundation +import UIKit + +public final class Empty: Equatable { + static let shared: Empty = Empty() + + public static func ==(lhs: Empty, rhs: Empty) -> Bool { + return true + } +} + +public class _Environment { + fileprivate var data: [Int: _EnvironmentValue] = [:] + var _isUpdated: Bool = false + + func calculateIsUpdated() -> Bool { + if self._isUpdated { + return true + } + for (_, item) in self.data { + if let parentEnvironment = item.parentEnvironment, parentEnvironment.calculateIsUpdated() { + return true + } + } + return false + } + + fileprivate func set(index: Int, value: EnvironmentValue) { + if let current = self.data[index] { + self.data[index] = value + if current as! EnvironmentValue != value { + self._isUpdated = true + } + } else { + self.data[index] = value + self._isUpdated = true + } + } +} + +private enum EnvironmentValueStorage { + case direct(T) + case reference(_Environment, Int) +} + +public class _EnvironmentValue { + fileprivate let parentEnvironment: _Environment? + + fileprivate init(parentEnvironment: _Environment?) { + self.parentEnvironment = parentEnvironment + } +} + +@dynamicMemberLookup +public final class EnvironmentValue: _EnvironmentValue, Equatable { + private var storage: EnvironmentValueStorage + + fileprivate var value: T { + switch self.storage { + case let .direct(value): + return value + case let .reference(environment, index): + return (environment.data[index] as! EnvironmentValue).value + } + } + + fileprivate init(_ value: T) { + self.storage = .direct(value) + + super.init(parentEnvironment: nil) + } + + fileprivate init(environment: _Environment, index: Int) { + self.storage = .reference(environment, index) + + super.init(parentEnvironment: environment) + } + + public static func ==(lhs: EnvironmentValue, rhs: EnvironmentValue) -> Bool { + if lhs === rhs { + return true + } + // TODO: follow the reference chain for faster equality checking + return lhs.value == rhs.value + } + + public subscript(dynamicMember keyPath: KeyPath) -> V { + return self.value[keyPath: keyPath] + } +} + +public class Environment: _Environment { + private let file: StaticString + private let line: Int + + public init(_ file: StaticString = #file, _ line: Int = #line) { + self.file = file + self.line = line + } +} + +public extension Environment where T == Empty { + static let value: Environment = { + let result = Environment() + result.set(index: 0, value: EnvironmentValue(Empty())) + return result + }() +} + +public extension Environment { + subscript(_ t1: T.Type) -> EnvironmentValue where T: Equatable { + return EnvironmentValue(environment: self, index: 0) + } + + subscript(_ t1: T1.Type) -> EnvironmentValue where T == (T1, T2), T1: Equatable, T2: Equatable { + return EnvironmentValue(environment: self, index: 0) + } + + subscript(_ t2: T2.Type) -> EnvironmentValue where T == (T1, T2), T1: Equatable, T2: Equatable { + return EnvironmentValue(environment: self, index: 1) + } + + subscript(_ t1: T1.Type) -> EnvironmentValue where T == (T1, T2, T3), T1: Equatable, T2: Equatable, T3: Equatable { + return EnvironmentValue(environment: self, index: 0) + } + + subscript(_ t2: T2.Type) -> EnvironmentValue where T == (T1, T2, T3), T1: Equatable, T2: Equatable, T3: Equatable { + return EnvironmentValue(environment: self, index: 1) + } + + subscript(_ t3: T3.Type) -> EnvironmentValue where T == (T1, T2, T3), T1: Equatable, T2: Equatable, T3: Equatable { + return EnvironmentValue(environment: self, index: 2) + } + + subscript(_ t1: T1.Type) -> EnvironmentValue where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable { + return EnvironmentValue(environment: self, index: 0) + } + + subscript(_ t2: T2.Type) -> EnvironmentValue where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable { + return EnvironmentValue(environment: self, index: 1) + } + + subscript(_ t3: T3.Type) -> EnvironmentValue where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable { + return EnvironmentValue(environment: self, index: 2) + } + + subscript(_ t4: T4.Type) -> EnvironmentValue where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable { + return EnvironmentValue(environment: self, index: 3) + } +} + +@resultBuilder +public struct EnvironmentBuilder { + static var _environment: _Environment? + private static func current(_ type: T.Type) -> Environment { + return self._environment as! Environment + } + + public struct Partial { + fileprivate var value: EnvironmentValue + } + + public static func buildBlock() -> Environment { + let result = self.current(Empty.self) + result.set(index: 0, value: EnvironmentValue(Empty.shared)) + return result + } + + public static func buildExpression(_ expression: T) -> Partial { + return Partial(value: EnvironmentValue(expression)) + } + + public static func buildExpression(_ expression: EnvironmentValue) -> Partial { + return Partial(value: expression) + } + + public static func buildBlock(_ t1: Partial) -> Environment { + let result = self.current(T1.self) + result.set(index: 0, value: t1.value) + return result + } + + public static func buildBlock(_ t1: Partial, _ t2: Partial) -> Environment<(T1, T2)> { + let result = self.current((T1, T2).self) + result.set(index: 0, value: t1.value) + result.set(index: 1, value: t2.value) + return result + } + + public static func buildBlock(_ t1: Partial, _ t2: Partial, _ t3: Partial) -> Environment<(T1, T2, T3)> { + let result = self.current((T1, T2, T3).self) + result.set(index: 0, value: t1.value) + result.set(index: 1, value: t2.value) + result.set(index: 2, value: t3.value) + return result + } + + public static func buildBlock(_ t1: Partial, _ t2: Partial, _ t3: Partial, _ t4: Partial) -> Environment<(T1, T2, T3, T4)> { + let result = self.current((T1, T2, T3, T4).self) + result.set(index: 0, value: t1.value) + result.set(index: 1, value: t2.value) + result.set(index: 2, value: t3.value) + result.set(index: 3, value: t4.value) + return result + } +} + +@propertyWrapper +public struct ZeroEquatable: Equatable { + public var wrappedValue: T + + public init(_ wrappedValue: T) { + self.wrappedValue = wrappedValue + } + + public static func ==(lhs: ZeroEquatable, rhs: ZeroEquatable) -> Bool { + return true + } +} diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift new file mode 100644 index 0000000000..f0187f4d08 --- /dev/null +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -0,0 +1,270 @@ +import Foundation +import UIKit + +private extension UIView { + static var animationDurationFactor: Double { + return 1.0 + } +} + +@objc private class CALayerAnimationDelegate: NSObject, CAAnimationDelegate { + private let keyPath: String? + var completion: ((Bool) -> Void)? + + init(animation: CAAnimation, completion: ((Bool) -> Void)?) { + if let animation = animation as? CABasicAnimation { + self.keyPath = animation.keyPath + } else { + self.keyPath = nil + } + self.completion = completion + + super.init() + } + + @objc func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { + if let anim = anim as? CABasicAnimation { + if anim.keyPath != self.keyPath { + return + } + } + if let completion = self.completion { + completion(flag) + } + } +} + +private func makeSpringAnimation(keyPath: String) -> CASpringAnimation { + let springAnimation = CASpringAnimation(keyPath: keyPath) + springAnimation.mass = 3.0; + springAnimation.stiffness = 1000.0 + springAnimation.damping = 500.0 + springAnimation.duration = 0.5 + springAnimation.timingFunction = CAMediaTimingFunction(name: .linear) + return springAnimation +} + +private extension CALayer { + func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) -> CAAnimation { + switch curve { + case .spring: + let animation = makeSpringAnimation(keyPath: keyPath) + animation.fromValue = from + animation.toValue = to + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = .forwards + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + + let k = Float(UIView.animationDurationFactor) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + animation.speed = speed * Float(animation.duration / duration) + animation.isAdditive = additive + + if !delay.isZero { + animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor + animation.fillMode = .both + } + + return animation + default: + let k = Float(UIView.animationDurationFactor) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + let animation = CABasicAnimation(keyPath: keyPath) + animation.fromValue = from + animation.toValue = to + animation.duration = duration + animation.timingFunction = curve.asTimingFunction() + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = .forwards + animation.speed = speed + animation.isAdditive = additive + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + + if !delay.isZero { + animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor + animation.fillMode = .both + } + + return animation + } + } + + func animate(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) { + let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, duration: duration, delay: delay, curve: curve, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + self.add(animation, forKey: additive ? nil : keyPath) + } +} + +private extension Transition.Animation.Curve { + func asTimingFunction() -> CAMediaTimingFunction { + switch self { + case .easeInOut: + return CAMediaTimingFunction(name: .easeInEaseOut) + case .spring: + preconditionFailure() + } + } +} + +public struct Transition { + public enum Animation { + public enum Curve { + case easeInOut + case spring + } + + case none + case curve(duration: Double, curve: Curve) + } + + public var animation: Animation + private var _userData: [Any] = [] + + public func userData(_ type: T.Type) -> T? { + for item in self._userData { + if let item = item as? T { + return item + } + } + return nil + } + + public func withUserData(_ userData: Any) -> Transition { + var result = self + result._userData.append(userData) + return result + } + + public static var immediate: Transition = Transition(animation: .none) + + public static func easeInOut(duration: Double) -> Transition { + return Transition(animation: .curve(duration: duration, curve: .easeInOut)) + } + + public init(animation: Animation) { + self.animation = animation + } + + public func setFrame(view: UIView, frame: CGRect, completion: ((Bool) -> Void)? = nil) { + if view.frame == frame { + completion?(true) + return + } + switch self.animation { + case .none: + view.frame = frame + completion?(true) + case .curve: + let previousPosition = view.center + let previousBounds = view.bounds + view.frame = frame + + self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion) + self.animateBounds(view: view, from: previousBounds, to: view.bounds) + } + } + + public func setAlpha(view: UIView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { + if view.alpha == alpha { + completion?(true) + return + } + switch self.animation { + case .none: + view.alpha = alpha + completion?(true) + case .curve: + let previousAlpha = view.alpha + view.alpha = alpha + self.animateAlpha(view: view, from: previousAlpha, to: alpha, completion: completion) + } + } + + public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + completion?(true) + case let .curve(duration, curve): + view.layer.animate( + from: fromValue as NSNumber, + to: toValue as NSNumber, + keyPath: "transform.scale", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: additive, + completion: completion + ) + } + } + + public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + completion?(true) + case let .curve(duration, curve): + view.layer.animate( + from: fromValue as NSNumber, + to: toValue as NSNumber, + keyPath: "opacity", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: additive, + completion: completion + ) + } + } + + public func animatePosition(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + completion?(true) + case let .curve(duration, curve): + view.layer.animate( + from: NSValue(cgPoint: fromValue), + to: NSValue(cgPoint: toValue), + keyPath: "position", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: additive, + completion: completion + ) + } + } + + public func animateBounds(view: UIView, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + break + case let .curve(duration, curve): + view.layer.animate( + from: NSValue(cgRect: fromValue), + to: NSValue(cgRect: toValue), + keyPath: "bounds", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: additive, + completion: completion + ) + } + } +} diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift new file mode 100644 index 0000000000..33f7a3634d --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -0,0 +1,68 @@ +import Foundation +import UIKit + +final class Button: CombinedComponent, Equatable { + let content: AnyComponent + let insets: UIEdgeInsets + let action: () -> Void + + init( + content: AnyComponent, + insets: UIEdgeInsets, + action: @escaping () -> Void + ) { + self.content = content + self.insets = insets + self.action = action + } + + static func ==(lhs: Button, rhs: Button) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + + final class State: ComponentState { + var isHighlighted = false + + override init() { + super.init() + } + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let content = Child(environment: Empty.self) + + return { context in + let content = content.update( + component: context.component.content, + availableSize: CGSize(width: context.availableSize.width, height: 44.0), transition: context.transition + ) + + let size = CGSize(width: content.size.width + context.component.insets.left + context.component.insets.right, height: content.size.height + context.component.insets.top + context.component.insets.bottom) + + let component = context.component + + context.add(content + .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + .opacity(context.state.isHighlighted ? 0.2 : 1.0) + .update(Transition.Update { component, view, transition in + view.frame = component.size.centered(around: component._position ?? CGPoint()) + }) + .gesture(.tap { + component.action() + }) + ) + + return size + } + } +} diff --git a/submodules/ComponentFlow/Source/Components/List.swift b/submodules/ComponentFlow/Source/Components/List.swift new file mode 100644 index 0000000000..b73b6d7efd --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/List.swift @@ -0,0 +1,48 @@ +import Foundation +import UIKit + +public final class List: CombinedComponent { + public typealias EnvironmentType = ChildEnvironment + + private let items: [AnyComponentWithIdentity] + private let appear: Transition.Appear + + public init(_ items: [AnyComponentWithIdentity], appear: Transition.Appear = .default()) { + self.items = items + self.appear = appear + } + + public static func ==(lhs: List, rhs: List) -> Bool { + if lhs.items != rhs.items { + return false + } + return true + } + + public static var body: Body { + let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self) + + return { context in + let updatedChildren = context.component.items.map { item in + return children[item.id].update( + component: item.component, environment: { + context.environment[ChildEnvironment.self] + }, + availableSize: context.availableSize, + transition: context.transition + ) + } + + var nextOrigin: CGFloat = 0.0 + for child in updatedChildren { + context.add(child + .position(CGPoint(x: child.size.width / 2.0, y: nextOrigin + child.size.height / 2.0)) + .appear(context.component.appear) + ) + nextOrigin += child.size.height + } + + return context.availableSize + } + } +} diff --git a/submodules/ComponentFlow/Source/Components/Rectangle.swift b/submodules/ComponentFlow/Source/Components/Rectangle.swift new file mode 100644 index 0000000000..885bb0ef29 --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/Rectangle.swift @@ -0,0 +1,41 @@ +import Foundation +import UIKit + +public final class Rectangle: Component { + private let color: UIColor + private let width: CGFloat? + private let height: CGFloat? + + public init(color: UIColor, width: CGFloat? = nil, height: CGFloat? = nil) { + self.color = color + self.width = width + self.height = height + } + + public static func ==(lhs: Rectangle, rhs: Rectangle) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } + if lhs.width != rhs.width { + return false + } + if lhs.height != rhs.height { + return false + } + return true + } + + public func update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize { + var size = availableSize + if let width = self.width { + size.width = min(size.width, width) + } + if let height = self.height { + size.height = min(size.height, height) + } + + view.backgroundColor = self.color + + return size + } +} diff --git a/submodules/ComponentFlow/Source/Components/Text.swift b/submodules/ComponentFlow/Source/Components/Text.swift new file mode 100644 index 0000000000..f3b66ce6f6 --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/Text.swift @@ -0,0 +1,101 @@ +import Foundation +import UIKit + +public final class Text: Component { + private final class MeasureState: Equatable { + let attributedText: NSAttributedString + let availableSize: CGSize + let size: CGSize + + init(attributedText: NSAttributedString, availableSize: CGSize, size: CGSize) { + self.attributedText = attributedText + self.availableSize = availableSize + self.size = size + } + + static func ==(lhs: MeasureState, rhs: MeasureState) -> Bool { + if !lhs.attributedText.isEqual(rhs.attributedText) { + return false + } + if lhs.availableSize != rhs.availableSize { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + } + + public final class View: UIView { + private var measureState: MeasureState? + + func update(component: Text, availableSize: CGSize) -> CGSize { + let attributedText = NSAttributedString(string: component.text, attributes: [ + NSAttributedString.Key.font: component.font, + NSAttributedString.Key.foregroundColor: component.color + ]) + + if let measureState = self.measureState { + if measureState.attributedText.isEqual(to: attributedText) && measureState.availableSize == availableSize { + return measureState.size + } + } + + var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil) + boundingRect.size.width = ceil(boundingRect.size.width) + boundingRect.size.height = ceil(boundingRect.size.height) + + let measureState = MeasureState(attributedText: attributedText, availableSize: availableSize, size: boundingRect.size) + if #available(iOS 10.0, *) { + let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: measureState.size)) + let image = renderer.image { context in + UIGraphicsPushContext(context.cgContext) + measureState.attributedText.draw(at: CGPoint()) + UIGraphicsPopContext() + } + self.layer.contents = image.cgImage + } else { + UIGraphicsBeginImageContextWithOptions(measureState.size, false, 0.0) + measureState.attributedText.draw(at: CGPoint()) + self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage + UIGraphicsEndImageContext() + } + + self.measureState = measureState + + return boundingRect.size + } + } + + public let text: String + public let font: UIFont + public let color: UIColor + + public init(text: String, font: UIFont, color: UIColor) { + self.text = text + self.font = font + self.color = color + } + + public static func ==(lhs: Text, rhs: Text) -> Bool { + if lhs.text != rhs.text { + return false + } + if !lhs.font.isEqual(rhs.font) { + return false + } + if !lhs.color.isEqual(rhs.color) { + return false + } + return true + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize) + } +} diff --git a/submodules/ComponentFlow/Source/Gestures/Gesture.swift b/submodules/ComponentFlow/Source/Gestures/Gesture.swift new file mode 100644 index 0000000000..a67e5895b0 --- /dev/null +++ b/submodules/ComponentFlow/Source/Gestures/Gesture.swift @@ -0,0 +1,29 @@ +import Foundation +import UIKit + +public class Gesture { + class Id { + private var _id: UInt = 0 + public var id: UInt { + return self._id + } + + init() { + self._id = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque()) + } + } + + let id: Id + + init(id: Id) { + self.id = id + } + + func create() -> UIGestureRecognizer { + preconditionFailure() + } + + func update(gesture: UIGestureRecognizer) { + preconditionFailure() + } +} diff --git a/submodules/ComponentFlow/Source/Gestures/PanGesture.swift b/submodules/ComponentFlow/Source/Gestures/PanGesture.swift new file mode 100644 index 0000000000..38a5e9b538 --- /dev/null +++ b/submodules/ComponentFlow/Source/Gestures/PanGesture.swift @@ -0,0 +1,59 @@ +import Foundation +import UIKit + +public extension Gesture { + enum PanGestureState { + case began + case updated(offset: CGPoint) + case ended + } + + private final class PanGesture: Gesture { + private class Impl: UIPanGestureRecognizer { + var action: (PanGestureState) -> Void + + init(action: @escaping (PanGestureState) -> Void) { + self.action = action + + super.init(target: nil, action: nil) + self.addTarget(self, action: #selector(self.onAction)) + } + + @objc private func onAction() { + switch self.state { + case .began: + self.action(.began) + case .ended, .cancelled: + self.action(.ended) + case .changed: + let offset = self.translation(in: self.view) + self.action(.updated(offset: offset)) + default: + break + } + } + } + + static let id = Id() + + private let action: (PanGestureState) -> Void + + init(action: @escaping (PanGestureState) -> Void) { + self.action = action + + super.init(id: Self.id) + } + + override func create() -> UIGestureRecognizer { + return Impl(action: self.action) + } + + override func update(gesture: UIGestureRecognizer) { + (gesture as! Impl).action = action + } + } + + static func pan(_ action: @escaping (PanGestureState) -> Void) -> Gesture { + return PanGesture(action: action) + } +} diff --git a/submodules/ComponentFlow/Source/Gestures/TapGesture.swift b/submodules/ComponentFlow/Source/Gestures/TapGesture.swift new file mode 100644 index 0000000000..da93e71d5a --- /dev/null +++ b/submodules/ComponentFlow/Source/Gestures/TapGesture.swift @@ -0,0 +1,43 @@ +import Foundation +import UIKit + +public extension Gesture { + private final class TapGesture: Gesture { + private class Impl: UITapGestureRecognizer { + var action: () -> Void + + init(action: @escaping () -> Void) { + self.action = action + + super.init(target: nil, action: nil) + self.addTarget(self, action: #selector(self.onAction)) + } + + @objc private func onAction() { + self.action() + } + } + + static let id = Id() + + private let action: () -> Void + + init(action: @escaping () -> Void) { + self.action = action + + super.init(id: Self.id) + } + + override func create() -> UIGestureRecognizer { + return Impl(action: self.action) + } + + override func update(gesture: UIGestureRecognizer) { + (gesture as! Impl).action = action + } + } + + static func tap(_ action: @escaping () -> Void) -> Gesture { + return TapGesture(action: action) + } +} diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift new file mode 100644 index 0000000000..9cd8780b38 --- /dev/null +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -0,0 +1,69 @@ +import Foundation +import UIKit + +public final class ComponentHostView: UIView { + private var componentView: UIView? + private(set) var isUpdating: Bool = false + + public init() { + super.init(frame: CGRect()) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(transition: Transition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, containerSize: CGSize) -> CGSize { + self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, containerSize: containerSize) + } + + private func _update(transition: Transition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, containerSize: CGSize) -> CGSize { + precondition(!self.isUpdating) + self.isUpdating = true + + precondition(containerSize.width.isFinite) + precondition(containerSize.width < .greatestFiniteMagnitude) + precondition(containerSize.height.isFinite) + precondition(containerSize.height < .greatestFiniteMagnitude) + + let componentView: UIView + if let current = self.componentView { + componentView = current + } else { + componentView = component._makeView() + self.componentView = componentView + self.addSubview(componentView) + } + + let context = componentView.context(component: component) + + let componentState: ComponentState = context.erasedState + + if updateEnvironment { + EnvironmentBuilder._environment = context.erasedEnvironment + let _ = maybeEnvironment() + EnvironmentBuilder._environment = nil + } + + componentState._updated = { [weak self] transition in + guard let strongSelf = self else { + return + } + let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: { + preconditionFailure() + } as () -> Environment, updateEnvironment: false, containerSize: containerSize) + } + + let updatedSize = component._update(view: componentView, availableSize: containerSize, transition: transition) + transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: updatedSize)) + + self.isUpdating = false + + return updatedSize + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result + } +} diff --git a/submodules/ComponentFlow/Source/Host/NavigationLayoutEnvironment.swift b/submodules/ComponentFlow/Source/Host/NavigationLayoutEnvironment.swift new file mode 100644 index 0000000000..d6eb55eb31 --- /dev/null +++ b/submodules/ComponentFlow/Source/Host/NavigationLayoutEnvironment.swift @@ -0,0 +1,8 @@ +import Foundation +import UIKit + +public struct NavigationLayout: Equatable { + public var statusBarHeight: CGFloat + public var inputHeight: CGFloat + public var bottomNavigationHeight: CGFloat +} diff --git a/submodules/ComponentFlow/Source/Host/RootHostView.swift b/submodules/ComponentFlow/Source/Host/RootHostView.swift new file mode 100644 index 0000000000..37d916ddf2 --- /dev/null +++ b/submodules/ComponentFlow/Source/Host/RootHostView.swift @@ -0,0 +1,134 @@ +import Foundation +import UIKit + +public final class RootHostView: UIViewController { + private let content: AnyComponent<(NavigationLayout, EnvironmentType)> + + private var keyboardWillChangeFrameObserver: NSObjectProtocol? + private var inputHeight: CGFloat = 0.0 + + private let environment: Environment + private var componentView: ComponentHostView<(NavigationLayout, EnvironmentType)> + + private var scheduledTransition: Transition? + + public init( + content: AnyComponent<(NavigationLayout, EnvironmentType)>, + @EnvironmentBuilder environment: () -> Environment + ) { + self.content = content + + self.environment = Environment() + self.componentView = ComponentHostView<(NavigationLayout, EnvironmentType)>() + + EnvironmentBuilder._environment = self.environment + let _ = environment() + EnvironmentBuilder._environment = nil + + super.init(nibName: nil, bundle: nil) + + NotificationCenter.default.addObserver(forName: UIApplication.keyboardWillChangeFrameNotification, object: nil, queue: nil, using: { [weak self] notification in + guard let strongSelf = self else { + return + } + guard let keyboardFrame = notification.userInfo?[UIApplication.keyboardFrameEndUserInfoKey] as? CGRect else { + return + } + + var duration: Double = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0 + if duration > Double.ulpOfOne { + duration = 0.5 + } + let curve: UInt = (notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 7 + + let transition: Transition + if curve == 7 { + transition = Transition(animation: .curve(duration: duration, curve: .spring)) + } else { + transition = Transition(animation: .curve(duration: duration, curve: .easeInOut)) + } + + strongSelf.updateKeyboardLayout(keyboardFrame: keyboardFrame, transition: transition) + }) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.keyboardWillChangeFrameObserver.flatMap(NotificationCenter.default.removeObserver) + } + + private func updateKeyboardLayout(keyboardFrame: CGRect, transition: Transition) { + self.inputHeight = max(0.0, self.view.bounds.height - keyboardFrame.minY) + if self.componentView.isUpdating || true { + if let _ = self.scheduledTransition { + if case .curve = transition.animation { + self.scheduledTransition = transition + } + } else { + self.scheduledTransition = transition + } + self.view.setNeedsLayout() + } else { + self.updateComponent(size: self.view.bounds.size, transition: transition) + } + } + + private func updateComponent(size: CGSize, transition: Transition) { + self.environment._isUpdated = false + + transition.setFrame(view: self.componentView, frame: CGRect(origin: CGPoint(), size: size)) + self.componentView.update( + transition: transition, + component: self.content, + environment: { + NavigationLayout( + statusBarHeight: size.width > size.height ? 0.0 : 40.0, + inputHeight: self.inputHeight, + bottomNavigationHeight: 22.0 + ) + self.environment[EnvironmentType.self] + }, + containerSize: size + ) + } + + public func updateEnvironment(@EnvironmentBuilder environment: () -> Environment) { + EnvironmentBuilder._environment = self.environment + let _ = environment() + EnvironmentBuilder._environment = nil + + if self.environment.calculateIsUpdated() { + if !self.view.bounds.size.width.isZero { + self.updateComponent(size: self.view.bounds.size, transition: .immediate) + } + } + } + + override public func viewDidLoad() { + super.viewDidLoad() + + self.view.addSubview(self.componentView) + + if !self.view.bounds.size.width.isZero { + self.updateComponent(size: self.view.bounds.size, transition: .immediate) + } + } + + override public func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if let scheduledTransition = self.scheduledTransition { + self.scheduledTransition = nil + self.updateComponent(size: self.view.bounds.size, transition: scheduledTransition) + } + } + + override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + self.updateComponent(size: size, transition: coordinator.isAnimated ? .easeInOut(duration: 0.3) : .immediate) + } +} diff --git a/submodules/ComponentFlow/Source/Utils/Color.swift b/submodules/ComponentFlow/Source/Utils/Color.swift new file mode 100644 index 0000000000..1eb3f11a39 --- /dev/null +++ b/submodules/ComponentFlow/Source/Utils/Color.swift @@ -0,0 +1,8 @@ +import Foundation +import UIKit + +extension UIColor { + convenience init(rgb: UInt32) { + self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) + } +} diff --git a/submodules/ComponentFlow/Source/Utils/Condition.swift b/submodules/ComponentFlow/Source/Utils/Condition.swift new file mode 100644 index 0000000000..f74c72e8cf --- /dev/null +++ b/submodules/ComponentFlow/Source/Utils/Condition.swift @@ -0,0 +1,9 @@ +import Foundation + +public func Condition(_ f: @autoclosure () -> Bool, _ pass: () -> R) -> R? { + if f() { + return pass() + } else { + return nil + } +} diff --git a/submodules/ComponentFlow/Source/Utils/EscapeGuard.swift b/submodules/ComponentFlow/Source/Utils/EscapeGuard.swift new file mode 100644 index 0000000000..94d6b9cd25 --- /dev/null +++ b/submodules/ComponentFlow/Source/Utils/EscapeGuard.swift @@ -0,0 +1,13 @@ +import Foundation + +final class EscapeGuard { + final class Status { + fileprivate(set) var isDeallocated: Bool = false + } + + let status = Status() + + deinit { + self.status.isDeallocated = true + } +} diff --git a/submodules/ComponentFlow/Source/Utils/Insets.swift b/submodules/ComponentFlow/Source/Utils/Insets.swift new file mode 100644 index 0000000000..f456851c1a --- /dev/null +++ b/submodules/ComponentFlow/Source/Utils/Insets.swift @@ -0,0 +1,8 @@ +import Foundation +import UIKit + +public extension UIEdgeInsets { + init(_ value: CGFloat) { + self.init(top: value, left: value, bottom: value, right: value) + } +} diff --git a/submodules/ComponentFlow/Source/Utils/Rect.swift b/submodules/ComponentFlow/Source/Utils/Rect.swift new file mode 100644 index 0000000000..7b5b76d064 --- /dev/null +++ b/submodules/ComponentFlow/Source/Utils/Rect.swift @@ -0,0 +1,8 @@ +import Foundation +import UIKit + +public extension CGRect { + var center: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} diff --git a/submodules/ComponentFlow/Source/Utils/Size.swift b/submodules/ComponentFlow/Source/Utils/Size.swift new file mode 100644 index 0000000000..310f8ce322 --- /dev/null +++ b/submodules/ComponentFlow/Source/Utils/Size.swift @@ -0,0 +1,28 @@ +import Foundation +import UIKit + +public extension CGSize { + func centered(in rect: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: rect.minX + floor((rect.width - self.width) / 2.0), y: rect.minY + floor((rect.height - self.height) / 2.0)), size: self) + } + + func centered(around position: CGPoint) -> CGRect { + return CGRect(origin: CGPoint(x: position.x - self.width / 2.0, y: position.y - self.height / 2.0), size: self) + } + + func leftCentered(in rect: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: rect.minX, y: rect.minY + floor((rect.height - self.height) / 2.0)), size: self) + } + + func rightCentered(in rect: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: rect.maxX - self.width, y: rect.minY + floor((rect.height - self.height) / 2.0)), size: self) + } + + func topCentered(in rect: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: rect.minX + floor((rect.width - self.width) / 2.0), y: 0.0), size: self) + } + + func bottomCentered(in rect: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: rect.minX + floor((rect.width - self.width) / 2.0), y: rect.maxY - self.height), size: self) + } +} diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index b5b9e7cac5..3eb5c4492d 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -396,8 +396,8 @@ public enum TabBarItemContextActionType { } navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, isLandscape: isLandscape, transition: transition) if !transition.isAnimated { - navigationBar.layer.cancelAnimationsRecursive(key: "bounds") - navigationBar.layer.cancelAnimationsRecursive(key: "position") + navigationBar.layer.removeAnimation(forKey: "bounds") + navigationBar.layer.removeAnimation(forKey: "position") } transition.updateFrame(node: navigationBar, frame: navigationBarFrame) navigationBar.setHidden(!self.displayNavigationBar, animated: transition.isAnimated) diff --git a/submodules/Postbox/Sources/Coding.swift b/submodules/Postbox/Sources/Coding.swift index 26de86c051..8a4f17e519 100644 --- a/submodules/Postbox/Sources/Coding.swift +++ b/submodules/Postbox/Sources/Coding.swift @@ -215,7 +215,7 @@ public final class ReadBuffer: MemoryBuffer { } } -private enum ValueType: Int8 { +enum ValueType: Int8 { case Int32 = 0 case Int64 = 1 case Bool = 2 @@ -258,35 +258,21 @@ public final class PostboxEncoder { self.buffer.reset() } - public func encodeKey(_ key: StaticString) { - var length: Int8 = Int8(key.utf8CodeUnitCount) - self.buffer.write(&length, offset: 0, length: 1) - self.buffer.write(key.utf8Start, offset: 0, length: Int(length)) - } - public func encodeKey(_ key: String) { let data = key.data(using: .utf8)! - data.withUnsafeBytes { (keyBytes: UnsafePointer) -> Void in - var length: Int8 = Int8(data.count) - self.buffer.write(&length, offset: 0, length: 1) - self.buffer.write(keyBytes, offset: 0, length: Int(length)) + var length: Int8 = Int8(data.count) + self.buffer.write(&length, offset: 0, length: 1) + data.withUnsafeBytes { bytes in + self.buffer.write(bytes.baseAddress!, offset: 0, length: Int(length)) } } - public func encodeNil(forKey key: StaticString) { + public func encodeNil(forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Nil.rawValue self.buffer.write(&type, offset: 0, length: 1) } - public func encodeInt32(_ value: Int32, forKey key: StaticString) { - self.encodeKey(key) - var type: Int8 = ValueType.Int32.rawValue - self.buffer.write(&type, offset: 0, length: 1) - var v = value - self.buffer.write(&v, offset: 0, length: 4) - } - public func encodeInt32(_ value: Int32, forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Int32.rawValue @@ -295,7 +281,7 @@ public final class PostboxEncoder { self.buffer.write(&v, offset: 0, length: 4) } - public func encodeInt64(_ value: Int64, forKey key: StaticString) { + public func encodeInt64(_ value: Int64, forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Int64.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -303,7 +289,7 @@ public final class PostboxEncoder { self.buffer.write(&v, offset: 0, length: 8) } - public func encodeBool(_ value: Bool, forKey key: StaticString) { + public func encodeBool(_ value: Bool, forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Bool.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -311,7 +297,7 @@ public final class PostboxEncoder { self.buffer.write(&v, offset: 0, length: 1) } - public func encodeDouble(_ value: Double, forKey key: StaticString) { + public func encodeDouble(_ value: Double, forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Double.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -319,7 +305,7 @@ public final class PostboxEncoder { self.buffer.write(&v, offset: 0, length: 8) } - public func encodeString(_ value: String, forKey key: StaticString) { + public func encodeString(_ value: String, forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.String.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -337,13 +323,13 @@ public final class PostboxEncoder { self.encodeObject(value, forKey: "_") } - public func encodeCodable(_ value: T, forKey key: StaticString) { + public func encodeCodable(_ value: T, forKey key: String) { if let data = try? JSONEncoder().encode(value) { self.encodeData(data, forKey: key) } } - public func encodeObject(_ value: PostboxCoding, forKey key: StaticString) { + public func encodeObject(_ value: PostboxCoding, forKey key: String) { self.encodeKey(key) var t: Int8 = ValueType.Object.rawValue self.buffer.write(&t, offset: 0, length: 1) @@ -377,7 +363,7 @@ public final class PostboxEncoder { self.buffer.write(innerEncoder.buffer.memory, offset: 0, length: Int(length)) } - public func encodeInt32Array(_ value: [Int32], forKey key: StaticString) { + public func encodeInt32Array(_ value: [Int32], forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Int32Array.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -389,7 +375,7 @@ public final class PostboxEncoder { } } - public func encodeInt64Array(_ value: [Int64], forKey key: StaticString) { + public func encodeInt64Array(_ value: [Int64], forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Int64Array.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -401,7 +387,7 @@ public final class PostboxEncoder { } } - public func encodeObjectArray(_ value: [T], forKey key: StaticString) { + public func encodeObjectArray(_ value: [T], forKey key: String) { self.encodeKey(key) var t: Int8 = ValueType.ObjectArray.rawValue self.buffer.write(&t, offset: 0, length: 1) @@ -421,7 +407,7 @@ public final class PostboxEncoder { } } - public func encodeObjectArrayWithEncoder(_ value: [T], forKey key: StaticString, encoder: (T, PostboxEncoder) -> Void) { + public func encodeObjectArrayWithEncoder(_ value: [T], forKey key: String, encoder: (T, PostboxEncoder) -> Void) { self.encodeKey(key) var t: Int8 = ValueType.ObjectArray.rawValue self.buffer.write(&t, offset: 0, length: 1) @@ -441,7 +427,7 @@ public final class PostboxEncoder { } } - public func encodeGenericObjectArray(_ value: [PostboxCoding], forKey key: StaticString) { + public func encodeGenericObjectArray(_ value: [PostboxCoding], forKey key: String) { self.encodeKey(key) var t: Int8 = ValueType.ObjectArray.rawValue self.buffer.write(&t, offset: 0, length: 1) @@ -461,7 +447,7 @@ public final class PostboxEncoder { } } - public func encodeStringArray(_ value: [String], forKey key: StaticString) { + public func encodeStringArray(_ value: [String], forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.StringArray.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -476,7 +462,7 @@ public final class PostboxEncoder { } } - public func encodeBytesArray(_ value: [MemoryBuffer], forKey key: StaticString) { + public func encodeBytesArray(_ value: [MemoryBuffer], forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.BytesArray.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -490,7 +476,7 @@ public final class PostboxEncoder { } } - public func encodeDataArray(_ value: [Data], forKey key: StaticString) { + public func encodeDataArray(_ value: [Data], forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.BytesArray.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -506,7 +492,7 @@ public final class PostboxEncoder { } } - public func encodeObjectDictionary(_ value: [K : V], forKey key: StaticString) where K: PostboxCoding { + public func encodeObjectDictionary(_ value: [K : V], forKey key: String) where K: PostboxCoding { self.encodeKey(key) var t: Int8 = ValueType.ObjectDictionary.rawValue self.buffer.write(&t, offset: 0, length: 1) @@ -533,7 +519,7 @@ public final class PostboxEncoder { } } - public func encodeObjectDictionary(_ value: [K : V], forKey key: StaticString, keyEncoder: (K, PostboxEncoder) -> Void) { + public func encodeObjectDictionary(_ value: [K : V], forKey key: String, keyEncoder: (K, PostboxEncoder) -> Void) { self.encodeKey(key) var t: Int8 = ValueType.ObjectDictionary.rawValue self.buffer.write(&t, offset: 0, length: 1) @@ -560,7 +546,7 @@ public final class PostboxEncoder { } } - public func encodeBytes(_ bytes: WriteBuffer, forKey key: StaticString) { + public func encodeBytes(_ bytes: WriteBuffer, forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Bytes.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -569,7 +555,7 @@ public final class PostboxEncoder { self.buffer.write(bytes.memory, offset: 0, length: bytes.offset) } - public func encodeBytes(_ bytes: ReadBuffer, forKey key: StaticString) { + public func encodeBytes(_ bytes: ReadBuffer, forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Bytes.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -578,7 +564,7 @@ public final class PostboxEncoder { self.buffer.write(bytes.memory, offset: 0, length: bytes.offset) } - public func encodeBytes(_ bytes: MemoryBuffer, forKey key: StaticString) { + public func encodeBytes(_ bytes: MemoryBuffer, forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Bytes.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -587,7 +573,7 @@ public final class PostboxEncoder { self.buffer.write(bytes.memory, offset: 0, length: bytes.length) } - public func encodeData(_ data: Data, forKey key: StaticString) { + public func encodeData(_ data: Data, forKey key: String) { self.encodeKey(key) var type: Int8 = ValueType.Bytes.rawValue self.buffer.write(&type, offset: 0, length: 1) @@ -598,6 +584,24 @@ public final class PostboxEncoder { } } + public func encode(_ value: T, forKey key: String) { + let typeHash: Int32 = murMurHashString32("\(type(of: value))") + let innerEncoder = _AdaptedPostboxEncoder(typeHash: typeHash) + try! value.encode(to: innerEncoder) + + let (data, valueType) = innerEncoder.makeData() + self.encodeInnerObjectData(data, valueType: valueType, forKey: key) + } + + func encodeInnerObjectData(_ value: Data, valueType: ValueType, forKey key: String) { + self.encodeKey(key) + + var t: Int8 = valueType.rawValue + self.buffer.write(&t, offset: 0, length: 1) + + self.buffer.write(value) + } + public let sharedWriteBuffer = WriteBuffer() } @@ -680,41 +684,65 @@ public final class PostboxDecoder { } } } - - private class func positionOnKey(_ rawBytes: UnsafeRawPointer, offset: inout Int, maxOffset: Int, length: Int, key: StaticString, valueType: ValueType) -> Bool + + private class func positionOnKey(_ rawBytes: UnsafeRawPointer, offset: inout Int, maxOffset: Int, length: Int, key: String, valueType: ValueType) -> Bool { + var actualValueType: ValueType = .Nil + return positionOnKey(rawBytes, offset: &offset, maxOffset: maxOffset, length: length, key: key, valueType: valueType, actualValueType: &actualValueType, consumeKey: true) + } + + private class func positionOnKey(_ rawBytes: UnsafeRawPointer, offset: inout Int, maxOffset: Int, length: Int, key: String, valueType: ValueType?, actualValueType: inout ValueType, consumeKey: Bool) -> Bool { let bytes = rawBytes.assumingMemoryBound(to: Int8.self) - + let startOffset = offset - - let keyLength: Int = key.utf8CodeUnitCount + + let keyData = key.data(using: .utf8)! + + let keyLength: Int = keyData.count while (offset < maxOffset) { + let keyOffset = offset let readKeyLength = bytes[offset] assert(readKeyLength >= 0) offset += 1 offset += Int(readKeyLength) - + let readValueType = bytes[offset] offset += 1 - - if keyLength == Int(readKeyLength) && memcmp(bytes + (offset - Int(readKeyLength) - 1), key.utf8Start, keyLength) == 0 { - if readValueType == valueType.rawValue { - return true - } else if readValueType == ValueType.Nil.rawValue { - return false + + if keyLength != Int(readKeyLength) { + skipValue(bytes, offset: &offset, length: length, valueType: ValueType(rawValue: readValueType)!) + continue + } + + if keyData.withUnsafeBytes({ keyBytes -> Bool in + return memcmp(bytes + (offset - Int(readKeyLength) - 1), keyBytes.baseAddress!, keyLength) == 0 + }) { + if let valueType = valueType { + if readValueType == valueType.rawValue { + actualValueType = valueType + return true + } else if readValueType == ValueType.Nil.rawValue { + return false + } else { + skipValue(bytes, offset: &offset, length: length, valueType: ValueType(rawValue: readValueType)!) + } } else { - skipValue(bytes, offset: &offset, length: length, valueType: ValueType(rawValue: readValueType)!) + if !consumeKey { + offset = keyOffset + } + actualValueType = ValueType(rawValue: readValueType)! + return true } } else { skipValue(bytes, offset: &offset, length: length, valueType: ValueType(rawValue: readValueType)!) } } - + if (startOffset != 0) { offset = 0 - return positionOnKey(bytes, offset: &offset, maxOffset: startOffset, length: length, key: key, valueType: valueType) + return positionOnKey(bytes, offset: &offset, maxOffset: startOffset, length: length, key: key, valueType: valueType, actualValueType: &actualValueType, consumeKey: consumeKey) } - + return false } @@ -789,20 +817,26 @@ public final class PostboxDecoder { return false } - - public func decodeInt32ForKey(_ key: StaticString, orElse: Int32) -> Int32 { - if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) { - var value: Int32 = 0 - memcpy(&value, self.buffer.memory + self.offset, 4) - self.offset += 4 - return value + + public func containsKey(_ key: String) -> Bool { + var actualValueType: ValueType = .Nil + if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: nil, actualValueType: &actualValueType, consumeKey: false) { + return true } else { - return orElse + return false + } + } + + public func decodeNilForKey(_ key: String) -> Bool { + if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Nil) { + return true + } else { + return false } } public func decodeInt32ForKey(_ key: String, orElse: Int32) -> Int32 { - if PostboxDecoder.positionOnStringKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) { + if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) { var value: Int32 = 0 memcpy(&value, self.buffer.memory + self.offset, 4) self.offset += 4 @@ -812,7 +846,7 @@ public final class PostboxDecoder { } } - public func decodeOptionalInt32ForKey(_ key: StaticString) -> Int32? { + public func decodeOptionalInt32ForKey(_ key: String) -> Int32? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) { var value: Int32 = 0 memcpy(&value, self.buffer.memory + self.offset, 4) @@ -823,18 +857,7 @@ public final class PostboxDecoder { } } - public func decodeOptionalInt32ForKey(_ key: String) -> Int32? { - if PostboxDecoder.positionOnStringKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32) { - var value: Int32 = 0 - memcpy(&value, self.buffer.memory + self.offset, 4) - self.offset += 4 - return value - } else { - return nil - } - } - - public func decodeInt64ForKey(_ key: StaticString, orElse: Int64) -> Int64 { + public func decodeInt64ForKey(_ key: String, orElse: Int64) -> Int64 { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int64) { var value: Int64 = 0 memcpy(&value, self.buffer.memory + self.offset, 8) @@ -845,7 +868,7 @@ public final class PostboxDecoder { } } - public func decodeOptionalInt64ForKey(_ key: StaticString) -> Int64? { + public func decodeOptionalInt64ForKey(_ key: String) -> Int64? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int64) { var value: Int64 = 0 memcpy(&value, self.buffer.memory + self.offset, 8) @@ -856,7 +879,7 @@ public final class PostboxDecoder { } } - public func decodeBoolForKey(_ key: StaticString, orElse: Bool) -> Bool { + public func decodeBoolForKey(_ key: String, orElse: Bool) -> Bool { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bool) { var value: Int8 = 0 memcpy(&value, self.buffer.memory + self.offset, 1) @@ -867,7 +890,7 @@ public final class PostboxDecoder { } } - public func decodeOptionalBoolForKey(_ key: StaticString) -> Bool? { + public func decodeOptionalBoolForKey(_ key: String) -> Bool? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bool) { var value: Int8 = 0 memcpy(&value, self.buffer.memory + self.offset, 1) @@ -878,7 +901,7 @@ public final class PostboxDecoder { } } - public func decodeDoubleForKey(_ key: StaticString, orElse: Double) -> Double { + public func decodeDoubleForKey(_ key: String, orElse: Double) -> Double { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Double) { var value: Double = 0 memcpy(&value, self.buffer.memory + self.offset, 8) @@ -889,7 +912,7 @@ public final class PostboxDecoder { } } - public func decodeOptionalDoubleForKey(_ key: StaticString) -> Double? { + public func decodeOptionalDoubleForKey(_ key: String) -> Double? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Double) { var value: Double = 0 memcpy(&value, self.buffer.memory + self.offset, 8) @@ -900,7 +923,7 @@ public final class PostboxDecoder { } } - public func decodeStringForKey(_ key: StaticString, orElse: String) -> String { + public func decodeStringForKey(_ key: String, orElse: String) -> String { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .String) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -912,7 +935,7 @@ public final class PostboxDecoder { } } - public func decodeOptionalStringForKey(_ key: StaticString) -> String? { + public func decodeOptionalStringForKey(_ key: String) -> String? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .String) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -928,7 +951,7 @@ public final class PostboxDecoder { return self.decodeObjectForKey("_") } - public func decodeCodable(_ type: T.Type, forKey key: StaticString) -> T? { + public func decodeCodable(_ type: T.Type, forKey key: String) -> T? { if let data = self.decodeDataForKey(key) { return try? JSONDecoder().decode(T.self, from: data) } else { @@ -936,7 +959,7 @@ public final class PostboxDecoder { } } - public func decodeObjectForKey(_ key: StaticString) -> PostboxCoding? { + public func decodeObjectForKey(_ key: String) -> PostboxCoding? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) { var typeHash: Int32 = 0 memcpy(&typeHash, self.buffer.memory + self.offset, 4) @@ -953,8 +976,35 @@ public final class PostboxDecoder { return nil } } + + func decodeObjectDataForKey(_ key: String) -> (Data, ValueType)? { + var actualValueType: ValueType = .Nil + if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: nil, actualValueType: &actualValueType, consumeKey: true) { + if case .Object = actualValueType { + self.offset += 4 + + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + self.offset += 4 + + let innerData = ReadBuffer(memory: self.buffer.memory + self.offset, length: Int(length), freeWhenDone: false).makeData() + self.offset += Int(length) + + return (innerData, actualValueType) + } else { + let initialOffset = self.offset + PostboxDecoder.skipValue(self.buffer.memory.assumingMemoryBound(to: Int8.self), offset: &self.offset, length: self.buffer.length, valueType: actualValueType) + + let data = ReadBuffer(memory: UnsafeMutableRawPointer(mutating: self.buffer.memory.advanced(by: initialOffset)), length: self.offset - initialOffset, freeWhenDone: false).makeData() + + return (data, actualValueType) + } + } else { + return nil + } + } - public func decodeObjectForKey(_ key: StaticString, decoder: (PostboxDecoder) -> PostboxCoding) -> PostboxCoding? { + public func decodeObjectForKey(_ key: String, decoder: (PostboxDecoder) -> PostboxCoding) -> PostboxCoding? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) { var typeHash: Int32 = 0 memcpy(&typeHash, self.buffer.memory + self.offset, 4) @@ -972,7 +1022,7 @@ public final class PostboxDecoder { } } - public func decodeAnyObjectForKey(_ key: StaticString, decoder: (PostboxDecoder) -> Any?) -> Any? { + public func decodeAnyObjectForKey(_ key: String, decoder: (PostboxDecoder) -> Any?) -> Any? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) { var typeHash: Int32 = 0 memcpy(&typeHash, self.buffer.memory + self.offset, 4) @@ -990,7 +1040,7 @@ public final class PostboxDecoder { } } - public func decodeObjectForKeyThrowing(_ key: StaticString, decoder: (PostboxDecoder) throws -> Any) throws -> Any? { + public func decodeObjectForKeyThrowing(_ key: String, decoder: (PostboxDecoder) throws -> Any) throws -> Any? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) { var typeHash: Int32 = 0 memcpy(&typeHash, self.buffer.memory + self.offset, 4) @@ -1008,47 +1058,55 @@ public final class PostboxDecoder { } } - public func decodeInt32ArrayForKey(_ key: StaticString) -> [Int32] { + public func decodeInt32ArrayForKey(_ key: String) -> [Int32] { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32Array) { - var length: Int32 = 0 - memcpy(&length, self.buffer.memory + self.offset, 4) - var array: [Int32] = [] - array.reserveCapacity(Int(length)) - var i: Int32 = 0 - while i < length { - var element: Int32 = 0 - memcpy(&element, self.buffer.memory + (self.offset + 4 + 4 * Int(i)), 4) - array.append(element) - i += 1 - } - self.offset += 4 + Int(length) * 4 - return array + return decodeInt32ArrayRaw() } else { return [] } } + + func decodeInt32ArrayRaw() -> [Int32] { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + var array: [Int32] = [] + array.reserveCapacity(Int(length)) + var i: Int32 = 0 + while i < length { + var element: Int32 = 0 + memcpy(&element, self.buffer.memory + (self.offset + 4 + 4 * Int(i)), 4) + array.append(element) + i += 1 + } + self.offset += 4 + Int(length) * 4 + return array + } - public func decodeInt64ArrayForKey(_ key: StaticString) -> [Int64] { + public func decodeInt64ArrayForKey(_ key: String) -> [Int64] { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int64Array) { - var length: Int32 = 0 - memcpy(&length, self.buffer.memory + self.offset, 4) - var array: [Int64] = [] - array.reserveCapacity(Int(length)) - var i: Int32 = 0 - while i < length { - var element: Int64 = 0 - memcpy(&element, self.buffer.memory + (self.offset + 4 + 8 * Int(i)), 8) - array.append(element) - i += 1 - } - self.offset += 4 + Int(length) * 8 - return array + return decodeInt64ArrayRaw() } else { return [] } } + + func decodeInt64ArrayRaw() -> [Int64] { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + var array: [Int64] = [] + array.reserveCapacity(Int(length)) + var i: Int32 = 0 + while i < length { + var element: Int64 = 0 + memcpy(&element, self.buffer.memory + (self.offset + 4 + 8 * Int(i)), 8) + array.append(element) + i += 1 + } + self.offset += 4 + Int(length) * 8 + return array + } - public func decodeObjectArrayWithDecoderForKey(_ key: StaticString) -> [T] where T: PostboxCoding { + public func decodeObjectArrayWithDecoderForKey(_ key: String) -> [T] where T: PostboxCoding { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1080,7 +1138,7 @@ public final class PostboxDecoder { } } - public func decodeOptionalObjectArrayWithDecoderForKey(_ key: StaticString) -> [T]? where T: PostboxCoding { + public func decodeOptionalObjectArrayWithDecoderForKey(_ key: String) -> [T]? where T: PostboxCoding { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1108,11 +1166,20 @@ public final class PostboxDecoder { return array } else { - return nil + if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Int32Array) { + let array = decodeInt32ArrayRaw() + if array.isEmpty { + return [] + } else { + return nil + } + } else { + return nil + } } } - public func decodeObjectArrayWithCustomDecoderForKey(_ key: StaticString, decoder: (PostboxDecoder) throws -> T) throws -> [T] { + public func decodeObjectArrayWithCustomDecoderForKey(_ key: String, decoder: (PostboxDecoder) throws -> T) throws -> [T] { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1145,65 +1212,105 @@ public final class PostboxDecoder { } } - public func decodeStringArrayForKey(_ key: StaticString) -> [String] { + public func decodeStringArrayForKey(_ key: String) -> [String] { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .StringArray) { - var length: Int32 = 0 - memcpy(&length, self.buffer.memory + self.offset, 4) - self.offset += 4 - - var array: [String] = [] - array.reserveCapacity(Int(length)) - - var i: Int32 = 0 - while i < length { - var length: Int32 = 0 - memcpy(&length, self.buffer.memory + self.offset, 4) - let data = Data(bytes: self.buffer.memory.assumingMemoryBound(to: UInt8.self).advanced(by: self.offset + 4), count: Int(length)) - self.offset += 4 + Int(length) - if let string = String(data: data, encoding: .utf8) { - array.append(string) - } else { - assertionFailure() - array.append("") - } - - i += 1 - } - - return array + return decodeStringArrayRaw() } else { return [] } } + + public func decodeStringArrayRaw() -> [String] { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + self.offset += 4 + + var array: [String] = [] + array.reserveCapacity(Int(length)) + + var i: Int32 = 0 + while i < length { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + let data = Data(bytes: self.buffer.memory.assumingMemoryBound(to: UInt8.self).advanced(by: self.offset + 4), count: Int(length)) + self.offset += 4 + Int(length) + if let string = String(data: data, encoding: .utf8) { + array.append(string) + } else { + assertionFailure() + array.append("") + } + + i += 1 + } + + return array + } - public func decodeBytesArrayForKey(_ key: StaticString) -> [MemoryBuffer] { + public func decodeBytesArrayForKey(_ key: String) -> [MemoryBuffer] { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .BytesArray) { - var length: Int32 = 0 - memcpy(&length, self.buffer.memory + self.offset, 4) - self.offset += 4 - - var array: [MemoryBuffer] = [] - array.reserveCapacity(Int(length)) - - var i: Int32 = 0 - while i < length { - var length: Int32 = 0 - memcpy(&length, self.buffer.memory + self.offset, 4) - let bytes = malloc(Int(length))! - memcpy(bytes, self.buffer.memory.advanced(by: self.offset + 4), Int(length)) - array.append(MemoryBuffer(memory: bytes, capacity: Int(length), length: Int(length), freeWhenDone: true)) - self.offset += 4 + Int(length) - - i += 1 - } - - return array + return decodeBytesArrayRaw() } else { return [] } } + + func decodeBytesArrayRaw() -> [MemoryBuffer] { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + self.offset += 4 + + var array: [MemoryBuffer] = [] + array.reserveCapacity(Int(length)) + + var i: Int32 = 0 + while i < length { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + let bytes = malloc(Int(length))! + memcpy(bytes, self.buffer.memory.advanced(by: self.offset + 4), Int(length)) + array.append(MemoryBuffer(memory: bytes, capacity: Int(length), length: Int(length), freeWhenDone: true)) + self.offset += 4 + Int(length) + + i += 1 + } + + return array + } + + func decodeObjectDataArrayRaw() -> [Data] { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + self.offset += 4 + + var array: [Data] = [] + array.reserveCapacity(Int(length)) + + var i: Int32 = 0 + while i < length { + var typeHash: Int32 = 0 + memcpy(&typeHash, self.buffer.memory + self.offset, 4) + self.offset += 4 + + var objectLength: Int32 = 0 + memcpy(&objectLength, self.buffer.memory + self.offset, 4) + if objectLength < 0 || objectLength > 2 * 1024 * 1024 { + preconditionFailure() + } + + let innerBuffer = ReadBuffer(memory: self.buffer.memory + (self.offset + 4), length: Int(objectLength), freeWhenDone: false) + let innerData = innerBuffer.makeData() + self.offset += 4 + Int(objectLength) + + array.append(innerData) + + i += 1 + } + + return array + } - public func decodeOptionalDataArrayForKey(_ key: StaticString) -> [Data]? { + public func decodeOptionalDataArrayForKey(_ key: String) -> [Data]? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .BytesArray) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1228,7 +1335,7 @@ public final class PostboxDecoder { } } - public func decodeObjectArrayForKey(_ key: StaticString) -> [T] where T: PostboxCoding { + public func decodeObjectArrayForKey(_ key: String) -> [T] where T: PostboxCoding { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1271,7 +1378,7 @@ public final class PostboxDecoder { } } - public func decodeObjectArrayForKey(_ key: StaticString) -> [PostboxCoding] { + public func decodeObjectArrayForKey(_ key: String) -> [PostboxCoding] { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1314,7 +1421,7 @@ public final class PostboxDecoder { } } - public func decodeObjectDictionaryForKey(_ key: StaticString) -> [K : V] where K: PostboxCoding, K: Hashable { + public func decodeObjectDictionaryForKey(_ key: String) -> [K : V] where K: PostboxCoding, K: Hashable { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectDictionary) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1368,7 +1475,7 @@ public final class PostboxDecoder { } } - public func decodeObjectDictionaryForKey(_ key: StaticString, keyDecoder: (PostboxDecoder) -> K) -> [K : V] where K: Hashable { + public func decodeObjectDictionaryForKey(_ key: String, keyDecoder: (PostboxDecoder) -> K) -> [K : V] where K: Hashable { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectDictionary) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1425,7 +1532,7 @@ public final class PostboxDecoder { } } - public func decodeObjectDictionaryForKey(_ key: StaticString, keyDecoder: (PostboxDecoder) -> K, valueDecoder: (PostboxDecoder) -> V) -> [K : V] where K: Hashable { + public func decodeObjectDictionaryForKey(_ key: String, keyDecoder: (PostboxDecoder) -> K, valueDecoder: (PostboxDecoder) -> V) -> [K : V] where K: Hashable { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectDictionary) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1482,7 +1589,7 @@ public final class PostboxDecoder { } } - public func decodeBytesForKeyNoCopy(_ key: StaticString) -> ReadBuffer? { + public func decodeBytesForKeyNoCopy(_ key: String) -> ReadBuffer? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bytes) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1493,7 +1600,7 @@ public final class PostboxDecoder { } } - public func decodeBytesForKey(_ key: StaticString) -> ReadBuffer? { + public func decodeBytesForKey(_ key: String) -> ReadBuffer? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bytes) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1506,7 +1613,7 @@ public final class PostboxDecoder { } } - public func decodeDataForKey(_ key: StaticString) -> Data? { + public func decodeDataForKey(_ key: String) -> Data? { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bytes) { var length: Int32 = 0 memcpy(&length, self.buffer.memory + self.offset, 4) @@ -1520,4 +1627,23 @@ public final class PostboxDecoder { return nil } } + + public func decode(_ type: T.Type, forKey key: String) -> T? { + if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Object) { + var typeHash: Int32 = 0 + memcpy(&typeHash, self.buffer.memory + self.offset, 4) + self.offset += 4 + + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + + let innerBuffer = ReadBuffer(memory: self.buffer.memory + (self.offset + 4), length: Int(length), freeWhenDone: false) + let innerData = innerBuffer.makeData() + self.offset += 4 + Int(length) + + return try? AdaptedPostboxDecoder().decode(T.self, from: innerData) + } else { + return nil + } + } } diff --git a/submodules/Postbox/Sources/PeerChatInterfaceState.swift b/submodules/Postbox/Sources/PeerChatInterfaceState.swift index eb395081d0..be04895995 100644 --- a/submodules/Postbox/Sources/PeerChatInterfaceState.swift +++ b/submodules/Postbox/Sources/PeerChatInterfaceState.swift @@ -1,5 +1,5 @@ -public protocol PeerChatListEmbeddedInterfaceState: PostboxCoding { +public protocol PeerChatListEmbeddedInterfaceState: Codable { var timestamp: Int32 { get } func isEqual(to: PeerChatListEmbeddedInterfaceState) -> Bool diff --git a/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxDecoder.swift b/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxDecoder.swift new file mode 100644 index 0000000000..69d2074ced --- /dev/null +++ b/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxDecoder.swift @@ -0,0 +1,125 @@ +import Foundation + +final public class AdaptedPostboxDecoder { + enum ContentType { + case object + case int32Array + case int64Array + case objectArray + case stringArray + case dataArray + } + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + return try self.decode(type, from: data, contentType: .object) + } + + func decode(_ type: T.Type, from data: Data, contentType: ContentType) throws -> T where T : Decodable { + let decoder = _AdaptedPostboxDecoder(data: data, contentType: contentType) + return try T(from: decoder) + } +} + +extension AdaptedPostboxDecoder.ContentType { + init?(valueType: ValueType) { + switch valueType { + case .Int32: + return nil + case .Int64: + return nil + case .Bool: + return nil + case .Double: + return nil + case .String: + return nil + case .Object: + self = .object + case .Int32Array: + self = .int32Array + case .Int64Array: + self = .int64Array + case .ObjectArray: + self = .objectArray + case .ObjectDictionary: + return nil + case .Bytes: + return nil + case .Nil: + return nil + case .StringArray: + self = .stringArray + case .BytesArray: + self = .dataArray + } + } +} + +final class _AdaptedPostboxDecoder { + var codingPath: [CodingKey] = [] + + var userInfo: [CodingUserInfoKey : Any] = [:] + + var container: AdaptedPostboxDecodingContainer? + + fileprivate let data: Data + fileprivate let contentType: AdaptedPostboxDecoder.ContentType + + init(data: Data, contentType: AdaptedPostboxDecoder.ContentType) { + self.data = data + self.contentType = contentType + } +} + +extension _AdaptedPostboxDecoder: Decoder { + fileprivate func assertCanCreateContainer() { + precondition(self.container == nil) + } + + func container(keyedBy type: Key.Type) -> KeyedDecodingContainer where Key : CodingKey { + assertCanCreateContainer() + + let container = KeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo) + self.container = container + + return KeyedDecodingContainer(container) + } + + func unkeyedContainer() -> UnkeyedDecodingContainer { + assertCanCreateContainer() + + let decoder = PostboxDecoder(buffer: MemoryBuffer(data: self.data)) + + var content: UnkeyedContainer.Content? + switch self.contentType { + case .object: + preconditionFailure() + case .int32Array: + content = .int32Array(decoder.decodeInt32ArrayRaw()) + case .int64Array: + content = .int64Array(decoder.decodeInt64ArrayRaw()) + case .objectArray: + content = .objectArray(decoder.decodeObjectDataArrayRaw()) + case .stringArray: + content = .stringArray(decoder.decodeStringArrayRaw()) + case .dataArray: + content = .dataArray(decoder.decodeBytesArrayRaw().map { $0.makeData() }) + } + + if let content = content { + let container = UnkeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, content: content) + self.container = container + + return container + } else { + preconditionFailure() + } + } + + func singleValueContainer() -> SingleValueDecodingContainer { + preconditionFailure() + } +} + +protocol AdaptedPostboxDecodingContainer: AnyObject { +} diff --git a/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxKeyedDecodingContainer.swift b/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxKeyedDecodingContainer.swift new file mode 100644 index 0000000000..6d4f5e9faa --- /dev/null +++ b/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxKeyedDecodingContainer.swift @@ -0,0 +1,103 @@ +import Foundation + +extension _AdaptedPostboxDecoder { + final class KeyedContainer where Key: CodingKey { + var codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey: Any] + let decoder: PostboxDecoder + + init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { + self.codingPath = codingPath + self.userInfo = userInfo + self.decoder = PostboxDecoder(buffer: MemoryBuffer(data: data)) + } + } +} + +private func decodingErrorBreakpoint() { + #if DEBUG + print("Decoding error. Install a breakpoint at decodingErrorBreakpoint to debug.") + #endif +} + +extension _AdaptedPostboxDecoder.KeyedContainer: KeyedDecodingContainerProtocol { + var allKeys: [Key] { + preconditionFailure() + } + + func contains(_ key: Key) -> Bool { + return self.decoder.containsKey(key.stringValue) + } + + func decodeNil(forKey key: Key) throws -> Bool { + return self.decoder.decodeNilForKey(key.stringValue) + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { + if let (data, valueType) = self.decoder.decodeObjectDataForKey(key.stringValue) { + if let mappedType = AdaptedPostboxDecoder.ContentType(valueType: valueType) { + return try AdaptedPostboxDecoder().decode(T.self, from: data, contentType: mappedType) + } else { + decodingErrorBreakpoint() + throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: "")) + } + } else { + decodingErrorBreakpoint() + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: "")) + } + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + if let value = self.decoder.decodeOptionalInt32ForKey(key.stringValue) { + return value + } else { + decodingErrorBreakpoint() + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: "")) + } + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + if let value = self.decoder.decodeOptionalInt64ForKey(key.stringValue) { + return value + } else { + decodingErrorBreakpoint() + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: "")) + } + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + if let value = self.decoder.decodeOptionalBoolForKey(key.stringValue) { + return value + } else { + decodingErrorBreakpoint() + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: "")) + } + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + if let value = self.decoder.decodeOptionalStringForKey(key.stringValue) { + return value + } else { + decodingErrorBreakpoint() + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath + [key], debugDescription: "")) + } + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + preconditionFailure() + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + preconditionFailure() + } + + func superDecoder() throws -> Decoder { + preconditionFailure() + } + + func superDecoder(forKey key: Key) throws -> Decoder { + preconditionFailure() + } +} + +extension _AdaptedPostboxDecoder.KeyedContainer: AdaptedPostboxDecodingContainer {} diff --git a/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxSingleValueDecodingContainer.swift b/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxSingleValueDecodingContainer.swift new file mode 100644 index 0000000000..5f21bc88f7 --- /dev/null +++ b/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxSingleValueDecodingContainer.swift @@ -0,0 +1,82 @@ +import Foundation + +extension _AdaptedPostboxDecoder { + final class SingleValueContainer { + var codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey: Any] + + + init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { + self.codingPath = codingPath + self.userInfo = userInfo + } + } +} + +extension _AdaptedPostboxDecoder.SingleValueContainer: SingleValueDecodingContainer { + func decodeNil() -> Bool { + preconditionFailure() + } + + func decode(_ type: Bool.Type) throws -> Bool { + preconditionFailure() + } + + func decode(_ type: String.Type) throws -> String { + preconditionFailure() + } + + func decode(_ type: Double.Type) throws -> Double { + preconditionFailure() + } + + func decode(_ type: Float.Type) throws -> Float { + preconditionFailure() + } + + func decode(_ type: Int.Type) throws -> Int { + preconditionFailure() + } + + func decode(_ type: Int8.Type) throws -> Int8 { + preconditionFailure() + } + + func decode(_ type: Int16.Type) throws -> Int16 { + preconditionFailure() + } + + func decode(_ type: Int32.Type) throws -> Int32 { + preconditionFailure() + } + + func decode(_ type: Int64.Type) throws -> Int64 { + preconditionFailure() + } + + func decode(_ type: UInt.Type) throws -> UInt { + preconditionFailure() + } + + func decode(_ type: UInt8.Type) throws -> UInt8 { + preconditionFailure() + } + + func decode(_ type: UInt16.Type) throws -> UInt16 { + preconditionFailure() + } + + func decode(_ type: UInt32.Type) throws -> UInt32 { + preconditionFailure() + } + + func decode(_ type: UInt64.Type) throws -> UInt64 { + preconditionFailure() + } + + func decode(_ type: T.Type) throws -> T where T : Decodable { + preconditionFailure() + } +} + +extension _AdaptedPostboxDecoder.SingleValueContainer: AdaptedPostboxDecodingContainer {} diff --git a/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxUnkeyedDecodingContainer.swift b/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxUnkeyedDecodingContainer.swift new file mode 100644 index 0000000000..27bdeddcfd --- /dev/null +++ b/submodules/Postbox/Sources/Utils/Decoder/AdaptedPostboxUnkeyedDecodingContainer.swift @@ -0,0 +1,129 @@ +import Foundation + +extension _AdaptedPostboxDecoder { + final class UnkeyedContainer { + enum Content { + case int32Array([Int32]) + case int64Array([Int64]) + case objectArray([Data]) + case stringArray([String]) + case dataArray([Data]) + + var count: Int { + switch self { + case let .int32Array(array): + return array.count + case let .int64Array(array): + return array.count + case let .objectArray(array): + return array.count + case let .stringArray(array): + return array.count + case let .dataArray(array): + return array.count + } + } + } + + let codingPath: [CodingKey] + let userInfo: [CodingUserInfoKey: Any] + let content: Content + + var count: Int? { + return self.content.count + } + + var isAtEnd: Bool { + return self.currentIndex >= self.content.count + } + + fileprivate var _currentIndex: Int = 0 + + var currentIndex: Int { + return self._currentIndex + } + + init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey: Any], content: Content) { + self.codingPath = codingPath + self.userInfo = userInfo + self.content = content + } + } +} + +extension _AdaptedPostboxDecoder.UnkeyedContainer: UnkeyedDecodingContainer { + func decodeNil() throws -> Bool { + preconditionFailure() + } + + func decode(_ type: T.Type) throws -> T where T : Decodable { + if type == Data.self { + switch self.content { + case let .dataArray(array): + let index = self._currentIndex + self._currentIndex += 1 + return array[index] as! T + default: + throw DecodingError.typeMismatch(Data.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: "")) + } + } else { + switch self.content { + case let .objectArray(array): + let index = self._currentIndex + self._currentIndex += 1 + + let data = array[index] + return try AdaptedPostboxDecoder().decode(T.self, from: data) + default: + throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: "")) + } + } + } + + func decode(_ type: Int32.Type) throws -> Int32 { + switch self.content { + case let .int32Array(array): + let index = self._currentIndex + self._currentIndex += 1 + return array[index] + default: + throw DecodingError.typeMismatch(Int32.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: "")) + } + } + + func decode(_ type: Int64.Type) throws -> Int64 { + switch self.content { + case let .int64Array(array): + let index = self._currentIndex + self._currentIndex += 1 + return array[index] + default: + throw DecodingError.typeMismatch(Int64.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: "")) + } + } + + func decode(_ type: String.Type) throws -> String { + switch self.content { + case let .stringArray(array): + let index = self._currentIndex + self._currentIndex += 1 + return array[index] + default: + throw DecodingError.typeMismatch(String.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: "")) + } + } + + func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + preconditionFailure() + } + + func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + preconditionFailure() + } + + func superDecoder() throws -> Decoder { + preconditionFailure() + } +} + +extension _AdaptedPostboxDecoder.UnkeyedContainer: AdaptedPostboxDecodingContainer {} diff --git a/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxEncoder.swift b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxEncoder.swift new file mode 100644 index 0000000000..14bf249f28 --- /dev/null +++ b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxEncoder.swift @@ -0,0 +1,62 @@ +import Foundation +import MurMurHash32 + +public class AdaptedPostboxEncoder { + func encode(_ value: Encodable) throws -> Data { + let typeHash: Int32 = murMurHashString32("\(type(of: value))") + + let encoder = _AdaptedPostboxEncoder(typeHash: typeHash) + try value.encode(to: encoder) + return encoder.makeData().0 + } +} + +final class _AdaptedPostboxEncoder { + var codingPath: [CodingKey] = [] + + var userInfo: [CodingUserInfoKey : Any] = [:] + + let typeHash: Int32 + + fileprivate var container: AdaptedPostboxEncodingContainer? + + init(typeHash: Int32) { + self.typeHash = typeHash + } + + func makeData() -> (Data, ValueType) { + return self.container!.makeData() + } +} + +extension _AdaptedPostboxEncoder: Encoder { + fileprivate func assertCanCreateContainer() { + precondition(self.container == nil) + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + assertCanCreateContainer() + + let container = KeyedContainer(codingPath: self.codingPath, userInfo: self.userInfo, typeHash: self.typeHash) + self.container = container + + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + assertCanCreateContainer() + + let container = UnkeyedContainer(codingPath: self.codingPath, userInfo: self.userInfo) + self.container = container + + return container + } + + func singleValueContainer() -> SingleValueEncodingContainer { + preconditionFailure() + } +} + +protocol AdaptedPostboxEncodingContainer: AnyObject { + func makeData() -> (Data, ValueType) +} diff --git a/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxKeyedEncodingContainer.swift b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxKeyedEncodingContainer.swift new file mode 100644 index 0000000000..8d0401ceb1 --- /dev/null +++ b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxKeyedEncodingContainer.swift @@ -0,0 +1,88 @@ +import Foundation +import MurMurHash32 + +extension _AdaptedPostboxEncoder { + final class KeyedContainer where Key: CodingKey { + var codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey: Any] + let typeHash: Int32 + + let encoder: PostboxEncoder + + init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], typeHash: Int32) { + self.codingPath = codingPath + self.userInfo = userInfo + self.typeHash = typeHash + + self.encoder = PostboxEncoder() + } + + func makeData() -> (Data, ValueType) { + let buffer = WriteBuffer() + + var typeHash: Int32 = self.typeHash + buffer.write(&typeHash, offset: 0, length: 4) + + let data = self.encoder.makeData() + + var length: Int32 = Int32(data.count) + buffer.write(&length, offset: 0, length: 4) + buffer.write(data) + + return (buffer.makeData(), .Object) + } + } +} + +extension _AdaptedPostboxEncoder.KeyedContainer: KeyedEncodingContainerProtocol { + func encode(_ value: T, forKey key: Key) throws where T : Encodable { + let typeHash: Int32 = murMurHashString32("\(type(of: value))") + let innerEncoder = _AdaptedPostboxEncoder(typeHash: typeHash) + try! value.encode(to: innerEncoder) + + let (data, valueType) = innerEncoder.makeData() + self.encoder.encodeInnerObjectData(data, valueType: valueType, forKey: key.stringValue) + } + + func encodeNil(forKey key: Key) throws { + self.encoder.encodeNil(forKey: key.stringValue) + } + + func encode(_ value: Int32, forKey key: Key) throws { + self.encoder.encodeInt32(value, forKey: key.stringValue) + } + + func encode(_ value: Int64, forKey key: Key) throws { + self.encoder.encodeInt64(value, forKey: key.stringValue) + } + + func encode(_ value: Bool, forKey key: Key) throws { + self.encoder.encodeBool(value, forKey: key.stringValue) + } + + func encode(_ value: Double, forKey key: Key) throws { + self.encoder.encodeDouble(value, forKey: key.stringValue) + } + + func encode(_ value: String, forKey key: Key) throws { + self.encoder.encodeString(value, forKey: key.stringValue) + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + preconditionFailure() + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { + preconditionFailure() + } + + func superEncoder() -> Encoder { + preconditionFailure() + } + + func superEncoder(forKey key: Key) -> Encoder { + preconditionFailure() + } +} + +extension _AdaptedPostboxEncoder.KeyedContainer: AdaptedPostboxEncodingContainer {} diff --git a/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxSingleValueEncodingContainer.swift b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxSingleValueEncodingContainer.swift new file mode 100644 index 0000000000..7dd39a4ffa --- /dev/null +++ b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxSingleValueEncodingContainer.swift @@ -0,0 +1,85 @@ +import Foundation + +extension _AdaptedPostboxEncoder { + final class SingleValueContainer { + var codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey: Any] + + init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { + self.codingPath = codingPath + self.userInfo = userInfo + } + } +} + +extension _AdaptedPostboxEncoder.SingleValueContainer: SingleValueEncodingContainer { + func encodeNil() throws { + preconditionFailure() + } + + func encode(_ value: Bool) throws { + preconditionFailure() + } + + func encode(_ value: String) throws { + preconditionFailure() + } + + func encode(_ value: Double) throws { + preconditionFailure() + } + + func encode(_ value: Float) throws { + preconditionFailure() + } + + func encode(_ value: Int) throws { + preconditionFailure() + } + + func encode(_ value: Int8) throws { + preconditionFailure() + } + + func encode(_ value: Int16) throws { + preconditionFailure() + } + + func encode(_ value: Int32) throws { + preconditionFailure() + } + + func encode(_ value: Int64) throws { + preconditionFailure() + } + + func encode(_ value: UInt) throws { + preconditionFailure() + } + + func encode(_ value: UInt8) throws { + preconditionFailure() + } + + func encode(_ value: UInt16) throws { + preconditionFailure() + } + + func encode(_ value: UInt32) throws { + preconditionFailure() + } + + func encode(_ value: UInt64) throws { + preconditionFailure() + } + + func encode(_ value: T) throws where T : Encodable { + preconditionFailure() + } +} + +extension _AdaptedPostboxEncoder.SingleValueContainer: AdaptedPostboxEncodingContainer { + func makeData() -> (Data, ValueType) { + preconditionFailure() + } +} diff --git a/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift new file mode 100644 index 0000000000..8efa257466 --- /dev/null +++ b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift @@ -0,0 +1,156 @@ +import Foundation +import MurMurHash32 + +extension _AdaptedPostboxEncoder { + final class UnkeyedContainer { + fileprivate enum Item { + case int32(Int32) + case int64(Int64) + case string(String) + case object(Data) + case data(Data) + } + + let codingPath: [CodingKey] + let userInfo: [CodingUserInfoKey: Any] + + fileprivate var items: [Item] = [] + + var count: Int { + return self.items.count + } + + init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { + self.codingPath = codingPath + self.userInfo = userInfo + } + + func makeData() -> (Data, ValueType) { + if self.items.isEmpty { + let buffer = WriteBuffer() + + var length: Int32 = Int32(self.items.count) + buffer.write(&length, offset: 0, length: 4) + + return (buffer.makeData(), .Int32Array) + } else if self.items.allSatisfy({ if case .int32 = $0 { return true } else { return false } }) { + let buffer = WriteBuffer() + + var length: Int32 = Int32(self.items.count) + buffer.write(&length, offset: 0, length: 4) + + for case .int32(var value) in self.items { + buffer.write(&value, offset: 0, length: 4) + } + + return (buffer.makeData(), .Int32Array) + } else if self.items.allSatisfy({ if case .int64 = $0 { return true } else { return false } }) { + let buffer = WriteBuffer() + + var length: Int32 = Int32(self.items.count) + buffer.write(&length, offset: 0, length: 4) + + for case .int64(var value) in self.items { + buffer.write(&value, offset: 0, length: 4) + } + + return (buffer.makeData(), .Int64Array) + } else if self.items.allSatisfy({ if case .string = $0 { return true } else { return false } }) { + let buffer = WriteBuffer() + + var length: Int32 = Int32(self.items.count) + buffer.write(&length, offset: 0, length: 4) + + for case .string(let value) in self.items { + let data = value.data(using: .utf8, allowLossyConversion: true) ?? (String("").data(using: .utf8)!) + var valueLength: Int32 = Int32(data.count) + buffer.write(&valueLength, offset: 0, length: 4) + buffer.write(data) + } + + return (buffer.makeData(), .StringArray) + } else if self.items.allSatisfy({ if case .object = $0 { return true } else { return false } }) { + let buffer = WriteBuffer() + + var length: Int32 = Int32(self.items.count) + buffer.write(&length, offset: 0, length: 4) + + for case .object(let data) in self.items { + buffer.write(data) + } + + return (buffer.makeData(), .ObjectArray) + } else if self.items.allSatisfy({ if case .data = $0 { return true } else { return false } }) { + let buffer = WriteBuffer() + + var length: Int32 = Int32(self.items.count) + buffer.write(&length, offset: 0, length: 4) + + for case .data(let data) in self.items { + var valueLength: Int32 = Int32(data.count) + buffer.write(&valueLength, offset: 0, length: 4) + buffer.write(data) + } + + return (buffer.makeData(), .BytesArray) + } else { + preconditionFailure() + } + } + } +} + +extension _AdaptedPostboxEncoder.UnkeyedContainer: UnkeyedEncodingContainer { + func encodeNil() throws { + preconditionFailure() + } + + func encode(_ value: T) throws where T : Encodable { + let typeHash: Int32 = murMurHashString32("\(type(of: value))") + + let innerEncoder = _AdaptedPostboxEncoder(typeHash: typeHash) + try! value.encode(to: innerEncoder) + + let (data, _) = innerEncoder.makeData() + + let buffer = WriteBuffer() + + buffer.write(data) + + self.items.append(.object(buffer.makeData())) + } + + func encode(_ value: Int32) throws { + self.items.append(.int32(value)) + } + + func encode(_ value: Int64) throws { + self.items.append(.int64(value)) + } + + func encode(_ value: String) throws { + self.items.append(.string(value)) + } + + func encode(_ value: Data) throws { + self.items.append(.data(value)) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { + preconditionFailure() + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + preconditionFailure() + } + + func superEncoder() -> Encoder { + preconditionFailure() + } +} + +extension _AdaptedPostboxEncoder.UnkeyedContainer: AdaptedPostboxEncodingContainer { + func makeData() -> Data { + preconditionFailure() + } +} diff --git a/submodules/Postbox/Sources/Utils/PostboxCodingAdapter.swift b/submodules/Postbox/Sources/Utils/PostboxCodingAdapter.swift new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/submodules/Postbox/Sources/Utils/PostboxCodingAdapter.swift @@ -0,0 +1 @@ + diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 06c5964520..6ae77fdf6d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -478,5 +478,35 @@ public extension TelegramEngine { public func updatePeerDescription(peerId: PeerId, description: String?) -> Signal { return _internal_updatePeerDescription(account: self.account, peerId: peerId, description: description) } + + public func getNextUnreadChannel(peerId: PeerId) -> Signal { + return self.account.postbox.transaction { transaction -> EnginePeer? in + var peers: [RenderedPeer] = [] + peers.append(contentsOf: transaction.getTopChatListEntries(groupId: .root, count: 100)) + peers.append(contentsOf: transaction.getTopChatListEntries(groupId: Namespaces.PeerGroup.archive, count: 100)) + + var results: [(EnginePeer, Int32)] = [] + + for peer in peers { + guard let channel = peer.chatMainPeer as? TelegramChannel, case .broadcast = channel.info else { + continue + } + if channel.id == peerId { + continue + } + guard let readState = transaction.getCombinedPeerReadState(channel.id), readState.count != 0 else { + continue + } + guard let topMessageIndex = transaction.getTopPeerMessageIndex(peerId: channel.id) else { + continue + } + results.append((EnginePeer(channel), topMessageIndex.timestamp)) + } + + results.sort(by: { $0.1 > $1.1 }) + + return results.first?.0 + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Utils/StringCodingKey.swift b/submodules/TelegramCore/Sources/TelegramEngine/Utils/StringCodingKey.swift new file mode 100644 index 0000000000..07cd9767c3 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Utils/StringCodingKey.swift @@ -0,0 +1,24 @@ + +public struct StringCodingKey: CodingKey, ExpressibleByStringLiteral { + public var stringValue: String + + public init?(stringValue: String) { + self.stringValue = stringValue + } + + public init(_ stringValue: String) { + self.stringValue = stringValue + } + + public init(stringLiteral: String) { + self.stringValue = stringLiteral + } + + public var intValue: Int? { + return nil + } + + public init?(intValue: Int) { + return nil + } +} diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 586fc7fb26..7e22f4728c 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -238,6 +238,7 @@ swift_library( "//submodules/ImportStickerPackUI:ImportStickerPackUI", "//submodules/GradientBackground:GradientBackground", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", + "//submodules/ComponentFlow:ComponentFlow", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift index c26dcc45f3..48169b0556 100644 --- a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift @@ -52,4 +52,33 @@ final class ChatAvatarNavigationNode: ASDisplayNode { func onLayout() { } + + final class SnapshotState { + fileprivate let snapshotView: UIView + + fileprivate init(snapshotView: UIView) { + self.snapshotView = snapshotView + } + } + + func prepareSnapshotState() -> SnapshotState { + let snapshotView = self.avatarNode.view.snapshotView(afterScreenUpdates: false)! + return SnapshotState( + snapshotView: snapshotView + ) + } + + func animateFromSnapshot(_ snapshotState: SnapshotState) { + self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true) + + snapshotState.snapshotView.frame = self.frame + self.containerNode.view.addSubview(snapshotState.snapshotView) + + let snapshotView = snapshotState.snapshotView + snapshotState.snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5c6ac8f1c9..ba20f43c1c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -454,6 +454,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private var importStateDisposable: Disposable? + + private var nextChannelToReadDisposable: Disposable? public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) { let _ = ChatControllerCount.modify { value in @@ -3295,6 +3297,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedHasBotCommands(hasBotCommands).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages) .updatedAutoremoveTimeout(autoremoveTimeout) }) + + if let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { + if strongSelf.nextChannelToReadDisposable == nil { + strongSelf.nextChannelToReadDisposable = (strongSelf.context.engine.peers.getNextUnreadChannel(peerId: channel.id) + |> deliverOnMainQueue + |> then(.complete() |> delay(1.0, queue: .mainQueue())) + |> restart).start(next: { nextPeer in + guard let strongSelf = self else { + return + } + + strongSelf.chatDisplayNode.historyNode.offerNextChannelToRead = true + strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer + }) + } + } + if !strongSelf.didSetChatLocationInfoReady { strongSelf.didSetChatLocationInfoReady = true strongSelf._chatLocationInfoReady.set(.single(true)) @@ -3867,6 +3886,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.selectAddMemberDisposable.dispose() self.addMemberDisposable.dispose() self.importStateDisposable?.dispose() + self.nextChannelToReadDisposable?.dispose() } public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { @@ -7011,9 +7031,30 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }) + + self.chatDisplayNode.historyNode.openNextChannelToRead = { [weak self] peer in + guard let strongSelf = self else { + return + } + if let navigationController = strongSelf.effectiveNavigationController { + let snapshotState = strongSelf.chatDisplayNode.prepareSnapshotState( + titleViewSnapshotState: strongSelf.chatTitleView?.prepareSnapshotState(), + avatarSnapshotState: (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.prepareSnapshotState() + ) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: false, completion: { nextController in + (nextController as! ChatControllerImpl).animateFromPreviousController(snapshotState: snapshotState) + })) + } + } self.displayNodeDidLoad() } + + private var storedAnimateFromSnapshotState: ChatControllerNode.SnapshotState? + + private func animateFromPreviousController(snapshotState: ChatControllerNode.SnapshotState) { + self.storedAnimateFromSnapshotState = snapshotState + } override public func viewWillAppear(_ animated: Bool) { #if DEBUG @@ -7067,7 +7108,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.didAppear = true - self.chatDisplayNode.historyNode.preloadPages = true self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in return a && b @@ -7415,6 +7455,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return state.updatedInputMode({ _ in .text }) }) } + + if let snapshotState = self.storedAnimateFromSnapshotState { + self.storedAnimateFromSnapshotState = nil + + if let titleViewSnapshotState = snapshotState.titleViewSnapshotState { + self.chatTitleView?.animateFromSnapshot(titleViewSnapshotState) + } + if let avatarSnapshotState = snapshotState.avatarSnapshotState { + (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshotState) + } + self.chatDisplayNode.animateFromSnapshot(snapshotState) + } } override public func viewWillDisappear(_ animated: Bool) { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index d5b569a1dd..34fc8fcffc 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2498,4 +2498,86 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return false } } + + final class SnapshotState { + fileprivate let historySnapshotState: ChatHistoryListNode.SnapshotState + let titleViewSnapshotState: ChatTitleView.SnapshotState? + let avatarSnapshotState: ChatAvatarNavigationNode.SnapshotState? + let navigationButtonsSnapshotState: ChatHistoryNavigationButtons.SnapshotState + let titleAccessoryPanelSnapshot: UIView? + let navigationBarHeight: CGFloat + + fileprivate init( + historySnapshotState: ChatHistoryListNode.SnapshotState, + titleViewSnapshotState: ChatTitleView.SnapshotState?, + avatarSnapshotState: ChatAvatarNavigationNode.SnapshotState?, + navigationButtonsSnapshotState: ChatHistoryNavigationButtons.SnapshotState, + titleAccessoryPanelSnapshot: UIView?, + navigationBarHeight: CGFloat + ) { + self.historySnapshotState = historySnapshotState + self.titleViewSnapshotState = titleViewSnapshotState + self.avatarSnapshotState = avatarSnapshotState + self.navigationButtonsSnapshotState = navigationButtonsSnapshotState + self.titleAccessoryPanelSnapshot = titleAccessoryPanelSnapshot + self.navigationBarHeight = navigationBarHeight + } + } + + func prepareSnapshotState( + titleViewSnapshotState: ChatTitleView.SnapshotState?, + avatarSnapshotState: ChatAvatarNavigationNode.SnapshotState? + ) -> SnapshotState { + var titleAccessoryPanelSnapshot: UIView? + if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let snapshot = titleAccessoryPanelNode.view.snapshotView(afterScreenUpdates: false) { + snapshot.frame = titleAccessoryPanelNode.frame + titleAccessoryPanelSnapshot = snapshot + } + return SnapshotState( + historySnapshotState: self.historyNode.prepareSnapshotState(), + titleViewSnapshotState: titleViewSnapshotState, + avatarSnapshotState: avatarSnapshotState, + navigationButtonsSnapshotState: self.navigateButtons.prepareSnapshotState(), + titleAccessoryPanelSnapshot: titleAccessoryPanelSnapshot, + navigationBarHeight: self.navigationBar?.backgroundNode.bounds.height ?? 0.0 + ) + } + + func animateFromSnapshot(_ snapshotState: SnapshotState) { + self.historyNode.animateFromSnapshot(snapshotState.historySnapshotState) + self.navigateButtons.animateFromSnapshot(snapshotState.navigationButtonsSnapshotState) + + if let titleAccessoryPanelSnapshot = snapshotState.titleAccessoryPanelSnapshot { + self.titleAccessoryPanelContainer.view.addSubview(titleAccessoryPanelSnapshot) + if let _ = self.titleAccessoryPanelNode { + titleAccessoryPanelSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak titleAccessoryPanelSnapshot] _ in + titleAccessoryPanelSnapshot?.removeFromSuperview() + }) + titleAccessoryPanelSnapshot.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -10.0), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + } else { + titleAccessoryPanelSnapshot.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -titleAccessoryPanelSnapshot.bounds.height), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak titleAccessoryPanelSnapshot] _ in + titleAccessoryPanelSnapshot?.removeFromSuperview() + }) + } + } + + if let titleAccessoryPanelNode = self.titleAccessoryPanelNode { + if let _ = snapshotState.titleAccessoryPanelSnapshot { + titleAccessoryPanelNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true) + titleAccessoryPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: true) + } else { + titleAccessoryPanelNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -titleAccessoryPanelNode.bounds.height), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true) + } + } + + if let navigationBar = self.navigationBar { + let currentFrame = navigationBar.backgroundNode.frame + var previousFrame = currentFrame + previousFrame.size.height = snapshotState.navigationBarHeight + if previousFrame != currentFrame { + navigationBar.backgroundNode.update(size: previousFrame.size, transition: .immediate) + navigationBar.backgroundNode.update(size: currentFrame.size, transition: .animated(duration: 0.5, curve: .spring)) + } + } + } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index c3dad67922..828771efac 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -17,6 +17,7 @@ import ListMessageItem import AccountContext import ChatInterfaceState import ChatListUI +import ComponentFlow extension ChatReplyThreadMessage { var effectiveTopId: MessageId { @@ -546,6 +547,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let topVisibleMessageRange = ValuePromise(nil, ignoreRepeated: true) var isSelectionGestureEnabled = true + + private var overscrollView: ComponentHostView? + var nextChannelToRead: EnginePeer? + var offerNextChannelToRead: Bool = false + private var currentOverscrollExpandProgress: CGFloat = 0.0 + private var feedback: HapticFeedback? + var openNextChannelToRead: ((EnginePeer) -> Void)? private let clientId: Atomic @@ -577,6 +585,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { nextClientId += 1 super.init() + + self.clipsToBounds = false self.accessibilityPageScrolledString = { [weak self] row, count in if let strongSelf = self { @@ -617,7 +627,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - self.preloadPages = false + self.preloadPages = true switch self.mode { case .bubbles: self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) @@ -1115,11 +1125,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if strongSelf.tagMask == nil { var atBottom = false + var offsetFromBottom: CGFloat? switch offset { case let .known(offsetValue): if offsetValue.isLessThanOrEqualTo(0.0) { atBottom = true + offsetFromBottom = offsetValue } + //print("offsetValue: \(offsetValue)") default: break } @@ -1130,6 +1143,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf.isScrollAtBottomPositionUpdated?() } + + strongSelf.maybeUpdateOverscrollAction(offset: offsetFromBottom) } } } @@ -1150,10 +1165,22 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self?.isInteractivelyScrollingPromise.set(true) self?.beganDragging?() } + + self.endedInteractiveDragging = { [weak self] in + guard let strongSelf = self else { + return + } + if let channel = strongSelf.nextChannelToRead, strongSelf.currentOverscrollExpandProgress >= 0.99 { + strongSelf.openNextChannelToRead?(channel) + } + } self.didEndScrolling = { [weak self] in - self?.isInteractivelyScrollingValue = false - self?.isInteractivelyScrollingPromise.set(false) + guard let strongSelf = self else { + return + } + strongSelf.isInteractivelyScrollingValue = false + strongSelf.isInteractivelyScrollingPromise.set(false) } let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) @@ -1177,6 +1204,64 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) { self.loadStateUpdated = f } + + private func maybeUpdateOverscrollAction(offset: CGFloat?) { + if let offset = offset, offset < 0.0, self.offerNextChannelToRead { + let overscrollView: ComponentHostView + if let current = self.overscrollView { + overscrollView = current + } else { + overscrollView = ComponentHostView() + overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + self.overscrollView = overscrollView + self.view.addSubview(overscrollView) + } + + let expandProgress: CGFloat = min(1.0, max(-offset, 0.0) / 110.0) + + let text: String + if self.nextChannelToRead != nil { + if expandProgress >= 0.99 { + //TODO:localize + text = "Release to go to the next unread channel" + } else { + text = "Swipe up to go to the next unread channel" + } + + let previousType = self.currentOverscrollExpandProgress >= 0.99 + let currentType = expandProgress >= 0.99 + + if previousType != currentType { + if self.feedback == nil { + self.feedback = HapticFeedback() + } + self.feedback?.tap() + } + + self.currentOverscrollExpandProgress = expandProgress + } else { + text = "You have no unread channels" + } + + let overscrollSize = overscrollView.update( + transition: .immediate, + component: AnyComponent(ChatOverscrollControl( + text: text, + backgroundColor: selectDateFillStaticColor(theme: self.currentPresentationData.theme.theme, wallpaper: self.currentPresentationData.theme.wallpaper), + foregroundColor: bubbleVariableColor(variableColor: self.currentPresentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: self.currentPresentationData.theme.wallpaper), + peer: self.nextChannelToRead, + context: self.context, + expandProgress: expandProgress + )), + environment: {}, + containerSize: CGSize(width: self.bounds.width, height: 200.0) + ) + overscrollView.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - overscrollSize.width) / 2.0), y: -offset + self.insets.top - overscrollSize.height - 10.0), size: overscrollSize) + } else if let overscrollView = self.overscrollView { + self.overscrollView = nil + overscrollView.removeFromSuperview() + } + } func refreshPollActionsForVisibleMessages() { let _ = self.clientId.swap(nextClientId) @@ -2324,4 +2409,76 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } var animationCorrelationMessageFound: ((ChatMessageItemView, Int64?) -> Void)? + + final class SnapshotState { + fileprivate let snapshotTopInset: CGFloat + fileprivate let snapshotBottomInset: CGFloat + fileprivate let snapshotView: UIView + + fileprivate init( + snapshotTopInset: CGFloat, + snapshotBottomInset: CGFloat, + snapshotView: UIView + ) { + self.snapshotTopInset = snapshotTopInset + self.snapshotBottomInset = snapshotBottomInset + self.snapshotView = snapshotView + } + } + + func prepareSnapshotState() -> SnapshotState { + var snapshotTopInset: CGFloat = 0.0 + var snapshotBottomInset: CGFloat = 0.0 + self.forEachItemNode { itemNode in + let topOverflow = itemNode.frame.maxY - self.bounds.height + snapshotTopInset = max(snapshotTopInset, topOverflow) + + if itemNode.frame.minY < 0.0 { + snapshotBottomInset = max(snapshotBottomInset, -itemNode.frame.minY) + } + } + let snapshotView = self.view.snapshotView(afterScreenUpdates: false)! + + let currentSnapshotView = self.view.snapshotView(afterScreenUpdates: false)! + currentSnapshotView.frame = self.view.bounds + if let sublayers = self.layer.sublayers { + for sublayer in sublayers { + sublayer.isHidden = true + } + } + self.view.addSubview(currentSnapshotView) + + return SnapshotState( + snapshotTopInset: snapshotTopInset, + snapshotBottomInset: snapshotBottomInset, + snapshotView: snapshotView + ) + } + + func animateFromSnapshot(_ snapshotState: SnapshotState) { + var snapshotTopInset: CGFloat = 0.0 + var snapshotBottomInset: CGFloat = 0.0 + self.forEachItemNode { itemNode in + let topOverflow = itemNode.frame.maxY - self.bounds.height + snapshotTopInset = max(snapshotTopInset, topOverflow) + + if itemNode.frame.minY < 0.0 { + snapshotBottomInset = max(snapshotBottomInset, -itemNode.frame.minY) + } + } + + let snapshotParentView = UIView() + snapshotParentView.addSubview(snapshotState.snapshotView) + snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + snapshotParentView.frame = self.view.frame + + snapshotState.snapshotView.frame = snapshotParentView.bounds + self.view.superview?.insertSubview(snapshotParentView, belowSubview: self.view) + + snapshotParentView.layer.animatePosition(from: CGPoint(x: 0.0, y: 0.0), to: CGPoint(x: 0.0, y: -self.view.bounds.height - snapshotState.snapshotBottomInset - snapshotTopInset), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak snapshotParentView] _ in + snapshotParentView?.removeFromSuperview() + }) + + self.view.layer.animatePosition(from: CGPoint(x: 0.0, y: self.view.bounds.height + snapshotTopInset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true) + } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift index 888f2dcfe1..77538962d5 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift @@ -163,4 +163,34 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { } } } + + final class SnapshotState { + fileprivate let downButtonSnapshotView: UIView? + + fileprivate init( + downButtonSnapshotView: UIView? + ) { + self.downButtonSnapshotView = downButtonSnapshotView + } + } + + func prepareSnapshotState() -> SnapshotState { + var downButtonSnapshotView: UIView? + if !self.downButton.isHidden { + downButtonSnapshotView = self.downButton.view.snapshotView(afterScreenUpdates: false)! + } + return SnapshotState( + downButtonSnapshotView: downButtonSnapshotView + ) + } + + func animateFromSnapshot(_ snapshotState: SnapshotState) { + if self.downButton.isHidden != (snapshotState.downButtonSnapshotView == nil) { + if self.downButton.isHidden { + } else { + self.downButton.layer.animateAlpha(from: 0.0, to: self.downButton.alpha, duration: 0.3) + self.downButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true) + } + } + } } diff --git a/submodules/TelegramUI/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Sources/ChatOverscrollControl.swift new file mode 100644 index 0000000000..fdadbfec2d --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatOverscrollControl.swift @@ -0,0 +1,518 @@ +import UIKit +import ComponentFlow +import Display +import TelegramCore +import Postbox +import AccountContext +import AvatarNode + +final class BlurredRoundedRectangle: Component { + let color: UIColor + + init(color: UIColor) { + self.color = color + } + + static func ==(lhs: BlurredRoundedRectangle, rhs: BlurredRoundedRectangle) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } + return true + } + + final class View: UIView { + private let background: NavigationBackgroundNode + + init() { + self.background = NavigationBackgroundNode(color: .clear) + + super.init(frame: CGRect()) + + self.addSubview(self.background.view) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: BlurredRoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize { + transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.background.updateColor(color: component.color, transition: .immediate) + self.background.update(size: availableSize, cornerRadius: min(availableSize.width, availableSize.height) / 2.0, transition: .immediate) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +final class RadialProgressComponent: Component { + let color: UIColor + let lineWidth: CGFloat + let value: CGFloat + + init( + color: UIColor, + lineWidth: CGFloat, + value: CGFloat + ) { + self.color = color + self.lineWidth = lineWidth + self.value = value + } + + static func ==(lhs: RadialProgressComponent, rhs: RadialProgressComponent) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } + if lhs.lineWidth != rhs.lineWidth { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + final class View: UIView { + init() { + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: RadialProgressComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func draw(context: CGContext) { + let diameter = availableSize.width + + context.saveGState() + + context.setBlendMode(.normal) + context.setFillColor(component.color.cgColor) + context.setStrokeColor(component.color.cgColor) + + var progress: CGFloat + var startAngle: CGFloat + var endAngle: CGFloat + + let value = component.value + + progress = value + startAngle = -CGFloat.pi / 2.0 + endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + + if progress > 1.0 { + progress = 2.0 - progress + let tmp = startAngle + startAngle = endAngle + endAngle = tmp + } + progress = min(1.0, progress) + + let lineWidth: CGFloat = component.lineWidth + + let pathDiameter: CGFloat + + pathDiameter = diameter - lineWidth + + var angle: Double = 0.0 + angle *= 4.0 + + context.translateBy(x: diameter / 2.0, y: diameter / 2.0) + context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0))) + context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0) + + let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) + path.lineWidth = lineWidth + path.lineCapStyle = .round + path.stroke() + + context.restoreGState() + } + + if #available(iOS 10.0, *) { + let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: availableSize)) + let image = renderer.image { context in + UIGraphicsPushContext(context.cgContext) + draw(context: context.cgContext) + UIGraphicsPopContext() + } + self.layer.contents = image.cgImage + } else { + UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0) + draw(context: UIGraphicsGetCurrentContext()!) + self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage + UIGraphicsEndImageContext() + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +final class CheckComponent: Component { + let color: UIColor + let lineWidth: CGFloat + let value: CGFloat + + init( + color: UIColor, + lineWidth: CGFloat, + value: CGFloat + ) { + self.color = color + self.lineWidth = lineWidth + self.value = value + } + + static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } + if lhs.lineWidth != rhs.lineWidth { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + final class View: UIView { + init() { + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize { + func draw(context: CGContext) { + let size = availableSize + + let diameter = size.width + + let factor = diameter / 50.0 + + context.saveGState() + + context.setBlendMode(.normal) + context.setFillColor(component.color.cgColor) + context.setStrokeColor(component.color.cgColor) + + let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0) + + let lineWidth = component.lineWidth + + context.setLineWidth(max(1.7, lineWidth * factor)) + context.setLineCap(.round) + context.setLineJoin(.round) + context.setMiterLimit(10.0) + + let progress = component.value + let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0)) + + var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor) + var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor) + var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor) + + if diameter < 36.0 { + s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor) + p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor) + p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor) + } + + if !firstSegment.isZero { + if firstSegment < 1.0 { + context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment)) + context.addLine(to: s) + } else { + let secondSegment = (progress - 0.33) * 1.5 + context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment)) + context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y)) + context.addLine(to: s) + } + } + context.strokePath() + + context.restoreGState() + } + + if #available(iOS 10.0, *) { + let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: availableSize)) + let image = renderer.image { context in + UIGraphicsPushContext(context.cgContext) + draw(context: context.cgContext) + UIGraphicsPopContext() + } + self.layer.contents = image.cgImage + } else { + UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0) + draw(context: UIGraphicsGetCurrentContext()!) + self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage + UIGraphicsEndImageContext() + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +final class AvatarComponent: Component { + let context: AccountContext + let peer: EnginePeer + + init(context: AccountContext, peer: EnginePeer) { + self.context = context + self.peer = peer + } + + static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> 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 + + init() { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + super.init(frame: CGRect()) + + self.addSubview(self.avatarNode.view) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: AvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize) + self.avatarNode.setPeer(context: component.context, theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, peer: component.peer._asPeer(), synchronousLoad: true) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +final class ChatOverscrollControl: CombinedComponent { + let text: String + let backgroundColor: UIColor + let foregroundColor: UIColor + let peer: EnginePeer? + let context: AccountContext + let expandProgress: CGFloat + + init( + text: String, + backgroundColor: UIColor, + foregroundColor: UIColor, + peer: EnginePeer?, + context: AccountContext, + expandProgress: CGFloat + ) { + self.text = text + self.backgroundColor = backgroundColor + self.foregroundColor = foregroundColor + self.peer = peer + self.context = context + self.expandProgress = expandProgress + } + + static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool { + if lhs.text != rhs.text { + return false + } + if !lhs.backgroundColor.isEqual(rhs.backgroundColor) { + return false + } + if !lhs.foregroundColor.isEqual(rhs.foregroundColor) { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.context !== rhs.context { + return false + } + if lhs.expandProgress != rhs.expandProgress { + return false + } + return true + } + + static var body: Body { + let avatarBackground = Child(BlurredRoundedRectangle.self) + let avatarExpandProgress = Child(RadialProgressComponent.self) + let avatarCheck = Child(CheckComponent.self) + let avatar = Child(AvatarComponent.self) + let textBackground = Child(BlurredRoundedRectangle.self) + let text = Child(Text.self) + + return { context in + let text = text.update( + component: Text( + text: context.component.text, + font: Font.regular(12.0), + color: context.component.foregroundColor + ), + availableSize: CGSize(width: context.availableSize.width, height: 100.0), + transition: context.transition + ) + + let textHorizontalPadding: CGFloat = 6.0 + let textVerticalPadding: CGFloat = 2.0 + let avatarSize: CGFloat = 48.0 + let avatarPadding: CGFloat = 8.0 + let avatarTextSpacing: CGFloat = 8.0 + let avatarProgressPadding: CGFloat = 2.5 + + let avatarBackgroundSize: CGFloat = context.component.peer != nil ? (avatarSize + avatarPadding * 2.0) : avatarSize + + let avatarBackground = avatarBackground.update( + component: BlurredRoundedRectangle( + color: context.component.backgroundColor + ), + availableSize: CGSize(width: avatarBackgroundSize, height: avatarBackgroundSize), + transition: context.transition + ) + + let avatarCheck = Condition(context.component.peer == nil, { () -> _UpdatedChildComponent in + let avatarCheckSize = avatarBackgroundSize + 2.0 + + return avatarCheck.update( + component: CheckComponent( + color: context.component.foregroundColor, + lineWidth: 2.5, + value: 1.0 + ), + availableSize: CGSize(width: avatarCheckSize, height: avatarCheckSize), + transition: context.transition + ) + }) + + let avatarExpandProgress = avatarExpandProgress.update( + component: RadialProgressComponent( + color: context.component.foregroundColor, + lineWidth: 2.5, + value: context.component.peer == nil ? 0.0 : context.component.expandProgress + ), + availableSize: CGSize(width: avatarBackground.size.width - avatarProgressPadding * 2.0, height: avatarBackground.size.height - avatarProgressPadding * 2.0), + transition: context.transition + ) + + let textBackground = textBackground.update( + component: BlurredRoundedRectangle( + color: context.component.backgroundColor + ), + availableSize: CGSize(width: text.size.width + textHorizontalPadding * 2.0, height: text.size.height + textVerticalPadding * 2.0), + transition: context.transition + ) + + let size = CGSize(width: context.availableSize.width, height: avatarBackground.size.height + avatarTextSpacing + textBackground.size.height) + + let avatarBackgroundFrame = avatarBackground.size.topCentered(in: CGRect(origin: CGPoint(), size: size)) + + let avatar = context.component.peer.flatMap { peer in + avatar.update( + component: AvatarComponent( + context: context.component.context, + peer: peer + ), + availableSize: CGSize(width: avatarSize, height: avatarSize), + transition: context.transition + ) + } + + context.add(avatarBackground + .position(CGPoint( + x: avatarBackgroundFrame.midX, + y: avatarBackgroundFrame.midY + )) + ) + + if let avatarCheck = avatarCheck { + context.add(avatarCheck + .position(CGPoint( + x: avatarBackgroundFrame.midX, + y: avatarBackgroundFrame.midY + )) + ) + } + + context.add(avatarExpandProgress + .position(CGPoint( + x: avatarBackgroundFrame.midX, + y: avatarBackgroundFrame.midY + )) + ) + + if let avatar = avatar { + context.add(avatar + .position(CGPoint( + x: avatarBackgroundFrame.midX, + y: avatarBackgroundFrame.midY + )) + ) + } + + let textBackgroundFrame = textBackground.size.bottomCentered(in: CGRect(origin: CGPoint(), size: size)) + context.add(textBackground + .position(CGPoint( + x: textBackgroundFrame.midX, + y: textBackgroundFrame.midY + )) + ) + + let textFrame = text.size.centered(in: textBackgroundFrame) + context.add(text + .position(CGPoint( + x: textFrame.midX, + y: textFrame.midY + )) + ) + + return size + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index 7986280ed6..20e097103e 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -736,4 +736,33 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } return super.hitTest(point, with: event) } + + final class SnapshotState { + fileprivate let snapshotView: UIView + + fileprivate init(snapshotView: UIView) { + self.snapshotView = snapshotView + } + } + + func prepareSnapshotState() -> SnapshotState { + let snapshotView = self.snapshotView(afterScreenUpdates: false)! + return SnapshotState( + snapshotView: snapshotView + ) + } + + func animateFromSnapshot(_ snapshotState: SnapshotState) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.layer.animatePosition(from: CGPoint(x: 0.0, y: 20.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true) + + snapshotState.snapshotView.frame = self.frame + self.superview?.insertSubview(snapshotState.snapshotView, belowSubview: self) + + let snapshotView = snapshotState.snapshotView + snapshotState.snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -20.0), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + } } diff --git a/submodules/TelegramUI/Sources/DeclareEncodables.swift b/submodules/TelegramUI/Sources/DeclareEncodables.swift index 2800e6a5f8..747c363eea 100644 --- a/submodules/TelegramUI/Sources/DeclareEncodables.swift +++ b/submodules/TelegramUI/Sources/DeclareEncodables.swift @@ -16,7 +16,6 @@ import ChatInterfaceState private var telegramUIDeclaredEncodables: Void = { declareEncodable(InAppNotificationSettings.self, f: { InAppNotificationSettings(decoder: $0) }) declareEncodable(ChatInterfaceState.self, f: { ChatInterfaceState(decoder: $0) }) - declareEncodable(ChatEmbeddedInterfaceState.self, f: { ChatEmbeddedInterfaceState(decoder: $0) }) declareEncodable(VideoLibraryMediaResource.self, f: { VideoLibraryMediaResource(decoder: $0) }) declareEncodable(LocalFileVideoMediaResource.self, f: { LocalFileVideoMediaResource(decoder: $0) }) declareEncodable(LocalFileGifMediaResource.self, f: { LocalFileGifMediaResource(decoder: $0) }) diff --git a/versions.json b/versions.json index c2ec49706b..adda46e6c6 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { "app": "7.9", "bazel": "4.0.0", - "xcode": "12.4" + "xcode": "12.5.1" }