mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-28 02:45:58 +00:00
930 lines
50 KiB
Swift
930 lines
50 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import TelegramPresentationData
|
|
import ChatInputAccessoryPanel
|
|
import AccountContext
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import ComponentFlow
|
|
import Display
|
|
import GlassBackgroundComponent
|
|
import MultilineTextComponent
|
|
import MultilineTextWithEntitiesComponent
|
|
import TelegramStringFormatting
|
|
import PhotoResources
|
|
import TextFormat
|
|
import CompositeTextNode
|
|
import ChatInterfaceState
|
|
|
|
private func generateCloseIcon() -> UIImage {
|
|
return generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setBlendMode(.copy)
|
|
context.setStrokeColor(UIColor.white.cgColor)
|
|
context.setLineWidth(2.0)
|
|
context.setLineCap(.round)
|
|
context.move(to: CGPoint(x: 1.0, y: 1.0))
|
|
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0))
|
|
context.strokePath()
|
|
context.move(to: CGPoint(x: size.width - 1.0, y: 1.0))
|
|
context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0))
|
|
context.strokePath()
|
|
})!.withRenderingMode(.alwaysTemplate)
|
|
}
|
|
|
|
private func textStringForForwardedMessage(_ message: EngineMessage, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) {
|
|
for media in message.media {
|
|
switch media {
|
|
case _ as TelegramMediaImage:
|
|
return (strings.Message_Photo, [], true)
|
|
case let file as TelegramMediaFile:
|
|
if file.isVideoSticker || file.isAnimatedSticker {
|
|
return (strings.Message_Sticker, [], true)
|
|
}
|
|
var fileName: String = strings.Message_File
|
|
for attribute in file.attributes {
|
|
switch attribute {
|
|
case .Sticker:
|
|
return (strings.Message_Sticker, [], true)
|
|
case let .FileName(name):
|
|
fileName = name
|
|
case let .Audio(isVoice, _, title, performer, _):
|
|
if isVoice {
|
|
return (strings.Message_Audio, [], true)
|
|
} else {
|
|
if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty {
|
|
return (title + " — " + performer, [], true)
|
|
} else if let title = title, !title.isEmpty {
|
|
return (title, [], true)
|
|
} else if let performer = performer, !performer.isEmpty {
|
|
return (performer, [], true)
|
|
} else {
|
|
return (strings.Message_Audio, [], true)
|
|
}
|
|
}
|
|
case .Video:
|
|
if file.isAnimated {
|
|
return (strings.Message_Animation, [], true)
|
|
} else {
|
|
return (strings.Message_Video, [], true)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return (fileName, [], true)
|
|
case _ as TelegramMediaContact:
|
|
return (strings.Message_Contact, [], true)
|
|
case let game as TelegramMediaGame:
|
|
return (game.title, [], true)
|
|
case _ as TelegramMediaMap:
|
|
return (strings.Message_Location, [], true)
|
|
case _ as TelegramMediaAction:
|
|
return ("", [], true)
|
|
case _ as TelegramMediaPoll:
|
|
return (strings.ForwardedPolls(1), [], true)
|
|
case let todo as TelegramMediaTodo:
|
|
return (todo.text, [], true)
|
|
case let dice as TelegramMediaDice:
|
|
return (dice.emoji, [], true)
|
|
case let invoice as TelegramMediaInvoice:
|
|
return (invoice.title, [], true)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return (message.text, message._asMessage().textEntitiesAttribute?.entities ?? [], false)
|
|
}
|
|
|
|
public final class ChatInputMessageAccessoryPanel: Component {
|
|
public typealias EnvironmentType = ChatInputAccessoryPanelEnvironment
|
|
|
|
public enum Contents: Equatable {
|
|
public final class Reply: Equatable {
|
|
public let id: EngineMessage.Id
|
|
public let quote: EngineMessageReplyQuote?
|
|
public let todoItemId: Int32?
|
|
public let message: EngineMessage?
|
|
|
|
public init(id: EngineMessage.Id, quote: EngineMessageReplyQuote?, todoItemId: Int32?, message: EngineMessage?) {
|
|
self.id = id
|
|
self.quote = quote
|
|
self.todoItemId = todoItemId
|
|
self.message = message
|
|
}
|
|
|
|
public static func ==(lhs: Reply, rhs: Reply) -> Bool {
|
|
if lhs.id != rhs.id {
|
|
return false
|
|
}
|
|
if lhs.quote != rhs.quote {
|
|
return false
|
|
}
|
|
if lhs.todoItemId != rhs.todoItemId {
|
|
return false
|
|
}
|
|
if lhs.message?.id != rhs.message?.id {
|
|
return false
|
|
}
|
|
if lhs.message?.stableVersion != rhs.message?.stableVersion {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public final class Edit: Equatable {
|
|
public let id: EngineMessage.Id
|
|
public let message: EngineMessage?
|
|
|
|
public init(id: EngineMessage.Id, message: EngineMessage?) {
|
|
self.id = id
|
|
self.message = message
|
|
}
|
|
|
|
public static func ==(lhs: Edit, rhs: Edit) -> Bool {
|
|
if lhs.id != rhs.id {
|
|
return false
|
|
}
|
|
if lhs.message?.id != rhs.message?.id {
|
|
return false
|
|
}
|
|
if lhs.message?.stableVersion != rhs.message?.stableVersion {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public final class Forward: Equatable {
|
|
public let messageIds: [EngineMessage.Id]
|
|
public let forwardOptionsState: ChatInterfaceForwardOptionsState?
|
|
|
|
public init(messageIds: [EngineMessage.Id], forwardOptionsState: ChatInterfaceForwardOptionsState?) {
|
|
self.messageIds = messageIds
|
|
self.forwardOptionsState = forwardOptionsState
|
|
}
|
|
|
|
public static func ==(lhs: Forward, rhs: Forward) -> Bool {
|
|
if lhs.messageIds != rhs.messageIds {
|
|
return false
|
|
}
|
|
if lhs.forwardOptionsState != rhs.forwardOptionsState {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public final class LinkPreview: Equatable {
|
|
public let url: String
|
|
public let webpage: TelegramMediaWebpage
|
|
|
|
public init(url: String, webpage: TelegramMediaWebpage) {
|
|
self.url = url
|
|
self.webpage = webpage
|
|
}
|
|
|
|
public static func ==(lhs: LinkPreview, rhs: LinkPreview) -> Bool {
|
|
if lhs.url != rhs.url {
|
|
return false
|
|
}
|
|
if lhs.webpage != rhs.webpage {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public final class SuggestPost: Equatable {
|
|
public let state: ChatInterfaceState.PostSuggestionState
|
|
|
|
public init(state: ChatInterfaceState.PostSuggestionState) {
|
|
self.state = state
|
|
}
|
|
|
|
public static func ==(lhs: SuggestPost, rhs: SuggestPost) -> Bool {
|
|
if lhs.state != rhs.state {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
case reply(Reply)
|
|
case edit(Edit)
|
|
case forward(Forward)
|
|
case linkPreview(LinkPreview)
|
|
case suggestPost(SuggestPost)
|
|
}
|
|
|
|
let context: AccountContext
|
|
let contents: Contents
|
|
let chatPeerId: EnginePeer.Id?
|
|
let action: ((UIView) -> Void)?
|
|
let dismiss: (UIView) -> Void
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
contents: Contents,
|
|
chatPeerId: EnginePeer.Id?,
|
|
action: ((UIView) -> Void)?,
|
|
dismiss: @escaping (UIView) -> Void
|
|
) {
|
|
self.context = context
|
|
self.contents = contents
|
|
self.chatPeerId = chatPeerId
|
|
self.action = action
|
|
self.dismiss = dismiss
|
|
}
|
|
|
|
public static func ==(lhs: ChatInputMessageAccessoryPanel, rhs: ChatInputMessageAccessoryPanel) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.contents != rhs.contents {
|
|
return false
|
|
}
|
|
if lhs.chatPeerId != rhs.chatPeerId {
|
|
return false
|
|
}
|
|
if (lhs.action == nil) != (rhs.action == nil) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView, ChatInputAccessoryPanelView {
|
|
private let closeButton: HighlightTrackingButton
|
|
private let closeButtonIcon: GlassBackgroundView.ContentImageView
|
|
|
|
private let lineView: UIImageView
|
|
private let titleNode: CompositeTextNode
|
|
private let text = ComponentView<Empty>()
|
|
private let tintText = ComponentView<Empty>()
|
|
|
|
public let contentTintView: UIView
|
|
|
|
private var isUpdating: Bool = false
|
|
private var component: ChatInputMessageAccessoryPanel?
|
|
private weak var state: EmptyComponentState?
|
|
private var environment: EnvironmentType?
|
|
|
|
private var messages: [EngineMessage] = []
|
|
private var contentDisposable: Disposable?
|
|
|
|
private var inlineTextStarImage: UIImage?
|
|
private var inlineTextTonImage: (UIImage, UIColor)?
|
|
|
|
public var transitionData: ChatInputAccessoryPanelTransitionData? {
|
|
guard let textView = self.text.view else {
|
|
return nil
|
|
}
|
|
return ChatInputAccessoryPanelTransitionData(
|
|
titleView: self.titleNode.view,
|
|
textView: textView,
|
|
lineView: self.lineView,
|
|
imageView: nil
|
|
)
|
|
}
|
|
|
|
public var storedFrameBeforeDismissed: CGRect?
|
|
|
|
override public init(frame: CGRect) {
|
|
self.contentTintView = UIView()
|
|
|
|
self.closeButton = HighlightTrackingButton()
|
|
self.closeButtonIcon = GlassBackgroundView.ContentImageView()
|
|
|
|
self.lineView = UIImageView()
|
|
self.titleNode = CompositeTextNode()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.lineView)
|
|
self.addSubview(self.titleNode.view)
|
|
|
|
self.addSubview(self.closeButtonIcon)
|
|
self.contentTintView.addSubview(self.closeButtonIcon.tintMask)
|
|
self.addSubview(self.closeButton)
|
|
|
|
self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), for: .touchUpInside)
|
|
|
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.contentDisposable?.dispose()
|
|
}
|
|
|
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
if case .ended = recognizer.state {
|
|
component.action?(self)
|
|
}
|
|
}
|
|
|
|
@objc private func closeButtonPressed() {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
component.dismiss(self)
|
|
}
|
|
|
|
public func update(component: ChatInputMessageAccessoryPanel, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
self.isUpdating = true
|
|
defer {
|
|
self.isUpdating = false
|
|
}
|
|
|
|
let environment = environment[EnvironmentType.self].value
|
|
|
|
let messageIdsFromComponent: (ChatInputMessageAccessoryPanel) -> [EngineMessage.Id] = { component in
|
|
let messageIds: [EngineMessage.Id]
|
|
switch component.contents {
|
|
case let .edit(edit):
|
|
messageIds = [edit.id]
|
|
case let .reply(reply):
|
|
messageIds = [reply.id]
|
|
case let .forward(forward):
|
|
messageIds = forward.messageIds
|
|
case .linkPreview, .suggestPost:
|
|
messageIds = []
|
|
}
|
|
return messageIds
|
|
}
|
|
|
|
let messageIds = messageIdsFromComponent(component)
|
|
if self.component == nil || self.component.flatMap(messageIdsFromComponent) != messageIds {
|
|
self.contentDisposable?.dispose()
|
|
if !messageIds.isEmpty {
|
|
self.contentDisposable = (component.context.engine.data.subscribe(
|
|
EngineDataList(messageIds.map { id in
|
|
return TelegramEngine.EngineData.Item.Messages.Message(id: id)
|
|
})
|
|
)
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] messages in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.messages = messages.compactMap { $0 }
|
|
if !self.isUpdating {
|
|
self.state?.updated(transition: .immediate, isLocal: true)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
self.component = component
|
|
self.state = state
|
|
self.environment = environment
|
|
|
|
if self.closeButtonIcon.image == nil {
|
|
self.closeButtonIcon.image = generateCloseIcon()
|
|
}
|
|
if self.lineView.image == nil {
|
|
self.lineView.image = generateImage(CGSize(width: 2.0, height: 3.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setFillColor(UIColor.white.cgColor)
|
|
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 1.0).cgPath)
|
|
context.fillPath()
|
|
})?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 0, topCapHeight: 1)
|
|
}
|
|
|
|
let size = CGSize(width: availableSize.width, height: 52.0)
|
|
|
|
let containerInsets = UIEdgeInsets(top: 8.0, left: 12.0, bottom: 6.0, right: 0.0)
|
|
|
|
let lineSize = CGSize(width: 2.0, height: size.height - containerInsets.top - containerInsets.bottom)
|
|
let lineFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: containerInsets.top), size: lineSize)
|
|
transition.setFrame(view: self.lineView, frame: lineFrame)
|
|
self.lineView.tintColor = environment.theme.chat.inputPanel.panelControlAccentColor
|
|
|
|
let closeButtonSize = CGSize(width: 44.0, height: 44.0)
|
|
let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - closeButtonSize.width, y: floor((size.height - closeButtonSize.height) * 0.5)), size: closeButtonSize)
|
|
transition.setFrame(view: self.closeButton, frame: closeButtonFrame)
|
|
|
|
if let image = self.closeButtonIcon.image {
|
|
let closeButtonIconFrame = image.size.centered(in: closeButtonFrame)
|
|
transition.setFrame(view: self.closeButtonIcon, frame: closeButtonIconFrame)
|
|
}
|
|
self.closeButtonIcon.tintColor = environment.theme.chat.inputPanel.inputControlColor
|
|
|
|
let secondaryTextColor = environment.theme.chat.inputPanel.inputPlaceholderColor
|
|
|
|
var textString: NSAttributedString
|
|
var isPhoto = false
|
|
if self.messages.count == 1, let message = self.messages.first {
|
|
var text = ""
|
|
let effectiveMessage = message
|
|
//TODO:release media
|
|
/*if let currentEditMediaReference = self.currentEditMediaReference {
|
|
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
|
|
}*/
|
|
let (attributedText, _, _) = descriptionStringForMessage(
|
|
contentSettings: component.context.currentContentSettings.with { $0 },
|
|
message: effectiveMessage,
|
|
strings: environment.strings,
|
|
nameDisplayOrder: environment.nameDisplayOrder,
|
|
dateTimeFormat: environment.dateTimeFormat,
|
|
accountPeerId: component.context.account.peerId
|
|
)
|
|
text = attributedText.string
|
|
|
|
var updatedMediaReference: AnyMediaReference?
|
|
var imageDimensions: CGSize?
|
|
if !message._asMessage().containsSecretMedia {
|
|
var candidateMediaReference: AnyMediaReference?
|
|
for media in message.media {
|
|
if media is TelegramMediaImage || media is TelegramMediaFile {
|
|
candidateMediaReference = .message(message: MessageReference(message._asMessage()), media: media)
|
|
break
|
|
}
|
|
}
|
|
|
|
if let imageReference = candidateMediaReference?.concrete(TelegramMediaImage.self) {
|
|
updatedMediaReference = imageReference.abstract
|
|
if let representation = largestRepresentationForPhoto(imageReference.media) {
|
|
imageDimensions = representation.dimensions.cgSize
|
|
}
|
|
} else if let fileReference = candidateMediaReference?.concrete(TelegramMediaFile.self) {
|
|
updatedMediaReference = fileReference.abstract
|
|
if !fileReference.media.isInstantVideo, let representation = largestImageRepresentation(fileReference.media.previewRepresentations), !fileReference.media.isSticker {
|
|
imageDimensions = representation.dimensions.cgSize
|
|
}
|
|
}
|
|
}
|
|
|
|
/*let imageNodeLayout = self.imageNode.asyncLayout()
|
|
var applyImage: (() -> Void)?
|
|
if let imageDimensions = imageDimensions {
|
|
let boundingSize = CGSize(width: 35.0, height: 35.0)
|
|
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
|
|
}
|
|
|
|
var mediaUpdated = false
|
|
if let updatedMediaReference = updatedMediaReference, let previousMediaReference = self.previousMediaReference {
|
|
mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media)
|
|
} else if (updatedMediaReference != nil) != (self.previousMediaReference != nil) {
|
|
mediaUpdated = true
|
|
}
|
|
self.previousMediaReference = updatedMediaReference*/
|
|
|
|
let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute })
|
|
|
|
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
|
let _ = updateImageSignal
|
|
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
|
|
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
|
|
updateImageSignal = chatMessagePhotoThumbnail(account: component.context.account, userLocation: MediaResourceUserLocation.peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler)
|
|
isPhoto = true
|
|
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
|
|
if fileReference.media.isVideo {
|
|
updateImageSignal = chatMessageVideoThumbnail(account: component.context.account, userLocation: MediaResourceUserLocation.peer(message.id.peerId), fileReference: fileReference, blurred: hasSpoiler)
|
|
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
|
|
updateImageSignal = chatWebpageSnippetFile(account: component.context.account, userLocation: MediaResourceUserLocation.peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation)
|
|
}
|
|
}
|
|
} else {
|
|
updateImageSignal = .single({ _ in return nil })
|
|
}
|
|
|
|
let isMedia: Bool
|
|
let isText: Bool
|
|
/*if let currentEditMediaReference = self.currentEditMediaReference {
|
|
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
|
|
}*/
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
switch messageContentKind(contentSettings: component.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: environment.strings, nameDisplayOrder: environment.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: component.context.account.peerId) {
|
|
case .text:
|
|
isMedia = false
|
|
isText = true
|
|
default:
|
|
isMedia = effectiveMessage.text.isEmpty
|
|
isText = false
|
|
}
|
|
|
|
let textFont = Font.regular(14.0)
|
|
let messageText: NSAttributedString
|
|
if isText {
|
|
let entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in
|
|
switch entity.type {
|
|
case .Spoiler, .CustomEmoji:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
let textColor = environment.theme.chat.inputPanel.primaryTextColor
|
|
if entities.count > 0 {
|
|
messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
|
|
} else {
|
|
messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? secondaryTextColor : environment.theme.chat.inputPanel.primaryTextColor)
|
|
}
|
|
} else {
|
|
messageText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: isMedia ? secondaryTextColor : environment.theme.chat.inputPanel.primaryTextColor)
|
|
}
|
|
textString = messageText
|
|
} else {
|
|
textString = NSAttributedString()
|
|
}
|
|
|
|
var titleText: [CompositeTextNode.Component] = []
|
|
switch component.contents {
|
|
case .edit:
|
|
let canEditMedia: Bool
|
|
//TODO:release
|
|
/*if let message = self.message, !messageMediaEditingOptions(message: message).isEmpty {
|
|
canEditMedia = true
|
|
} else {
|
|
canEditMedia = false
|
|
}*/
|
|
canEditMedia = !"".isEmpty
|
|
|
|
let titleStringValue: String
|
|
if let message = self.messages.first, message.id.namespace == Namespaces.Message.QuickReplyCloud {
|
|
titleStringValue = environment.strings.Conversation_EditingQuickReplyPanelTitle
|
|
} else if canEditMedia {
|
|
titleStringValue = isPhoto ? environment.strings.Conversation_EditingPhotoPanelTitle : environment.strings.Conversation_EditingCaptionPanelTitle
|
|
} else {
|
|
titleStringValue = environment.strings.Conversation_EditingMessagePanelTitle
|
|
}
|
|
titleText = [.text(NSAttributedString(string: titleStringValue, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
|
|
case let .reply(reply):
|
|
if let peer = self.messages.first?.peers[reply.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
|
|
let icon: UIImage?
|
|
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon"), color: environment.theme.chat.inputPanel.panelControlAccentColor)
|
|
|
|
if let icon {
|
|
let rawString: PresentationStrings.FormattedString
|
|
if reply.quote != nil {
|
|
rawString = environment.strings.Chat_ReplyPanel_ReplyToQuoteBy(peer.debugDisplayTitle)
|
|
} else {
|
|
rawString = environment.strings.Chat_ReplyPanel_ReplyTo(peer.debugDisplayTitle)
|
|
}
|
|
if let nameRange = rawString.ranges.first {
|
|
titleText = []
|
|
|
|
let rawNsString = rawString.string as NSString
|
|
if nameRange.range.lowerBound != 0 {
|
|
titleText.append(.text(NSAttributedString(string: rawNsString.substring(with: NSRange(location: 0, length: nameRange.range.lowerBound)), font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
|
|
}
|
|
titleText.append(.icon(icon))
|
|
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
|
|
|
|
if nameRange.range.upperBound != rawNsString.length {
|
|
titleText.append(.text(NSAttributedString(string: rawNsString.substring(with: NSRange(location: nameRange.range.upperBound, length: rawNsString.length - nameRange.range.upperBound)), font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
|
|
}
|
|
} else {
|
|
titleText.append(.text(NSAttributedString(string: rawString.string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
|
|
}
|
|
}
|
|
} else {
|
|
var authorName = ""
|
|
if let forwardInfo = self.messages.first?._asMessage().forwardInfo, forwardInfo.flags.contains(.isImported) {
|
|
if let author = forwardInfo.author {
|
|
authorName = EnginePeer(author).displayTitle(strings: environment.strings, displayOrder: environment.nameDisplayOrder)
|
|
} else if let authorSignature = forwardInfo.authorSignature {
|
|
authorName = authorSignature
|
|
}
|
|
} else if let author = self.messages.first?._asMessage().effectiveAuthor {
|
|
authorName = EnginePeer(author).displayTitle(strings: environment.strings, displayOrder: environment.nameDisplayOrder)
|
|
}
|
|
|
|
if let _ = reply.todoItemId {
|
|
let string = environment.strings.Chat_ReplyPanel_ReplyToTodoItem
|
|
titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
|
|
} else if let _ = reply.quote {
|
|
let string = environment.strings.Chat_ReplyPanel_ReplyToQuoteBy(authorName).string
|
|
titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
|
|
} else {
|
|
let string = environment.strings.Conversation_ReplyMessagePanelTitle(authorName).string
|
|
titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
|
|
}
|
|
|
|
if reply.id.peerId != component.chatPeerId {
|
|
if let peer = self.messages.first?.peers[reply.id.peerId], (peer is TelegramChannel || peer is TelegramGroup) {
|
|
let icon: UIImage?
|
|
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
|
icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon")
|
|
} else {
|
|
icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon")
|
|
}
|
|
if let iconImage = generateTintedImage(image: icon, color: environment.theme.chat.inputPanel.panelControlAccentColor) {
|
|
titleText.append(.icon(iconImage))
|
|
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
|
|
}
|
|
}
|
|
}
|
|
|
|
if let message = self.messages.first {
|
|
let textFont = Font.regular(14.0)
|
|
if let quote = reply.quote {
|
|
let textColor = environment.theme.chat.inputPanel.primaryTextColor
|
|
textString = stringWithAppliedEntities(trimToLineCount(quote.text, lineCount: 1), entities: quote.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
|
|
} else if let todoItemId = reply.todoItemId, let todo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) {
|
|
let textColor = environment.theme.chat.inputPanel.primaryTextColor
|
|
textString = stringWithAppliedEntities(trimToLineCount(todoItem.text, lineCount: 1), entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
|
|
}
|
|
}
|
|
}
|
|
case let .forward(forward):
|
|
var title = ""
|
|
var authors = ""
|
|
var uniquePeerIds = Set<EnginePeer.Id>()
|
|
var text = NSMutableAttributedString(string: "")
|
|
|
|
for message in self.messages {
|
|
if let author = message.forwardInfo?.author ?? message._asMessage().effectiveAuthor, !uniquePeerIds.contains(author.id) {
|
|
uniquePeerIds.insert(author.id)
|
|
if !authors.isEmpty {
|
|
authors.append(", ")
|
|
}
|
|
if author.id == component.context.account.peerId {
|
|
authors.append(environment.strings.DialogList_You)
|
|
} else {
|
|
authors.append(EnginePeer(author).compactDisplayTitle)
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.messages.count == 1 {
|
|
title = environment.strings.Conversation_ForwardOptions_ForwardTitleSingle
|
|
let (string, entities, _) = textStringForForwardedMessage(messages[0], strings: environment.strings)
|
|
|
|
text = NSMutableAttributedString(attributedString: NSAttributedString(string: "\(authors): ", font: Font.regular(14.0), textColor: secondaryTextColor))
|
|
|
|
let additionalText = NSMutableAttributedString(attributedString: NSAttributedString(string: string, font: Font.regular(14.0), textColor: secondaryTextColor))
|
|
for entity in entities {
|
|
switch entity.type {
|
|
case let .CustomEmoji(_, fileId):
|
|
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
|
if range.lowerBound >= 0 && range.upperBound <= additionalText.length {
|
|
additionalText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: messages[0].associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile), range: range)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
text.append(additionalText)
|
|
} else {
|
|
title = environment.strings.Conversation_ForwardOptions_ForwardTitle(Int32(messages.count))
|
|
text = NSMutableAttributedString(attributedString: NSAttributedString(string: environment.strings.Conversation_ForwardFrom(authors).string, font: Font.regular(14.0), textColor: secondaryTextColor))
|
|
}
|
|
|
|
if forward.forwardOptionsState?.hideNames == true {
|
|
text = NSMutableAttributedString(attributedString: NSAttributedString(string: environment.strings.Conversation_ForwardOptions_SenderNamesRemoved, font: Font.regular(14.0), textColor: secondaryTextColor))
|
|
}
|
|
|
|
titleText = [.text(NSAttributedString(string: title, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
|
|
textString = text
|
|
case let .linkPreview(linkPreview):
|
|
var authorName = ""
|
|
var text = ""
|
|
switch linkPreview.webpage.content {
|
|
case .Pending:
|
|
authorName = environment.strings.Channel_NotificationLoading
|
|
text = linkPreview.url
|
|
case let .Loaded(content):
|
|
if let contentText = content.text {
|
|
text = contentText
|
|
} else {
|
|
if let file = content.file, let mediaKind = mediaContentKind(EngineMedia(file)) {
|
|
if content.type == "telegram_background" {
|
|
text = environment.strings.Message_Wallpaper
|
|
} else if content.type == "telegram_theme" {
|
|
text = environment.strings.Message_Theme
|
|
} else {
|
|
text = stringForMediaKind(mediaKind, strings: environment.strings).0.string
|
|
}
|
|
} else if content.type == "telegram_theme" {
|
|
text = environment.strings.Message_Theme
|
|
} else if content.type == "video" {
|
|
text = stringForMediaKind(.video, strings: environment.strings).0.string
|
|
} else if content.type == "telegram_story" {
|
|
text = stringForMediaKind(.story, strings: environment.strings).0.string
|
|
} else if let _ = content.image {
|
|
text = stringForMediaKind(.image, strings: environment.strings).0.string
|
|
}
|
|
}
|
|
|
|
if let title = content.title {
|
|
authorName = title
|
|
} else if let websiteName = content.websiteName {
|
|
authorName = websiteName
|
|
} else {
|
|
authorName = content.displayUrl
|
|
}
|
|
}
|
|
|
|
titleText = [.text(NSAttributedString(string: authorName, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
|
|
textString = NSAttributedString(string: text, font: Font.regular(14.0), textColor: environment.theme.chat.inputPanel.primaryTextColor)
|
|
case let .suggestPost(suggestPost):
|
|
if suggestPost.state.editingOriginalMessageId != nil {
|
|
titleText.append(.text(NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputEditTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
|
|
} else {
|
|
titleText.append(.text(NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
|
|
}
|
|
|
|
let textFont = Font.regular(14.0)
|
|
|
|
if let price = suggestPost.state.price, price.amount != .zero {
|
|
let currencySymbol: String
|
|
let amountString: String
|
|
switch price.currency {
|
|
case .stars:
|
|
currencySymbol = "#"
|
|
amountString = "\(price.amount)"
|
|
case .ton:
|
|
currencySymbol = "$"
|
|
amountString = formatTonAmountText(price.amount.value, dateTimeFormat: environment.dateTimeFormat)
|
|
}
|
|
if let timestamp = suggestPost.state.timestamp {
|
|
let timeString = humanReadableStringForTimestamp(strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, timestamp: timestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat(
|
|
dateFormatString: { value in
|
|
return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_Date(value).string, ranges: [])
|
|
},
|
|
tomorrowFormatString: { value in
|
|
return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_TomorrowAt(value).string, ranges: [])
|
|
},
|
|
todayFormatString: { value in
|
|
return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_TodayAt(value).string, ranges: [])
|
|
},
|
|
yesterdayFormatString: { value in
|
|
return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_TodayAt(value).string, ranges: [])
|
|
}
|
|
)).string
|
|
textString = NSAttributedString(string: "\(currencySymbol)\(amountString) 📅 \(timeString)", font: textFont, textColor: environment.theme.chat.inputPanel.primaryTextColor)
|
|
} else {
|
|
textString = NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputSubtitleAnytime("\(currencySymbol)\(amountString)").string, font: textFont, textColor: environment.theme.chat.inputPanel.primaryTextColor)
|
|
}
|
|
} else {
|
|
textString = NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputSubtitleEmpty, font: textFont, textColor: environment.theme.chat.inputPanel.primaryTextColor)
|
|
}
|
|
|
|
let mutableTextString = NSMutableAttributedString(attributedString: textString)
|
|
for currency in [.stars, .ton] as [CurrencyAmount.Currency] {
|
|
var inlineTextStarImage: UIImage?
|
|
if let current = self.inlineTextStarImage {
|
|
inlineTextStarImage = current
|
|
} else {
|
|
if let image = UIImage(bundleImageName: "Premium/Stars/StarSmall") {
|
|
let starInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
|
inlineTextStarImage = generateImage(CGSize(width: starInsets.left + image.size.width + starInsets.right, height: image.size.height), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
UIGraphicsPushContext(context)
|
|
defer {
|
|
UIGraphicsPopContext()
|
|
}
|
|
|
|
image.draw(at: CGPoint(x: starInsets.left, y: starInsets.top))
|
|
})?.withRenderingMode(.alwaysOriginal)
|
|
self.inlineTextStarImage = inlineTextStarImage
|
|
}
|
|
}
|
|
|
|
var inlineTextTonImage: UIImage?
|
|
if let current = self.inlineTextTonImage, current.1 == environment.theme.list.itemAccentColor {
|
|
inlineTextTonImage = current.0
|
|
} else {
|
|
if let image = UIImage(bundleImageName: "Ads/TonMedium") {
|
|
let tonInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
|
let inlineTextTonImageValue = generateTintedImage(image: generateImage(CGSize(width: tonInsets.left + image.size.width + tonInsets.right, height: image.size.height), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
UIGraphicsPushContext(context)
|
|
defer {
|
|
UIGraphicsPopContext()
|
|
}
|
|
|
|
image.draw(at: CGPoint(x: tonInsets.left, y: tonInsets.top))
|
|
}), color: environment.theme.list.itemAccentColor)!.withRenderingMode(.alwaysOriginal)
|
|
inlineTextTonImage = inlineTextTonImageValue
|
|
self.inlineTextTonImage = (inlineTextTonImageValue, environment.theme.list.itemAccentColor)
|
|
}
|
|
}
|
|
|
|
let currencySymbol: String
|
|
let currencyImage: UIImage?
|
|
switch currency {
|
|
case .stars:
|
|
currencySymbol = "#"
|
|
currencyImage = inlineTextStarImage
|
|
case .ton:
|
|
currencySymbol = "$"
|
|
currencyImage = inlineTextTonImage
|
|
}
|
|
|
|
if let range = mutableTextString.string.range(of: currencySymbol), let currencyImage {
|
|
final class RunDelegateData {
|
|
let ascent: CGFloat
|
|
let descent: CGFloat
|
|
let width: CGFloat
|
|
|
|
init(ascent: CGFloat, descent: CGFloat, width: CGFloat) {
|
|
self.ascent = ascent
|
|
self.descent = descent
|
|
self.width = width
|
|
}
|
|
}
|
|
|
|
let runDelegateData = RunDelegateData(
|
|
ascent: Font.regular(14.0).ascender,
|
|
descent: Font.regular(14.0).descender,
|
|
width: currencyImage.size.width + 2.0
|
|
)
|
|
var callbacks = CTRunDelegateCallbacks(
|
|
version: kCTRunDelegateCurrentVersion,
|
|
dealloc: { dataRef in
|
|
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
|
|
},
|
|
getAscent: { dataRef in
|
|
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
|
|
return data.takeUnretainedValue().ascent
|
|
},
|
|
getDescent: { dataRef in
|
|
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
|
|
return data.takeUnretainedValue().descent
|
|
},
|
|
getWidth: { dataRef in
|
|
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
|
|
return data.takeUnretainedValue().width
|
|
}
|
|
)
|
|
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
|
|
mutableTextString.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: NSRange(range, in: mutableTextString.string))
|
|
}
|
|
mutableTextString.addAttribute(.attachment, value: currencyImage, range: NSRange(range, in: mutableTextString.string))
|
|
mutableTextString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: mutableTextString.string))
|
|
mutableTextString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: mutableTextString.string))
|
|
}
|
|
|
|
textString = mutableTextString
|
|
}
|
|
}
|
|
|
|
let textInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 0.0, right: 44.0)
|
|
|
|
self.titleNode.components = titleText
|
|
let titleSize = self.titleNode.update(constrainedSize: CGSize(width: availableSize.width - lineFrame.maxX - textInsets.left - textInsets.right, height: 100.0))
|
|
|
|
let textSize = self.text.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(textString),
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - lineFrame.maxX - textInsets.left - textInsets.right, height: 100.0)
|
|
)
|
|
let tintTextString = NSMutableAttributedString(attributedString: textString)
|
|
tintTextString.addAttribute(.foregroundColor, value: UIColor.black, range: NSRange(location: 0, length: tintTextString.length))
|
|
let _ = self.tintText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(tintTextString),
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - lineFrame.maxX - textInsets.left - textInsets.right, height: 100.0)
|
|
)
|
|
|
|
let titleTextSpacing: CGFloat = 1.0
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: lineFrame.maxX + textInsets.left, y: textInsets.top), size: titleSize)
|
|
let textFrame = CGRect(origin: CGPoint(x: lineFrame.maxX + textInsets.left, y: titleFrame.maxY + titleTextSpacing), size: textSize)
|
|
|
|
transition.setFrame(view: self.titleNode.view, frame: titleFrame)
|
|
|
|
if let textView = self.text.view, let tintTextView = self.tintText.view {
|
|
if textView.superview == nil {
|
|
textView.layer.anchorPoint = CGPoint()
|
|
self.addSubview(textView)
|
|
|
|
tintTextView.layer.anchorPoint = CGPoint()
|
|
self.contentTintView.addSubview(tintTextView)
|
|
}
|
|
transition.setPosition(view: textView, position: textFrame.origin)
|
|
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
|
|
|
transition.setPosition(view: tintTextView, position: textFrame.origin)
|
|
tintTextView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|