mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
[WIP] Stories UI
This commit is contained in:
@@ -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 }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,7 @@ swift_library(
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
19
submodules/TelegramUI/Components/TextFieldComponent/BUILD
Normal file
19
submodules/TelegramUI/Components/TextFieldComponent/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
12
submodules/TelegramUI/Images.xcassets/Media Gallery/InlineLike.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/InlineLike.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "GalleryShare.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
3
submodules/TelegramUI/Images.xcassets/Media Gallery/InlineLike.imageset/GalleryShare.svg
vendored
Normal file
3
submodules/TelegramUI/Images.xcassets/Media Gallery/InlineLike.imageset/GalleryShare.svg
vendored
Normal 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 |
12
submodules/TelegramUI/Images.xcassets/Media Gallery/InlineShare.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/InlineShare.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "GalleryForward.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
3
submodules/TelegramUI/Images.xcassets/Media Gallery/InlineShare.imageset/GalleryForward.svg
vendored
Normal file
3
submodules/TelegramUI/Images.xcassets/Media Gallery/InlineShare.imageset/GalleryForward.svg
vendored
Normal 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 |
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user