[WIP] Stories UI

This commit is contained in:
Ali
2023-04-21 19:52:09 +04:00
parent 1640a8c76f
commit 06434cc5ef
20 changed files with 1151 additions and 78 deletions

View File

@@ -842,6 +842,9 @@ public protocol SharedAccountContext: AnyObject {
var hasOngoingCall: ValuePromise<Bool> { get }
var immediateHasOngoingCall: Bool { get }
var enablePreloads: Promise<Bool> { get }
var hasPreloadBlockingContent: Promise<Bool> { get }
var hasGroupCallOnScreen: Signal<Bool, NoError> { get }
var currentGroupCallController: ViewController? { get }

View File

@@ -909,12 +909,12 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
if self.controlsHistoryPreload, case .chatList(groupId: .root) = self.location {
self.context.account.viewTracker.chatListPreloadItems.set(combineLatest(queue: .mainQueue(),
context.sharedContext.hasOngoingCall.get(),
context.sharedContext.enablePreloads.get(),
itemNode.listNode.preloadItems.get(),
enablePreload
)
|> map { hasOngoingCall, preloadItems, enablePreload -> Set<ChatHistoryPreloadItem> in
if hasOngoingCall || !enablePreload {
|> map { enablePreloads, preloadItems, enablePreload -> Set<ChatHistoryPreloadItem> in
if !enablePreloads || !enablePreload {
return Set()
} else {
return Set(preloadItems)

View File

@@ -3,6 +3,10 @@ import SwiftSignalKit
import Postbox
import TelegramApi
public enum EngineOutgoingMessageContent {
case text(String)
}
public extension TelegramEngine {
final class Messages {
private let account: Account
@@ -190,6 +194,31 @@ public extension TelegramEngine {
return _internal_exportMessageLink(account: self.account, peerId: peerId, messageId: messageId, isThread: isThread)
}
public func enqueueOutgoingMessage(
to peerId: EnginePeer.Id,
replyTo replyToMessageId: EngineMessage.Id?,
content: EngineOutgoingMessageContent
) {
switch content {
case let .text(text):
let message: EnqueueMessage = .message(
text: text,
attributes: [],
inlineStickers: [:],
mediaReference: nil,
replyToMessageId: replyToMessageId,
localGroupingKey: nil,
correlationId: nil,
bubbleUpEmojiOrStickersets: []
)
let _ = enqueueMessages(
account: self.account,
peerId: peerId,
messages: [message]
).start()
}
}
public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool {
return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId)
}
@@ -198,6 +227,19 @@ public extension TelegramEngine {
return _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId)
}
public func setMessageReactions(
id: EngineMessage.Id,
reactions: [UpdateMessageReaction]
) {
let _ = updateMessageReactionsInteractively(
account: self.account,
messageId: id,
reactions: reactions,
isLarge: false,
storeAsRecentlyUsed: false
).start()
}
public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> {
return _internal_requestChatContextResults(account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
}

View File

@@ -347,12 +347,16 @@ public extension Message {
var hasReactions: Bool {
for attribute in self.attributes {
if let attribute = attribute as? ReactionsMessageAttribute {
return !attribute.reactions.isEmpty
if !attribute.reactions.isEmpty {
return true
}
}
}
for attribute in self.attributes {
if let attribute = attribute as? PendingReactionsMessageAttribute {
return !attribute.reactions.isEmpty
if !attribute.reactions.isEmpty {
return true
}
}
}
return false

View File

@@ -13,6 +13,7 @@ swift_library(
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/TextFieldComponent",
],
visibility = [
"//visibility:public",

View File

@@ -0,0 +1,167 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AppBundle
public final class MessageInputActionButtonComponent: Component {
public enum Mode {
case send
case voiceInput
case videoInput
}
public let mode: Mode
public let action: () -> Void
public init(
mode: Mode,
action: @escaping () -> Void
) {
self.mode = mode
self.action = action
}
public static func ==(lhs: MessageInputActionButtonComponent, rhs: MessageInputActionButtonComponent) -> Bool {
if lhs.mode != rhs.mode {
return false
}
return true
}
public final class View: HighlightTrackingButton {
private let microphoneIconView: UIImageView
private let cameraIconView: UIImageView
private let sendIconView: UIImageView
private var component: MessageInputActionButtonComponent?
private weak var componentState: EmptyComponentState?
override init(frame: CGRect) {
self.microphoneIconView = UIImageView()
self.cameraIconView = UIImageView()
self.sendIconView = UIImageView()
super.init(frame: frame)
self.isMultipleTouchEnabled = false
self.addSubview(self.microphoneIconView)
self.addSubview(self.cameraIconView)
self.addSubview(self.sendIconView)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
let scale: CGFloat = highlighted ? 0.6 : 1.0
let transition = Transition(animation: .curve(duration: highlighted ? 0.5 : 0.3, curve: .spring))
transition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(scale, scale, 1.0))
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.action()
}
override public func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
return super.continueTracking(touch, with: event)
}
func update(component: MessageInputActionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.componentState = state
if self.microphoneIconView.image == nil {
self.microphoneIconView.image = UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone")?.withRenderingMode(.alwaysTemplate)
self.microphoneIconView.tintColor = .white
}
if self.cameraIconView.image == nil {
self.cameraIconView.image = UIImage(bundleImageName: "Chat/Input/Text/IconVideo")?.withRenderingMode(.alwaysTemplate)
self.cameraIconView.tintColor = .white
}
if self.sendIconView.image == nil {
self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.clear.cgColor)
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setLineJoin(.round)
context.translateBy(x: 5.45, y: 4.0)
context.saveGState()
context.translateBy(x: 4.0, y: 4.0)
let _ = try? drawSvgPath(context, path: "M1,7 L7,1 L13,7 S ")
context.restoreGState()
context.saveGState()
context.translateBy(x: 10.0, y: 4.0)
let _ = try? drawSvgPath(context, path: "M1,16 V1 S ")
context.restoreGState()
})
}
var sendAlpha: CGFloat = 0.0
var microphoneAlpha: CGFloat = 0.0
var cameraAlpha: CGFloat = 0.0
switch component.mode {
case .send:
sendAlpha = 1.0
case .videoInput:
cameraAlpha = 1.0
case .voiceInput:
microphoneAlpha = 1.0
}
transition.setAlpha(view: self.sendIconView, alpha: sendAlpha)
transition.setScale(view: self.sendIconView, scale: sendAlpha == 0.0 ? 0.01 : 1.0)
transition.setAlpha(view: self.cameraIconView, alpha: cameraAlpha)
transition.setScale(view: self.cameraIconView, scale: cameraAlpha == 0.0 ? 0.01 : 1.0)
transition.setAlpha(view: self.microphoneIconView, alpha: microphoneAlpha)
transition.setScale(view: self.microphoneIconView, scale: microphoneAlpha == 0.0 ? 0.01 : 1.0)
if let image = self.sendIconView.image {
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - image.size.width) * 0.5), y: floorToScreenPixels((availableSize.height - image.size.height) * 0.5)), size: image.size)
transition.setPosition(view: self.sendIconView, position: iconFrame.center)
transition.setBounds(view: self.sendIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
}
if let image = self.cameraIconView.image {
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - image.size.width) * 0.5), y: floorToScreenPixels((availableSize.height - image.size.height) * 0.5)), size: image.size)
transition.setPosition(view: self.cameraIconView, position: iconFrame.center)
transition.setBounds(view: self.cameraIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
}
if let image = self.microphoneIconView.image {
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - image.size.width) * 0.5), y: floorToScreenPixels((availableSize.height - image.size.height) * 0.5)), size: image.size)
transition.setPosition(view: self.microphoneIconView, position: iconFrame.center)
transition.setBounds(view: self.microphoneIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@@ -3,31 +3,56 @@ import UIKit
import Display
import ComponentFlow
import AppBundle
import TextFieldComponent
public final class MessageInputPanelComponent: Component {
public init() {
public final class ExternalState {
public fileprivate(set) var hasText: Bool = false
public init() {
}
}
public let externalState: ExternalState
public let sendMessageAction: () -> Void
public init(
externalState: ExternalState,
sendMessageAction: @escaping () -> Void
) {
self.externalState = externalState
self.sendMessageAction = sendMessageAction
}
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
if lhs.externalState !== rhs.externalState {
return false
}
return true
}
public enum SendMessageInput {
case text(String)
}
public final class View: UIView {
private let fieldBackgroundView: UIImageView
private let fieldPlaceholder = ComponentView<Empty>()
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
private let attachmentIconView: UIImageView
private let recordingIconView: UIImageView
private let inputActionButton = ComponentView<Empty>()
private let stickerIconView: UIImageView
private var currentMediaInputIsVoice: Bool = true
private var component: MessageInputPanelComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.fieldBackgroundView = UIImageView()
self.attachmentIconView = UIImageView()
self.recordingIconView = UIImageView()
self.stickerIconView = UIImageView()
super.init(frame: frame)
@@ -36,7 +61,6 @@ public final class MessageInputPanelComponent: Component {
self.addSubview(self.fieldBackgroundView)
self.addSubview(self.attachmentIconView)
self.addSubview(self.recordingIconView)
self.addSubview(self.stickerIconView)
}
@@ -44,7 +68,22 @@ public final class MessageInputPanelComponent: Component {
fatalError("init(coder:) has not been implemented")
}
public func getSendMessageInput() -> SendMessageInput {
guard let textFieldView = self.textField.view as? TextFieldComponent.View else {
return .text("")
}
return .text(textFieldView.getText())
}
public func clearSendMessageInput() {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
textFieldView.setText(string: "")
}
}
func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let baseHeight: CGFloat = 44.0
let insets = UIEdgeInsets(top: 5.0, left: 41.0, bottom: 5.0, right: 41.0)
let fieldCornerRadius: CGFloat = 16.0
@@ -58,47 +97,82 @@ public final class MessageInputPanelComponent: Component {
self.attachmentIconView.image = UIImage(bundleImageName: "Chat/Input/Text/IconAttachment")?.withRenderingMode(.alwaysTemplate)
self.attachmentIconView.tintColor = .white
}
if self.recordingIconView.image == nil {
self.recordingIconView.image = UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone")?.withRenderingMode(.alwaysTemplate)
self.recordingIconView.tintColor = .white
}
if self.stickerIconView.image == nil {
self.stickerIconView.image = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconStickers")?.withRenderingMode(.alwaysTemplate)
self.stickerIconView.tintColor = .white
}
let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: availableSize.height - insets.top - insets.bottom))
let availableTextFieldSize = CGSize(width: availableSize.width - insets.left - insets.right, height: availableSize.height - insets.top - insets.bottom)
self.textField.parentState = state
let textFieldSize = self.textField.update(
transition: .immediate,
component: AnyComponent(TextFieldComponent(
externalState: self.textFieldExternalState,
placeholder: "Reply Privately..."
)),
environment: {},
containerSize: availableTextFieldSize
)
let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height))
transition.setFrame(view: self.fieldBackgroundView, frame: fieldFrame)
let rightFieldInset: CGFloat = 34.0
let placeholderSize = self.fieldPlaceholder.update(
transition: .immediate,
component: AnyComponent(Text(text: "Reply Privately...", font: Font.regular(17.0), color: UIColor(white: 1.0, alpha: 0.16))),
environment: {},
containerSize: fieldFrame.size
)
if let fieldPlaceholderView = self.fieldPlaceholder.view {
if fieldPlaceholderView.superview == nil {
fieldPlaceholderView.layer.anchorPoint = CGPoint()
fieldPlaceholderView.isUserInteractionEnabled = false
self.addSubview(fieldPlaceholderView)
let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom)
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.addSubview(textFieldView)
}
fieldPlaceholderView.bounds = CGRect(origin: CGPoint(), size: placeholderSize)
transition.setPosition(view: fieldPlaceholderView, position: CGPoint(x: fieldFrame.minX + 12.0, y: fieldFrame.minY + floor((fieldFrame.height - placeholderSize.height) * 0.5)))
transition.setFrame(view: textFieldView, frame: CGRect(origin: CGPoint(x: fieldFrame.minX, y: fieldFrame.maxY - textFieldSize.height), size: textFieldSize))
}
if let image = self.attachmentIconView.image {
transition.setFrame(view: self.attachmentIconView, frame: CGRect(origin: CGPoint(x: floor((insets.left - image.size.width) * 0.5), y: floor((availableSize.height - image.size.height) * 0.5)), size: image.size))
}
if let image = self.recordingIconView.image {
transition.setFrame(view: self.recordingIconView, frame: CGRect(origin: CGPoint(x: availableSize.width - insets.right + floor((insets.right - image.size.width) * 0.5), y: floor((availableSize.height - image.size.height) * 0.5)), size: image.size))
}
if let image = self.stickerIconView.image {
transition.setFrame(view: self.stickerIconView, frame: CGRect(origin: CGPoint(x: fieldFrame.maxX - rightFieldInset + floor((rightFieldInset - image.size.width) * 0.5), y: fieldFrame.minY + floor((fieldFrame.height - image.size.height) * 0.5)), size: image.size))
transition.setFrame(view: self.attachmentIconView, frame: CGRect(origin: CGPoint(x: floor((insets.left - image.size.width) * 0.5), y: size.height - baseHeight + floor((baseHeight - image.size.height) * 0.5)), size: image.size))
}
return availableSize
let inputActionButtonSize = self.inputActionButton.update(
transition: transition,
component: AnyComponent(MessageInputActionButtonComponent(
mode: self.textFieldExternalState.hasText ? .send : (self.currentMediaInputIsVoice ? .voiceInput : .videoInput),
action: { [weak self] in
guard let self else {
return
}
if case .text("") = self.getSendMessageInput() {
self.currentMediaInputIsVoice = !self.currentMediaInputIsVoice
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
HapticFeedback().impact()
} else {
self.component?.sendMessageAction()
}
}
)),
environment: {},
containerSize: CGSize(width: 33.0, height: 33.0)
)
if let inputActionButtonView = self.inputActionButton.view {
if inputActionButtonView.superview == nil {
self.addSubview(inputActionButtonView)
}
transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5), y: size.height - baseHeight + floorToScreenPixels((baseHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize))
}
if let image = self.stickerIconView.image {
let stickerIconFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - rightFieldInset + floor((rightFieldInset - image.size.width) * 0.5), y: fieldFrame.minY + floor((fieldFrame.height - image.size.height) * 0.5)), size: image.size)
transition.setPosition(view: self.stickerIconView, position: stickerIconFrame.center)
transition.setBounds(view: self.stickerIconView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size))
transition.setAlpha(view: self.stickerIconView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0)
transition.setScale(view: self.stickerIconView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0)
}
component.externalState.hasText = self.textFieldExternalState.hasText
return size
}
}

View File

@@ -13,10 +13,14 @@ swift_library(
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
"//submodules/AccountContext",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AppBundle",
"//submodules/TelegramCore",
"//submodules/ShareController",
"//submodules/UndoUI",
],
visibility = [
"//visibility:public",

View File

@@ -0,0 +1,181 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AppBundle
import ComponentDisplayAdapters
public final class StoryActionsComponent: Component {
public struct Item: Equatable {
public enum Kind {
case like
case share
}
public let kind: Kind
public let isActivated: Bool
public init(kind: Kind, isActivated: Bool) {
self.kind = kind
self.isActivated = isActivated
}
}
public let items: [Item]
public let action: (Item) -> Void
public init(
items: [Item],
action: @escaping (Item) -> Void
) {
self.items = items
self.action = action
}
public static func ==(lhs: StoryActionsComponent, rhs: StoryActionsComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
return true
}
private final class ItemView: HighlightTrackingButton {
let action: (Item) -> Void
let maskBackgroundView = UIImageView()
let iconView: UIImageView
private var item: Item?
init(action: @escaping (Item) -> Void) {
self.action = action
self.iconView = UIImageView()
super.init(frame: CGRect())
self.addSubview(self.iconView)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
let scale: CGFloat = highlighted ? 0.6 : 1.0
let transition = Transition(animation: .curve(duration: highlighted ? 0.5 : 0.3, curve: .spring))
transition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(scale, scale, 1.0))
transition.setScale(view: self.maskBackgroundView, scale: scale)
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let item = self.item else {
return
}
self.action(item)
}
func update(item: Item, size: CGSize, transition: Transition) {
if self.item == item {
return
}
self.item = item
switch item.kind {
case .like:
self.iconView.image = UIImage(bundleImageName: "Media Gallery/InlineLike")?.withRenderingMode(.alwaysTemplate)
case .share:
self.iconView.image = UIImage(bundleImageName: "Media Gallery/InlineShare")?.withRenderingMode(.alwaysTemplate)
}
self.iconView.tintColor = item.isActivated ? UIColor(rgb: 0xFF6F66) : UIColor.white
if let image = self.iconView.image {
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size))
}
}
}
public final class View: UIView {
private let backgroundView: BlurredBackgroundView
private let backgroundMaskView: UIView
private var itemViews: [Item.Kind: ItemView] = [:]
private var component: StoryActionsComponent?
private weak var componentState: EmptyComponentState?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.backgroundMaskView = UIView()
self.backgroundView.mask = self.backgroundMaskView
super.init(frame: frame)
self.addSubview(self.backgroundView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StoryActionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.componentState = state
var contentHeight: CGFloat = 0.0
var validIds: [Item.Kind] = []
for item in component.items {
validIds.append(item.kind)
let itemView: ItemView
var itemTransition = transition
if let current = self.itemViews[item.kind] {
itemView = current
} else {
itemTransition = .immediate
itemView = ItemView(action: { [weak self] item in
self?.component?.action(item)
})
self.itemViews[item.kind] = itemView
self.addSubview(itemView)
itemView.maskBackgroundView.image = generateFilledCircleImage(diameter: 44.0, color: .white)
self.backgroundMaskView.addSubview(itemView.maskBackgroundView)
}
if !contentHeight.isZero {
contentHeight += 10.0
}
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: 44.0, height: 44.0))
itemView.update(item: item, size: itemFrame.size, transition: itemTransition)
itemTransition.setFrame(view: itemView, frame: itemFrame)
itemTransition.setPosition(view: itemView.maskBackgroundView, position: itemFrame.center)
itemTransition.setBounds(view: itemView.maskBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
contentHeight += itemFrame.height
}
let contentSize = CGSize(width: 44.0, height: contentHeight)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: contentSize))
self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.3), transition: .immediate)
self.backgroundView.update(size: contentSize, transition: transition.containedViewLayoutTransition)
return contentSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@@ -7,19 +7,40 @@ import AccountContext
import SwiftSignalKit
import AppBundle
import MessageInputPanelComponent
import ShareController
import TelegramCore
import UndoUI
private func hasFirstResponder(_ view: UIView) -> Bool {
if view.isFirstResponder {
return true
}
for subview in view.subviews {
if hasFirstResponder(subview) {
return true
}
}
return false
}
private final class StoryContainerScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let initialContent: StoryContentItemSlice
init(
context: AccountContext,
initialContent: StoryContentItemSlice
) {
self.context = context
self.initialContent = initialContent
}
static func ==(lhs: StoryContainerScreenComponent, rhs: StoryContainerScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.initialContent !== rhs.initialContent {
return false
}
@@ -65,16 +86,20 @@ private final class StoryContainerScreenComponent: Component {
private let contentContainerView: UIView
private let topContentGradientLayer: SimpleGradientLayer
private let bottomContentGradientLayer: SimpleGradientLayer
private let contentDimLayer: SimpleLayer
private let closeButton: HighlightableButton
private let closeButtonIconView: UIImageView
private let navigationStrip = ComponentView<Empty>()
private let inlineActions = ComponentView<Empty>()
private var centerInfoItem: InfoItem?
private var rightInfoItem: InfoItem?
private let inputPanel = ComponentView<Empty>()
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
private var component: StoryContainerScreenComponent?
private weak var state: EmptyComponentState?
@@ -89,6 +114,8 @@ private final class StoryContainerScreenComponent: Component {
private var visibleItems: [AnyHashable: VisibleItem] = [:]
private var preloadContexts: [AnyHashable: Disposable] = [:]
override init(frame: CGRect) {
self.scrollView = ScrollView()
@@ -97,6 +124,8 @@ private final class StoryContainerScreenComponent: Component {
self.contentContainerView.isUserInteractionEnabled = false
self.topContentGradientLayer = SimpleGradientLayer()
self.bottomContentGradientLayer = SimpleGradientLayer()
self.contentDimLayer = SimpleLayer()
self.closeButton = HighlightableButton()
self.closeButtonIconView = UIImageView()
@@ -123,7 +152,9 @@ private final class StoryContainerScreenComponent: Component {
self.scrollView.clipsToBounds = true
self.addSubview(self.contentContainerView)
self.layer.addSublayer(self.contentDimLayer)
self.layer.addSublayer(self.topContentGradientLayer)
self.layer.addSublayer(self.bottomContentGradientLayer)
self.closeButton.addSubview(self.closeButtonIconView)
self.addSubview(self.closeButton)
@@ -142,6 +173,9 @@ private final class StoryContainerScreenComponent: Component {
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state, let currentSlice = self.currentSlice, let focusedItemId = self.focusedItemId, let currentIndex = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }), let itemLayout = self.itemLayout {
if hasFirstResponder(self) {
self.endEditing(true)
} else {
let point = recognizer.location(in: self)
var nextIndex: Int
@@ -171,6 +205,7 @@ private final class StoryContainerScreenComponent: Component {
}
}
}
}
@objc private func closePressed() {
guard let environment = self.environment, let controller = environment.controller() else {
@@ -256,6 +291,123 @@ private final class StoryContainerScreenComponent: Component {
})
}
private func performSendMessageAction() {
guard let component = self.component else {
return
}
guard let focusedItemId = self.focusedItemId, let focusedItem = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) else {
return
}
guard let targetMessageId = focusedItem.targetMessageId else {
return
}
guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else {
return
}
switch inputPanelView.getSendMessageInput() {
case let .text(text):
if !text.isEmpty {
component.context.engine.messages.enqueueOutgoingMessage(
to: targetMessageId.peerId,
replyTo: targetMessageId,
content: .text(text)
)
inputPanelView.clearSendMessageInput()
if let controller = self.environment?.controller() {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .succeed(text: "Message Sent"),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
}
}
}
}
private func performInlineAction(item: StoryActionsComponent.Item) {
guard let component = self.component else {
return
}
guard let focusedItemId = self.focusedItemId, let focusedItem = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) else {
return
}
guard let targetMessageId = focusedItem.targetMessageId else {
return
}
switch item.kind {
case .like:
if item.isActivated {
component.context.engine.messages.setMessageReactions(
id: targetMessageId,
reactions: [
]
)
} else {
component.context.engine.messages.setMessageReactions(
id: targetMessageId,
reactions: [
.builtin("")
]
)
}
case .share:
let _ = (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Messages.Message(id: targetMessageId)
)
|> deliverOnMainQueue).start(next: { [weak self] message in
guard let self, let message, let component = self.component, let controller = self.environment?.controller() else {
return
}
let shareController = ShareController(
context: component.context,
subject: .messages([message._asMessage()]),
externalShare: false,
immediateExternalShare: false,
updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }),
component.context.sharedContext.presentationData)
)
controller.present(shareController, in: .window(.root))
})
}
}
private func updatePreloads() {
var validIds: [AnyHashable] = []
if let currentSlice = self.currentSlice, let focusedItemId = self.focusedItemId, let currentIndex = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }) {
for i in 0 ..< 2 {
var nextIndex: Int = currentIndex - 1 - i
nextIndex = max(0, min(nextIndex, currentSlice.items.count - 1))
if nextIndex != currentIndex {
let nextItem = currentSlice.items[nextIndex]
validIds.append(nextItem.id)
if self.preloadContexts[nextItem.id] == nil {
if let signal = nextItem.preload {
self.preloadContexts[nextItem.id] = signal.start()
}
}
}
}
}
var removeIds: [AnyHashable] = []
for (id, disposable) in self.preloadContexts {
if !validIds.contains(id) {
removeIds.append(id)
disposable.dispose()
}
}
for id in removeIds {
self.preloadContexts.removeValue(forKey: id)
}
}
func update(component: StoryContainerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
let isFirstTime = self.component == nil
@@ -298,6 +450,27 @@ private final class StoryContainerScreenComponent: Component {
self.topContentGradientLayer.colors = colors
self.topContentGradientLayer.type = .axial
}
if self.bottomContentGradientLayer.colors == nil {
var locations: [NSNumber] = []
var colors: [CGColor] = []
let numStops = 10
let baseAlpha: CGFloat = 0.7
for i in 0 ..< numStops {
let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1)
locations.append((1.0 - step) as NSNumber)
let alphaStep: CGFloat = pow(step, 1.5)
colors.append(UIColor.black.withAlphaComponent(alphaStep * baseAlpha).cgColor)
}
self.bottomContentGradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
self.bottomContentGradientLayer.endPoint = CGPoint(x: 0.0, y: 0.0)
self.bottomContentGradientLayer.locations = locations
self.bottomContentGradientLayer.colors = colors
self.bottomContentGradientLayer.type = .axial
self.contentDimLayer.backgroundColor = UIColor(white: 0.0, alpha: 0.3).cgColor
}
if let focusedItemId = self.focusedItemId {
if let currentSlice = self.currentSlice {
@@ -309,15 +482,47 @@ private final class StoryContainerScreenComponent: Component {
}
}
self.updatePreloads()
self.component = component
self.state = state
self.environment = environment
let bottomContentInset: CGFloat
var bottomContentInset: CGFloat
if !environment.safeInsets.bottom.isZero {
bottomContentInset = environment.safeInsets.bottom + 5.0 + 44.0
bottomContentInset = environment.safeInsets.bottom + 5.0
} else {
bottomContentInset = 44.0
bottomContentInset = 0.0
}
self.inputPanel.parentState = state
let inputPanelSize = self.inputPanel.update(
transition: transition,
component: AnyComponent(MessageInputPanelComponent(
externalState: self.inputPanelExternalState,
sendMessageAction: { [weak self] in
guard let self else {
return
}
self.performSendMessageAction()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 200.0)
)
let bottomContentInsetWithoutInput = bottomContentInset
let inputPanelBottomInset: CGFloat
let inputPanelIsOverlay: Bool
if environment.inputHeight < bottomContentInset + inputPanelSize.height {
inputPanelBottomInset = bottomContentInset
bottomContentInset += inputPanelSize.height
inputPanelIsOverlay = false
} else {
bottomContentInset += 44.0
inputPanelBottomInset = environment.inputHeight
inputPanelIsOverlay = true
}
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.statusBarHeight), size: CGSize(width: availableSize.width, height: availableSize.height - environment.statusBarHeight - bottomContentInset))
@@ -348,7 +553,7 @@ private final class StoryContainerScreenComponent: Component {
if let rightInfoItem = self.rightInfoItem, currentRightInfoItem?.component != rightInfoItem.component {
self.rightInfoItem = nil
if let view = rightInfoItem.view.view {
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.5, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
view?.removeFromSuperview()
})
@@ -395,7 +600,7 @@ private final class StoryContainerScreenComponent: Component {
if animateIn, !isFirstTime {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
}
}
@@ -445,27 +650,76 @@ private final class StoryContainerScreenComponent: Component {
}
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: contentFrame.minX + navigationStripSideInset, y: contentFrame.minY + navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
}
if let focusedItemId = self.focusedItemId, let focusedItem = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) {
let inlineActionsSize = self.inlineActions.update(
transition: transition,
component: AnyComponent(StoryActionsComponent(
items: [
StoryActionsComponent.Item(
kind: .like,
isActivated: focusedItem.hasLike
),
StoryActionsComponent.Item(
kind: .share,
isActivated: false
)
],
action: { [weak self] item in
guard let self else {
return
}
self.performInlineAction(item: item)
}
)),
environment: {},
containerSize: contentFrame.size
)
if let inlineActionsView = self.inlineActions.view {
if inlineActionsView.superview == nil {
self.addSubview(inlineActionsView)
}
transition.setFrame(view: inlineActionsView, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 10.0 - inlineActionsSize.width, y: contentFrame.maxY - 20.0 - inlineActionsSize.height), size: inlineActionsSize))
transition.setAlpha(view: inlineActionsView, alpha: inputPanelIsOverlay ? 0.0 : 1.0)
}
}
}
let gradientHeight: CGFloat = 74.0
transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: gradientHeight)))
let itemLayout = ItemLayout(size: contentFrame.size)
let itemLayout = ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - environment.statusBarHeight - 44.0 - bottomContentInsetWithoutInput))
self.itemLayout = itemLayout
let inputPanelSize = self.inputPanel.update(
transition: transition,
component: AnyComponent(MessageInputPanelComponent(
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 44.0)
)
let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - inputPanelSize.height), size: inputPanelSize)
if let inputPanelView = self.inputPanel.view {
if inputPanelView.superview == nil {
self.addSubview(inputPanelView)
}
transition.setFrame(view: inputPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentFrame.maxY), size: inputPanelSize))
transition.setFrame(view: inputPanelView, frame: inputPanelFrame)
}
let bottomGradientHeight = inputPanelSize.height + 32.0
transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - environment.inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight)))
transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0)
if let controller = environment.controller() {
let subLayout = ContainerViewLayout(
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: availableSize.height - min(inputPanelFrame.minY, contentFrame.maxY), right: 0.0),
safeInsets: UIEdgeInsets(),
additionalInsets: UIEdgeInsets(),
statusBarHeight: nil,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(subLayout, transition: transition.containedViewLayoutTransition)
}
transition.setFrame(layer: self.contentDimLayer, frame: contentFrame)
transition.setAlpha(layer: self.contentDimLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0)
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
@@ -490,20 +744,26 @@ private final class StoryContainerScreenComponent: Component {
}
public class StoryContainerScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var isDismissed: Bool = false
public init(
context: AccountContext,
initialContent: StoryContentItemSlice
) {
self.context = context
super.init(context: context, component: StoryContainerScreenComponent(
context: context,
initialContent: initialContent
), navigationBarAppearance: .none)
self.statusBar.statusBarStyle = .White
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
//self.automaticallyControlPresentationContextLayout = false
self.automaticallyControlPresentationContextLayout = false
self.context.sharedContext.hasPreloadBlockingContent.set(.single(true))
}
required public init(coder aDecoder: NSCoder) {
@@ -511,6 +771,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
}
deinit {
self.context.sharedContext.hasPreloadBlockingContent.set(.single(false))
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@@ -531,7 +792,11 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
if !self.isDismissed {
self.isDismissed = true
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View {
componentView.endEditing(true)
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)

View File

@@ -3,6 +3,7 @@ import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
public final class StoryContentItem {
public let id: AnyHashable
@@ -10,19 +11,28 @@ public final class StoryContentItem {
public let component: AnyComponent<Empty>
public let centerInfoComponent: AnyComponent<Empty>?
public let rightInfoComponent: AnyComponent<Empty>?
public let targetMessageId: EngineMessage.Id?
public let preload: Signal<Never, NoError>?
public let hasLike: Bool
public init(
id: AnyHashable,
position: Int,
component: AnyComponent<Empty>,
centerInfoComponent: AnyComponent<Empty>?,
rightInfoComponent: AnyComponent<Empty>?
rightInfoComponent: AnyComponent<Empty>?,
targetMessageId: EngineMessage.Id?,
preload: Signal<Never, NoError>?,
hasLike: Bool
) {
self.id = id
self.position = position
self.component = component
self.centerInfoComponent = centerInfoComponent
self.rightInfoComponent = rightInfoComponent
self.targetMessageId = targetMessageId
self.preload = preload
self.hasLike = hasLike
}
}

View File

@@ -30,6 +30,22 @@ public enum StoryChatContent {
if let location = entry.location {
totalCount = location.count
}
var hasLike = false
if let reactions = entry.message.effectiveReactions {
for reaction in reactions {
if !reaction.isSelected {
continue
}
if reaction.value == .builtin("") {
hasLike = true
}
}
}
var preload: Signal<Never, NoError>?
preload = StoryMessageContentComponent.preload(context: context, message: EngineMessage(entry.message))
items.append(StoryContentItem(
id: AnyHashable(entry.message.id),
position: entry.location?.index ?? 0,
@@ -46,7 +62,10 @@ public enum StoryChatContent {
context: context,
peer: EnginePeer(author)
))
}
},
targetMessageId: entry.message.id,
preload: preload,
hasLike: hasLike
))
}
return StoryContentItemSlice(

View File

@@ -29,6 +29,56 @@ final class StoryMessageContentComponent: Component {
return true
}
static func preload(context: AccountContext, message: EngineMessage) -> Signal<Never, NoError> {
var messageMedia: EngineMedia?
for media in message.media {
switch media {
case let image as TelegramMediaImage:
messageMedia = .image(image)
case let file as TelegramMediaFile:
messageMedia = .file(file)
default:
break
}
}
guard let messageMedia else {
return .complete()
}
var fetchSignal: Signal<Never, NoError>?
switch messageMedia {
case let .image(image):
if let representation = image.representations.last {
fetchSignal = fetchedMediaResource(
mediaBox: context.account.postbox.mediaBox,
userLocation: .peer(message.id.peerId),
userContentType: .image,
reference: ImageMediaReference.message(message: MessageReference(message._asMessage()), media: image).resourceReference(representation.resource)
)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
}
case let .file(file):
fetchSignal = fetchedMediaResource(
mediaBox: context.account.postbox.mediaBox,
userLocation: .peer(message.id.peerId),
userContentType: .image,
reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource)
)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
default:
break
}
return fetchSignal ?? .complete()
}
final class View: UIView {
private let imageNode: TransformImageNode
private var videoNode: UniversalVideoNode?
@@ -85,6 +135,7 @@ final class StoryMessageContentComponent: Component {
return
}
if value {
self.videoNode?.seek(0.0)
self.videoNode?.play()
}
}
@@ -133,8 +184,16 @@ final class StoryMessageContentComponent: Component {
highQuality: true
)
if let representation = image.representations.last {
fetchSignal = messageMediaImageInteractiveFetched(context: component.context, message: component.message._asMessage(), image: image, resource: representation.resource, userInitiated: true, storeToDownloadsPeerId: component.message.id.peerId)
fetchSignal = fetchedMediaResource(
mediaBox: component.context.account.postbox.mediaBox,
userLocation: .peer(component.message.id.peerId),
userContentType: .image,
reference: ImageMediaReference.message(message: MessageReference(component.message._asMessage()), media: image).resourceReference(representation.resource)
)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
}
case let .file(file):
signal = chatMessageVideo(
@@ -143,8 +202,16 @@ final class StoryMessageContentComponent: Component {
videoReference: .message(message: MessageReference(component.message._asMessage()), media: file),
synchronousLoad: true
)
fetchSignal = messageMediaFileInteractiveFetched(context: component.context, message: component.message._asMessage(), file: file, userInitiated: true, storeToDownloadsPeerId: component.message.id.peerId)
fetchSignal = fetchedMediaResource(
mediaBox: component.context.account.postbox.mediaBox,
userLocation: .peer(component.message.id.peerId),
userContentType: .image,
reference: FileMediaReference.message(message: MessageReference(component.message._asMessage()), media: file).resourceReference(file.resource)
)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
default:
break
}

View File

@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "TextFieldComponent",
module_name = "TextFieldComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
],
visibility = [
"//visibility:public",
],
)

View File

@@ -0,0 +1,170 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class TextFieldComponent: Component {
public final class ExternalState {
public fileprivate(set) var hasText: Bool = false
public init() {
}
}
public final class AnimationHint {
public enum Kind {
case textChanged
}
public let kind: Kind
fileprivate init(kind: Kind) {
self.kind = kind
}
}
public let externalState: ExternalState
public let placeholder: String
public init(
externalState: ExternalState,
placeholder: String
) {
self.externalState = externalState
self.placeholder = placeholder
}
public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool {
if lhs.externalState !== rhs.externalState {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
return true
}
public final class View: UIView, UITextViewDelegate, UIScrollViewDelegate {
private let placeholder = ComponentView<Empty>()
private let textContainer: NSTextContainer
private let textStorage: NSTextStorage
private let layoutManager: NSLayoutManager
private let textView: UITextView
private var component: TextFieldComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.textContainer = NSTextContainer(size: CGSize())
self.textContainer.widthTracksTextView = false
self.textContainer.heightTracksTextView = false
self.textContainer.lineBreakMode = .byWordWrapping
self.textContainer.lineFragmentPadding = 8.0
self.textStorage = NSTextStorage()
self.layoutManager = NSLayoutManager()
self.layoutManager.allowsNonContiguousLayout = false
self.layoutManager.addTextContainer(self.textContainer)
self.textStorage.addLayoutManager(self.layoutManager)
self.textView = UITextView(frame: CGRect(), textContainer: self.textContainer)
self.textView.translatesAutoresizingMaskIntoConstraints = false
self.textView.textContainerInset = UIEdgeInsets(top: 6.0, left: 8.0, bottom: 7.0, right: 8.0)
self.textView.backgroundColor = nil
self.textView.layer.isOpaque = false
self.textView.keyboardAppearance = .dark
self.textView.indicatorStyle = .white
self.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: 0.0)
super.init(frame: frame)
self.clipsToBounds = true
self.textView.delegate = self
self.addSubview(self.textView)
self.textContainer.widthTracksTextView = false
self.textContainer.heightTracksTextView = false
self.textView.typingAttributes = [
NSAttributedString.Key.font: Font.regular(17.0),
NSAttributedString.Key.foregroundColor: UIColor.white
]
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func textViewDidChange(_ textView: UITextView) {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
//print("didScroll \(scrollView.bounds)")
}
public func getText() -> String {
Keyboard.applyAutocorrection(textView: self.textView)
return self.textView.text ?? ""
}
public func setText(string: String) {
self.textView.text = string
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
}
func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
self.textContainer.size = CGSize(width: availableSize.width - self.textView.textContainerInset.left - self.textView.textContainerInset.right, height: 10000000.0)
self.layoutManager.ensureLayout(for: self.textContainer)
let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer)
let size = CGSize(width: availableSize.width, height: min(100.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
let refreshScrolling = self.textView.bounds.size != size
self.textView.frame = CGRect(origin: CGPoint(), size: size)
//transition.setFrame(view: self.textView, frame: )
if refreshScrolling {
self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false)
}
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(Text(text: component.placeholder, font: Font.regular(17.0), color: UIColor(white: 1.0, alpha: 0.25))),
environment: {},
containerSize: availableSize
)
if let placeholderView = self.placeholder.view {
if placeholderView.superview == nil {
placeholderView.layer.anchorPoint = CGPoint()
placeholderView.isUserInteractionEnabled = false
self.insertSubview(placeholderView, belowSubview: self.textView)
}
let placeholderFrame = CGRect(origin: CGPoint(x: self.textView.textContainerInset.left + 5.0, y: self.textView.textContainerInset.top), size: placeholderSize)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.isHidden = self.textStorage.length != 0
}
component.externalState.hasText = self.textStorage.length != 0
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

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

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 22.0248C11.7574 22.0248 12.1238 21.8366 12.4208 21.6485C17.9554 18.0842 21.5 13.9059 21.5 9.66832C21.5 6.03465 18.9951 3.5 15.8366 3.5C13.8663 3.5 12.3911 4.58911 11.5 6.22277C10.6287 4.59901 9.14356 3.5 7.17327 3.5C4.01485 3.5 1.5 6.03465 1.5 9.66832C1.5 13.9059 5.04455 18.0842 10.5792 21.6485C10.8861 21.8366 11.2525 22.0248 11.5 22.0248Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 476 B

View File

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

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3672 7.01339V4.93582C12.3672 3.54513 12.3672 2.84978 12.6461 2.51578C12.8882 2.22574 13.2529 2.06671 13.6302 2.08657C14.0647 2.10943 14.5743 2.58253 15.5935 3.52874L22.4844 9.92631C23.011 10.4152 23.2743 10.6596 23.3719 10.9457C23.4578 11.197 23.4578 11.4697 23.3719 11.7211C23.2743 12.0072 23.011 12.2516 22.4844 12.7405L15.5935 19.138C14.5743 20.0842 14.0647 20.5573 13.6302 20.5802C13.2529 20.6001 12.8882 20.441 12.6461 20.151C12.3672 19.817 12.3672 19.1216 12.3672 17.7309V15.6534C6.97993 15.6534 3.71849 17.7586 1.90971 19.5691C1.1042 20.3754 0.70145 20.7786 0.513484 20.7859C0.33844 20.7927 0.20329 20.7259 0.102408 20.5827C-0.00592221 20.4289 0.060693 19.9321 0.193923 18.9386C0.788894 14.5017 3.15728 7.01339 12.3672 7.01339Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 907 B

View File

@@ -130,6 +130,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
private var hasOngoingCallDisposable: Disposable?
public let enablePreloads = Promise<Bool>()
public let hasPreloadBlockingContent = Promise<Bool>(false)
private var accountUserInterfaceInUseContexts: [AccountRecordId: AccountUserInterfaceInUseContext] = [:]
var switchingData: (settingsController: (SettingsController & ViewController)?, chatListController: ChatListController?, chatListBadge: String?) = (nil, nil, nil)
@@ -869,6 +872,20 @@ public final class SharedAccountContextImpl: SharedAccountContext {
let _ = immediateHasOngoingCallValue.swap(value)
})
self.enablePreloads.set(combineLatest(
self.hasOngoingCall.get(),
self.hasPreloadBlockingContent.get()
)
|> map { hasOngoingCall, hasPreloadBlockingContent -> Bool in
if hasOngoingCall {
return false
}
if hasPreloadBlockingContent {
return false
}
return true
})
let _ = managedCleanupAccounts(networkArguments: networkArguments, accountManager: self.accountManager, rootPath: rootPath, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(appDelegate: appDelegate), encryptionParameters: encryptionParameters).start()
self.updateNotificationTokensRegistration()