mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
[WIP] Quick replies
This commit is contained in:
@@ -6,173 +6,168 @@ import TelegramCore
|
||||
import AccountContext
|
||||
|
||||
final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtocol {
|
||||
private final class PendingMessageContext {
|
||||
let disposable = MetaDisposable()
|
||||
var message: Message?
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
private final class Impl {
|
||||
let queue: Queue
|
||||
let context: AccountContext
|
||||
|
||||
private var messages: [Message] = []
|
||||
private var nextMessageId: Int32 = 1000
|
||||
let messagesPromise = Promise<[Message]>([])
|
||||
private var shortcut: String
|
||||
private var shortcutId: Int32?
|
||||
|
||||
private var nextGroupingId: UInt32 = 0
|
||||
private var groupingKeyToGroupId: [Int64: UInt32] = [:]
|
||||
private(set) var mergedHistoryView: MessageHistoryView?
|
||||
private var sourceHistoryView: MessageHistoryView?
|
||||
|
||||
init(queue: Queue, context: AccountContext, messages: [EngineMessage]) {
|
||||
private var pendingMessages: [PendingMessageContext] = []
|
||||
private var historyViewDisposable: Disposable?
|
||||
let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>()
|
||||
private var nextUpdateIsHoleFill: Bool = false
|
||||
|
||||
init(queue: Queue, context: AccountContext, shortcut: String, shortcutId: Int32?) {
|
||||
self.queue = queue
|
||||
self.context = context
|
||||
self.messages = messages.map { $0._asMessage() }
|
||||
self.notifyMessagesUpdated()
|
||||
self.shortcut = shortcut
|
||||
self.shortcutId = shortcutId
|
||||
|
||||
if let maxMessageId = messages.map(\.id).max() {
|
||||
self.nextMessageId = maxMessageId.id + 1
|
||||
}
|
||||
if let maxGroupingId = messages.compactMap(\.groupInfo?.stableId).max() {
|
||||
self.nextGroupingId = maxGroupingId + 1
|
||||
}
|
||||
self.updateHistoryViewRequest(reload: false)
|
||||
}
|
||||
|
||||
deinit {
|
||||
for context in self.pendingMessages {
|
||||
context.disposable.dispose()
|
||||
}
|
||||
self.historyViewDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func notifyMessagesUpdated() {
|
||||
self.messages.sort(by: { $0.index > $1.index })
|
||||
self.messagesPromise.set(.single(self.messages))
|
||||
private func updateHistoryViewRequest(reload: Bool) {
|
||||
if let shortcutId = self.shortcutId {
|
||||
if self.historyViewDisposable == nil || reload {
|
||||
self.historyViewDisposable?.dispose()
|
||||
|
||||
self.historyViewDisposable = (self.context.account.viewTracker.quickReplyMessagesViewForLocation(quickReplyId: shortcutId)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] view, update, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if update == .FillHole {
|
||||
self.nextUpdateIsHoleFill = true
|
||||
self.updateHistoryViewRequest(reload: true)
|
||||
return
|
||||
}
|
||||
|
||||
let nextUpdateIsHoleFill = self.nextUpdateIsHoleFill
|
||||
self.nextUpdateIsHoleFill = false
|
||||
|
||||
self.sourceHistoryView = view
|
||||
self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if self.sourceHistoryView == nil {
|
||||
let sourceHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: [], holeEarlier: false, holeLater: false, isLoading: false)
|
||||
self.sourceHistoryView = sourceHistoryView
|
||||
self.updateHistoryView(updateType: .Initial)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateHistoryView(updateType: ViewUpdateType) {
|
||||
var entries = self.sourceHistoryView?.entries ?? []
|
||||
for pendingMessage in self.pendingMessages {
|
||||
if let message = pendingMessage.message {
|
||||
if !entries.contains(where: { $0.message.stableId == message.stableId }) {
|
||||
entries.append(MessageHistoryEntry(
|
||||
message: message,
|
||||
isRead: true,
|
||||
location: nil,
|
||||
monthLocation: nil,
|
||||
attributes: MutableMessageHistoryEntryAttributes(
|
||||
authorIsContact: false
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
entries.sort(by: { $0.message.index < $1.message.index })
|
||||
|
||||
let mergedHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: entries, holeEarlier: false, holeLater: false, isLoading: false)
|
||||
self.mergedHistoryView = mergedHistoryView
|
||||
|
||||
self.historyViewStream.putNext((mergedHistoryView, updateType))
|
||||
}
|
||||
|
||||
func enqueueMessages(messages: [EnqueueMessage]) {
|
||||
for message in messages {
|
||||
switch message {
|
||||
case let .message(text, attributes, _, mediaReference, _, _, _, localGroupingKey, correlationId, _):
|
||||
let _ = attributes
|
||||
let _ = mediaReference
|
||||
let _ = correlationId
|
||||
|
||||
let messageId = self.nextMessageId
|
||||
self.nextMessageId += 1
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
attributes.append(OutgoingMessageInfoAttribute(
|
||||
uniqueId: Int64.random(in: Int64.min ... Int64.max),
|
||||
flags: [],
|
||||
acknowledged: true,
|
||||
correlationId: correlationId,
|
||||
bubbleUpEmojiOrStickersets: []
|
||||
))
|
||||
|
||||
var media: [Media] = []
|
||||
if let mediaReference {
|
||||
media.append(mediaReference.media)
|
||||
}
|
||||
|
||||
let mappedMessage = Message(
|
||||
stableId: UInt32(messageId),
|
||||
stableVersion: 0,
|
||||
id: MessageId(
|
||||
peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)),
|
||||
namespace: Namespaces.Message.Local,
|
||||
id: Int32(messageId)
|
||||
),
|
||||
globallyUniqueId: nil,
|
||||
groupingKey: localGroupingKey,
|
||||
groupInfo: localGroupingKey.flatMap { value in
|
||||
if let current = self.groupingKeyToGroupId[value] {
|
||||
return MessageGroupInfo(stableId: current)
|
||||
} else {
|
||||
let groupId = self.nextGroupingId
|
||||
self.nextGroupingId += 1
|
||||
self.groupingKeyToGroupId[value] = groupId
|
||||
return MessageGroupInfo(stableId: groupId)
|
||||
}
|
||||
},
|
||||
threadId: nil,
|
||||
timestamp: messageId,
|
||||
flags: [],
|
||||
tags: [],
|
||||
globalTags: [],
|
||||
localTags: [],
|
||||
customTags: [],
|
||||
forwardInfo: nil,
|
||||
author: nil,
|
||||
text: text,
|
||||
attributes: attributes,
|
||||
media: media,
|
||||
peers: SimpleDictionary(),
|
||||
associatedMessages: SimpleDictionary(),
|
||||
associatedMessageIds: [],
|
||||
associatedMedia: [:],
|
||||
associatedThreadInfo: nil,
|
||||
associatedStories: [:]
|
||||
)
|
||||
self.messages.append(mappedMessage)
|
||||
case .forward:
|
||||
break
|
||||
let threadId = self.shortcutId.flatMap(Int64.init)
|
||||
let _ = (TelegramCore.enqueueMessages(account: self.context.account, peerId: self.context.account.peerId, messages: messages.map { message in
|
||||
return message.withUpdatedThreadId(threadId).withUpdatedAttributes { attributes in
|
||||
var attributes = attributes
|
||||
attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute })
|
||||
attributes.append(OutgoingQuickReplyMessageAttribute(shortcut: self.shortcut))
|
||||
return attributes
|
||||
}
|
||||
}
|
||||
self.notifyMessagesUpdated()
|
||||
})
|
||||
|> deliverOn(self.queue)).startStandalone(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.shortcutId != nil {
|
||||
return
|
||||
}
|
||||
for id in result {
|
||||
if let id {
|
||||
let pendingMessage = PendingMessageContext()
|
||||
self.pendingMessages.append(pendingMessage)
|
||||
pendingMessage.disposable.set((
|
||||
self.context.account.postbox.messageView(id)
|
||||
|> deliverOn(self.queue)
|
||||
).startStrict(next: { [weak self, weak pendingMessage] messageView in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let pendingMessage else {
|
||||
return
|
||||
}
|
||||
pendingMessage.message = messageView.message
|
||||
if let message = pendingMessage.message, message.id.namespace == Namespaces.Message.QuickReplyCloud, let threadId = message.threadId {
|
||||
self.shortcutId = Int32(clamping: threadId)
|
||||
self.updateHistoryViewRequest(reload: true)
|
||||
} else {
|
||||
self.updateHistoryView(updateType: .Generic)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func deleteMessages(ids: [EngineMessage.Id]) {
|
||||
self.messages = self.messages.filter({ !ids.contains($0.id) })
|
||||
self.notifyMessagesUpdated()
|
||||
let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: ids, type: .forEveryone).startStandalone()
|
||||
}
|
||||
|
||||
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
|
||||
guard let index = self.messages.firstIndex(where: { $0.id == id }) else {
|
||||
return
|
||||
}
|
||||
|
||||
func quickReplyUpdateShortcut(value: String) {
|
||||
if let shortcutId = self.shortcutId {
|
||||
self.context.engine.accountData.editMessageShortcut(id: shortcutId, shortcut: value)
|
||||
}
|
||||
let originalMessage = self.messages[index]
|
||||
|
||||
var mappedMedia = originalMessage.media
|
||||
switch media {
|
||||
case .keep:
|
||||
break
|
||||
case let .update(value):
|
||||
mappedMedia = [value.media]
|
||||
}
|
||||
|
||||
var mappedAtrributes = originalMessage.attributes
|
||||
mappedAtrributes.removeAll(where: { $0 is TextEntitiesMessageAttribute })
|
||||
if let entities {
|
||||
mappedAtrributes.append(entities)
|
||||
}
|
||||
|
||||
let mappedMessage = Message(
|
||||
stableId: originalMessage.stableId,
|
||||
stableVersion: originalMessage.stableVersion + 1,
|
||||
id: originalMessage.id,
|
||||
globallyUniqueId: originalMessage.globallyUniqueId,
|
||||
groupingKey: originalMessage.groupingKey,
|
||||
groupInfo: originalMessage.groupInfo,
|
||||
threadId: originalMessage.threadId,
|
||||
timestamp: originalMessage.timestamp,
|
||||
flags: originalMessage.flags,
|
||||
tags: originalMessage.tags,
|
||||
globalTags: originalMessage.globalTags,
|
||||
localTags: originalMessage.localTags,
|
||||
customTags: originalMessage.customTags,
|
||||
forwardInfo: originalMessage.forwardInfo,
|
||||
author: originalMessage.author,
|
||||
text: text,
|
||||
attributes: mappedAtrributes,
|
||||
media: mappedMedia,
|
||||
peers: originalMessage.peers,
|
||||
associatedMessages: originalMessage.associatedMessages,
|
||||
associatedMessageIds: originalMessage.associatedMessageIds,
|
||||
associatedMedia: originalMessage.associatedMedia,
|
||||
associatedThreadInfo: originalMessage.associatedThreadInfo,
|
||||
associatedStories: originalMessage.associatedStories
|
||||
)
|
||||
|
||||
self.messages[index] = mappedMessage
|
||||
self.notifyMessagesUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
var kind: ChatCustomContentsKind
|
||||
|
||||
var messages: Signal<[Message], NoError> {
|
||||
var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> {
|
||||
return self.impl.signalWith({ impl, subscriber in
|
||||
return impl.messagesPromise.get().start(next: subscriber.putNext)
|
||||
if let mergedHistoryView = impl.mergedHistoryView {
|
||||
subscriber.putNext((mergedHistoryView, .Initial))
|
||||
}
|
||||
return impl.historyViewStream.signal().start(next: subscriber.putNext)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -183,13 +178,23 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
init(context: AccountContext, messages: [EngineMessage], kind: ChatCustomContentsKind) {
|
||||
init(context: AccountContext, kind: ChatCustomContentsKind, shortcutId: Int32?) {
|
||||
self.kind = kind
|
||||
|
||||
let initialShortcut: String
|
||||
switch kind {
|
||||
case .awayMessageInput:
|
||||
initialShortcut = "_away"
|
||||
case .greetingMessageInput:
|
||||
initialShortcut = "_greeting"
|
||||
case let .quickReplyMessageInput(shortcut):
|
||||
initialShortcut = shortcut
|
||||
}
|
||||
|
||||
let queue = Queue()
|
||||
self.queue = queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, context: context, messages: messages)
|
||||
return Impl(queue: queue, context: context, shortcut: initialShortcut, shortcutId: shortcutId)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,5 +218,8 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
|
||||
|
||||
func quickReplyUpdateShortcut(value: String) {
|
||||
self.kind = .quickReplyMessageInput(shortcut: value)
|
||||
self.impl.with { impl in
|
||||
impl.quickReplyUpdateShortcut(value: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,8 +352,8 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
}
|
||||
let contents = AutomaticBusinessMessageSetupChatContents(
|
||||
context: component.context,
|
||||
messages: self.messages,
|
||||
kind: component.mode == .away ? .awayMessageInput : .greetingMessageInput
|
||||
kind: component.mode == .away ? .awayMessageInput : .greetingMessageInput,
|
||||
shortcutId: nil
|
||||
)
|
||||
let chatController = component.context.sharedContext.makeChatController(
|
||||
context: component.context,
|
||||
@@ -365,17 +365,6 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
chatController.navigationPresentation = .modal
|
||||
self.environment?.controller()?.push(chatController)
|
||||
self.messagesDisposable?.dispose()
|
||||
self.messagesDisposable = (contents.messages
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] messages in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let messages = messages.map(EngineMessage.init)
|
||||
if self.messages != messages {
|
||||
self.messages = messages
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func openCustomScheduleDateSetup(isStartTime: Bool, isDate: Bool) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
final class BottomPanelComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let content: AnyComponentWithIdentity<Empty>
|
||||
let insets: UIEdgeInsets
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
content: AnyComponentWithIdentity<Empty>,
|
||||
insets: UIEdgeInsets
|
||||
) {
|
||||
self.theme = theme
|
||||
self.content = content
|
||||
self.insets = insets
|
||||
}
|
||||
|
||||
static func ==(lhs: BottomPanelComponent, rhs: BottomPanelComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private var content = ComponentView<Empty>()
|
||||
|
||||
private var component: BottomPanelComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.separatorLayer = SimpleLayer()
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
let themeUpdated = previousComponent?.theme !== component.theme
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
contentHeight += component.insets.top
|
||||
|
||||
var contentTransition = transition
|
||||
if let previousComponent, previousComponent.content.id != component.content.id {
|
||||
contentTransition = contentTransition.withAnimation(.none)
|
||||
self.content.view?.removeFromSuperview()
|
||||
self.content = ComponentView<Empty>()
|
||||
}
|
||||
|
||||
let contentSize = self.content.update(
|
||||
transition: contentTransition,
|
||||
component: component.content.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: availableSize.height - component.insets.top - component.insets.bottom)
|
||||
)
|
||||
let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) * 0.5), y: contentHeight), size: contentSize)
|
||||
if let contentView = self.content.view {
|
||||
if contentView.superview == nil {
|
||||
self.addSubview(contentView)
|
||||
}
|
||||
contentTransition.setFrame(view: contentView, frame: contentFrame)
|
||||
}
|
||||
contentHeight += contentSize.height
|
||||
|
||||
contentHeight += component.insets.bottom
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: contentHeight)
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
|
||||
self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
|
||||
}
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import AppBundle
|
||||
import ButtonComponent
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import LottieComponent
|
||||
|
||||
final class QuickReplyEmptyStateComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
@@ -63,72 +64,6 @@ final class QuickReplyEmptyStateComponent: Component {
|
||||
|
||||
let _ = previousComponent
|
||||
|
||||
let iconTitleSpacing: CGFloat = 10.0
|
||||
let titleTextSpacing: CGFloat = 8.0
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "📝", font: Font.semibold(90.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
|
||||
//TODO:localize
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "No Quick Replies", font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Set up shortcuts with rich text and media to respond to messages faster.", font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 20
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
let centralContentsHeight: CGFloat = iconSize.height + iconTitleSpacing + titleSize.height + titleTextSpacing
|
||||
var centralContentsY: CGFloat = component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - centralContentsHeight) * 0.5)
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: centralContentsY), size: iconSize)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
}
|
||||
centralContentsY += iconSize.height + iconTitleSpacing
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: centralContentsY), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
}
|
||||
centralContentsY += titleSize.height + titleTextSpacing
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: centralContentsY), size: textSize)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
transition.setPosition(view: textView, position: textFrame.center)
|
||||
}
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
@@ -159,7 +94,7 @@ final class QuickReplyEmptyStateComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(availableSize.width - 16.0 * 2.0, 280.0), height: 50.0)
|
||||
)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: availableSize.height - component.insets.bottom - 8.0 - buttonSize.height), size: buttonSize)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: availableSize.height - component.insets.bottom - 14.0 - buttonSize.height), size: buttonSize)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
@@ -167,6 +102,75 @@ final class QuickReplyEmptyStateComponent: Component {
|
||||
transition.setFrame(view: buttonView, frame: buttonFrame)
|
||||
}
|
||||
|
||||
let iconTitleSpacing: CGFloat = 13.0
|
||||
let titleTextSpacing: CGFloat = 9.0
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "WriteEmoji"),
|
||||
loop: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 120.0, height: 120.0)
|
||||
)
|
||||
|
||||
//TODO:localize
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "No Quick Replies", font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Set up shortcuts with rich text and media to respond to messages faster.", font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 20,
|
||||
lineSpacing: 0.2
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
let topInset: CGFloat = component.insets.top
|
||||
|
||||
let centralContentsHeight: CGFloat = iconSize.height + iconTitleSpacing + titleSize.height + titleTextSpacing
|
||||
var centralContentsY: CGFloat = topInset + floor((buttonFrame.minY - topInset - centralContentsHeight) * 0.426)
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: centralContentsY), size: iconSize)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
}
|
||||
centralContentsY += iconSize.height + iconTitleSpacing
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: centralContentsY), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
}
|
||||
centralContentsY += titleSize.height + titleTextSpacing
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: centralContentsY), size: textSize)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
transition.setPosition(view: textView, position: textFrame.center)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import ItemListUI
|
||||
import ChatListUI
|
||||
import QuickReplyNameAlertController
|
||||
import ChatListHeaderComponent
|
||||
import PlainButtonComponent
|
||||
import MultilineTextComponent
|
||||
|
||||
final class QuickReplySetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@@ -45,30 +47,30 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
private enum ContentEntry: Comparable, Identifiable {
|
||||
enum Id: Hashable {
|
||||
case add
|
||||
case item(String)
|
||||
case item(Int32)
|
||||
}
|
||||
|
||||
var stableId: Id {
|
||||
switch self {
|
||||
case .add:
|
||||
return .add
|
||||
case let .item(item, _, _, _):
|
||||
return .item(item.shortcut)
|
||||
case let .item(item, _, _, _, _):
|
||||
return .item(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
case add
|
||||
case item(item: QuickReplyMessageShortcut, accountPeer: EnginePeer, sortIndex: Int, isEditing: Bool)
|
||||
case item(item: ShortcutMessageList.Item, accountPeer: EnginePeer, sortIndex: Int, isEditing: Bool, isSelected: Bool)
|
||||
|
||||
static func <(lhs: ContentEntry, rhs: ContentEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .add:
|
||||
return false
|
||||
case let .item(lhsItem, _, lhsSortIndex, _):
|
||||
case let .item(lhsItem, _, lhsSortIndex, _, _):
|
||||
switch rhs {
|
||||
case .add:
|
||||
return false
|
||||
case let .item(rhsItem, _, rhsSortIndex, _):
|
||||
case let .item(rhsItem, _, rhsSortIndex, _, _):
|
||||
if lhsSortIndex != rhsSortIndex {
|
||||
return lhsSortIndex < rhsSortIndex
|
||||
}
|
||||
@@ -97,10 +99,10 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.openQuickReplyChat(shortcut: nil)
|
||||
parentView.openQuickReplyChat(shortcut: nil, shortcutId: nil)
|
||||
}
|
||||
)
|
||||
case let .item(item, accountPeer, _, isEditing):
|
||||
case let .item(item, accountPeer, _, isEditing, isSelected):
|
||||
let chatListNodeInteraction = ChatListNodeInteraction(
|
||||
context: listNode.context,
|
||||
animationCache: listNode.context.animationCache,
|
||||
@@ -111,13 +113,21 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.openQuickReplyChat(shortcut: item.shortcut)
|
||||
parentView.openQuickReplyChat(shortcut: item.shortcut, shortcutId: item.id)
|
||||
},
|
||||
disabledPeerSelected: { _, _, _ in
|
||||
},
|
||||
togglePeerSelected: { _, _ in
|
||||
togglePeerSelected: { [weak listNode] _, _ in
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.toggleShortcutSelection(id: item.id)
|
||||
},
|
||||
togglePeersSelection: { _, _ in
|
||||
togglePeersSelection: { [weak listNode] _, _ in
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.toggleShortcutSelection(id: item.id)
|
||||
},
|
||||
additionalCategorySelected: { _ in
|
||||
},
|
||||
@@ -125,7 +135,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.openQuickReplyChat(shortcut: item.shortcut)
|
||||
parentView.openQuickReplyChat(shortcut: item.shortcut, shortcutId: item.id)
|
||||
},
|
||||
groupSelected: { _ in
|
||||
},
|
||||
@@ -143,7 +153,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.openDeleteShortcut(shortcut: item.shortcut)
|
||||
parentView.openDeleteShortcuts(ids: [item.id])
|
||||
},
|
||||
deletePeerThread: { _, _ in
|
||||
},
|
||||
@@ -193,7 +203,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.openEditShortcut(shortcut: item.shortcut)
|
||||
parentView.openEditShortcut(id: item.id, currentValue: item.shortcut)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -215,7 +225,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
filterData: nil,
|
||||
index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: listNode.context.account.peerId, namespace: 0, id: 0), timestamp: 0))),
|
||||
content: .peer(ChatListItemContent.PeerData(
|
||||
messages: item.messages.first.flatMap({ [$0] }) ?? [],
|
||||
messages: [item.topMessage],
|
||||
peer: EngineRenderedPeer(peer: accountPeer),
|
||||
threadInfo: nil,
|
||||
combinedReadState: nil,
|
||||
@@ -245,7 +255,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
)),
|
||||
editing: isEditing,
|
||||
hasActiveRevealControls: false,
|
||||
selected: false,
|
||||
selected: isSelected,
|
||||
header: nil,
|
||||
enableContextActions: true,
|
||||
hiddenOffset: false,
|
||||
@@ -260,6 +270,10 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
let context: AccountContext
|
||||
var presentationData: PresentationData
|
||||
private var currentEntries: [ContentEntry] = []
|
||||
private var originalEntries: [ContentEntry] = []
|
||||
private var tempOrder: [Int32]?
|
||||
private var pendingRemoveItems: [Int32]?
|
||||
private var resetTempOrderOnNextUpdate: Bool = false
|
||||
|
||||
init(parentView: View, context: AccountContext) {
|
||||
self.parentView = parentView
|
||||
@@ -267,6 +281,87 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
super.init()
|
||||
|
||||
self.reorderBegan = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.tempOrder = nil
|
||||
}
|
||||
self.reorderCompleted = { [weak self] _ in
|
||||
guard let self, let tempOrder = self.tempOrder else {
|
||||
return
|
||||
}
|
||||
self.resetTempOrderOnNextUpdate = true
|
||||
self.context.engine.accountData.reorderMessageShortcuts(ids: tempOrder, completion: {})
|
||||
}
|
||||
self.reorderItem = { [weak self] fromIndex, toIndex, transactionOpaqueState -> Signal<Bool, NoError> in
|
||||
guard let self else {
|
||||
return .single(false)
|
||||
}
|
||||
guard fromIndex >= 0 && fromIndex < self.currentEntries.count && toIndex >= 0 && toIndex < self.currentEntries.count else {
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
let fromEntry = self.currentEntries[fromIndex]
|
||||
let toEntry = self.currentEntries[toIndex]
|
||||
|
||||
var referenceId: Int32?
|
||||
var beforeAll = false
|
||||
switch toEntry {
|
||||
case let .item(item, _, _, _, _):
|
||||
referenceId = item.id
|
||||
case .add:
|
||||
beforeAll = true
|
||||
}
|
||||
|
||||
if case let .item(item, _, _, _, _) = fromEntry {
|
||||
var itemIds = self.currentEntries.compactMap { entry -> Int32? in
|
||||
switch entry {
|
||||
case .add:
|
||||
return nil
|
||||
case let .item(item, _, _, _, _):
|
||||
return item.id
|
||||
}
|
||||
}
|
||||
let itemId: Int32? = item.id
|
||||
|
||||
if let itemId {
|
||||
itemIds = itemIds.filter({ $0 != itemId })
|
||||
if let referenceId {
|
||||
var inserted = false
|
||||
for i in 0 ..< itemIds.count {
|
||||
if itemIds[i] == referenceId {
|
||||
if fromIndex < toIndex {
|
||||
itemIds.insert(itemId, at: i + 1)
|
||||
} else {
|
||||
itemIds.insert(itemId, at: i)
|
||||
}
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inserted {
|
||||
itemIds.append(itemId)
|
||||
}
|
||||
} else if beforeAll {
|
||||
itemIds.insert(itemId, at: 0)
|
||||
} else {
|
||||
itemIds.append(itemId)
|
||||
}
|
||||
if self.tempOrder != itemIds {
|
||||
self.tempOrder = itemIds
|
||||
self.setEntries(entries: self.originalEntries, animated: true)
|
||||
}
|
||||
|
||||
return .single(true)
|
||||
} else {
|
||||
return .single(false)
|
||||
}
|
||||
} else {
|
||||
return .single(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
|
||||
@@ -275,14 +370,74 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
deleteIndices: [],
|
||||
insertIndicesAndItems: [],
|
||||
updateIndicesAndItems: [],
|
||||
options: [.Synchronous, .LowLatency],
|
||||
options: [.Synchronous, .LowLatency, .PreferSynchronousResourceLoading],
|
||||
additionalScrollDistance: 0.0,
|
||||
updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: listViewDuration, curve: listViewCurve),
|
||||
updateOpaqueState: nil
|
||||
)
|
||||
}
|
||||
|
||||
func setPendingRemoveItems(itemIds: [Int32]) {
|
||||
self.pendingRemoveItems = itemIds
|
||||
self.setEntries(entries: self.originalEntries, animated: true)
|
||||
}
|
||||
|
||||
func setEntries(entries: [ContentEntry], animated: Bool) {
|
||||
if self.resetTempOrderOnNextUpdate {
|
||||
self.resetTempOrderOnNextUpdate = false
|
||||
self.tempOrder = nil
|
||||
}
|
||||
let pendingRemoveItems = self.pendingRemoveItems
|
||||
self.pendingRemoveItems = nil
|
||||
|
||||
self.originalEntries = entries
|
||||
|
||||
var entries = entries
|
||||
if let pendingRemoveItems {
|
||||
entries = entries.filter { entry in
|
||||
switch entry.stableId {
|
||||
case .add:
|
||||
return true
|
||||
case let .item(id):
|
||||
return !pendingRemoveItems.contains(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let tempOrder = self.tempOrder {
|
||||
let originalList = entries
|
||||
entries.removeAll()
|
||||
|
||||
if let entry = originalList.first(where: { entry in
|
||||
if case .add = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
entries.append(entry)
|
||||
}
|
||||
|
||||
for id in tempOrder {
|
||||
if let entry = originalList.first(where: { entry in
|
||||
if case let .item(listId) = entry.stableId, listId == id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
entries.append(entry)
|
||||
}
|
||||
}
|
||||
for entry in originalList {
|
||||
if !entries.contains(where: { listEntry in
|
||||
listEntry.stableId == entry.stableId
|
||||
}) {
|
||||
entries.append(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries)
|
||||
self.currentEntries = entries
|
||||
|
||||
@@ -316,15 +471,19 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
private let navigationBarView = ComponentView<Empty>()
|
||||
private var navigationHeight: CGFloat?
|
||||
|
||||
private var selectionPanel: ComponentView<Empty>?
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: QuickReplySetupScreenComponent?
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var items: [QuickReplyMessageShortcut] = []
|
||||
private var itemsDisposable: Disposable?
|
||||
private var messagesDisposable: Disposable?
|
||||
private var shortcutMessageList: ShortcutMessageList?
|
||||
private var shortcutMessageListDisposable: Disposable?
|
||||
private var keepUpdatedDisposable: Disposable?
|
||||
|
||||
private var selectedIds = Set<Int32>()
|
||||
|
||||
private var isEditing: Bool = false
|
||||
private var isSearchDisplayControllerActive: Bool = false
|
||||
@@ -340,44 +499,27 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.itemsDisposable?.dispose()
|
||||
self.messagesDisposable?.dispose()
|
||||
self.shortcutMessageListDisposable?.dispose()
|
||||
self.keepUpdatedDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
}
|
||||
|
||||
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
|
||||
guard let component = self.component else {
|
||||
return true
|
||||
}
|
||||
component.context.engine.accountData.updateShortcutMessages(state: QuickReplyMessageShortcutsState(shortcuts: self.items))
|
||||
return true
|
||||
}
|
||||
|
||||
func openQuickReplyChat(shortcut: String?) {
|
||||
func openQuickReplyChat(shortcut: String?, shortcutId: Int32?) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if let shortcut {
|
||||
var mappedMessages: [EngineMessage] = []
|
||||
if let messages = self.items.first(where: { $0.shortcut == shortcut })?.messages {
|
||||
var nextId: Int32 = 1
|
||||
for message in messages {
|
||||
var mappedMessage = message._asMessage()
|
||||
mappedMessage = mappedMessage.withUpdatedId(id: MessageId(peerId: component.context.account.peerId, namespace: 0, id: nextId))
|
||||
mappedMessage = mappedMessage.withUpdatedStableId(stableId: UInt32(nextId))
|
||||
mappedMessage = mappedMessage.withUpdatedTimestamp(nextId)
|
||||
mappedMessages.append(EngineMessage(mappedMessage))
|
||||
|
||||
nextId += 1
|
||||
}
|
||||
}
|
||||
let contents = AutomaticBusinessMessageSetupChatContents(
|
||||
context: component.context,
|
||||
messages: mappedMessages,
|
||||
kind: .quickReplyMessageInput(shortcut: shortcut)
|
||||
kind: .quickReplyMessageInput(shortcut: shortcut),
|
||||
shortcutId: shortcutId
|
||||
)
|
||||
let chatController = component.context.sharedContext.makeChatController(
|
||||
context: component.context,
|
||||
@@ -388,30 +530,6 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
)
|
||||
chatController.navigationPresentation = .modal
|
||||
self.environment?.controller()?.push(chatController)
|
||||
self.messagesDisposable?.dispose()
|
||||
self.messagesDisposable = (contents.messages
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] messages in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let messages = messages.reversed().map(EngineMessage.init)
|
||||
|
||||
if messages.isEmpty {
|
||||
if let index = self.items.firstIndex(where: { $0.shortcut == shortcut }) {
|
||||
self.items.remove(at: index)
|
||||
}
|
||||
} else {
|
||||
if let index = self.items.firstIndex(where: { $0.shortcut == shortcut }) {
|
||||
self.items[index] = QuickReplyMessageShortcut(id: self.items[index].id, shortcut: self.items[index].shortcut, messages: messages)
|
||||
} else {
|
||||
self.items.insert(QuickReplyMessageShortcut(id: Int32.random(in: Int32.min ... Int32.max), shortcut: shortcut, messages: messages), at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
component.context.engine.accountData.updateShortcutMessages(state: QuickReplyMessageShortcutsState(shortcuts: self.items))
|
||||
|
||||
self.state?.updated(transition: .immediate)
|
||||
})
|
||||
} else {
|
||||
var completion: ((String?) -> Void)?
|
||||
let alertController = quickReplyNameAlertController(
|
||||
@@ -430,7 +548,12 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
if let value, !value.isEmpty {
|
||||
if self.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) {
|
||||
guard let shortcutMessageList = self.shortcutMessageList else {
|
||||
alertController?.dismissAnimated()
|
||||
return
|
||||
}
|
||||
|
||||
if shortcutMessageList.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) {
|
||||
if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode {
|
||||
contentNode.setErrorText(errorText: "Shortcut with that name already exists")
|
||||
}
|
||||
@@ -438,7 +561,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
alertController?.dismissAnimated()
|
||||
self.openQuickReplyChat(shortcut: value)
|
||||
self.openQuickReplyChat(shortcut: value, shortcutId: nil)
|
||||
}
|
||||
}
|
||||
self.environment?.controller()?.present(alertController, in: .window(.root))
|
||||
@@ -447,13 +570,11 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
self.contentListNode?.clearHighlightAnimated(true)
|
||||
}
|
||||
|
||||
func openEditShortcut(shortcut: String) {
|
||||
func openEditShortcut(id: Int32, currentValue: String) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentValue = shortcut
|
||||
|
||||
var completion: ((String?) -> Void)?
|
||||
let alertController = quickReplyNameAlertController(
|
||||
context: component.context,
|
||||
@@ -475,26 +596,17 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
alertController?.dismissAnimated()
|
||||
return
|
||||
}
|
||||
|
||||
var shortcuts = self.items
|
||||
guard let index = shortcuts.firstIndex(where: { $0.shortcut.lowercased() == currentValue }) else {
|
||||
guard let shortcutMessageList = self.shortcutMessageList else {
|
||||
alertController?.dismissAnimated()
|
||||
return
|
||||
}
|
||||
|
||||
if shortcuts.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) {
|
||||
if shortcutMessageList.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) {
|
||||
if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode {
|
||||
contentNode.setErrorText(errorText: "Shortcut with that name already exists")
|
||||
}
|
||||
} else {
|
||||
shortcuts[index] = QuickReplyMessageShortcut(
|
||||
id: shortcuts[index].id,
|
||||
shortcut: value,
|
||||
messages: shortcuts[index].messages
|
||||
)
|
||||
self.items = shortcuts
|
||||
let updatedShortcutMessages = QuickReplyMessageShortcutsState(shortcuts: shortcuts)
|
||||
component.context.engine.accountData.updateShortcutMessages(state: updatedShortcutMessages)
|
||||
component.context.engine.accountData.editMessageShortcut(id: id, shortcut: value)
|
||||
|
||||
alertController?.dismissAnimated()
|
||||
}
|
||||
@@ -503,23 +615,48 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
self.environment?.controller()?.present(alertController, in: .window(.root))
|
||||
}
|
||||
|
||||
func openDeleteShortcut(shortcut: String) {
|
||||
func toggleShortcutSelection(id: Int32) {
|
||||
if self.selectedIds.contains(id) {
|
||||
self.selectedIds.remove(id)
|
||||
} else {
|
||||
self.selectedIds.insert(id)
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
|
||||
func openDeleteShortcuts(ids: [Int32]) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
var shortcuts = self.items
|
||||
guard let index = shortcuts.firstIndex(where: { $0.shortcut.lowercased() == shortcut }) else {
|
||||
return
|
||||
}
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
shortcuts.remove(at: index)
|
||||
self.items = shortcuts
|
||||
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
|
||||
let updatedShortcutMessages = QuickReplyMessageShortcutsState(shortcuts: shortcuts)
|
||||
component.context.engine.accountData.updateShortcutMessages(state: updatedShortcutMessages)
|
||||
//TODO:localize
|
||||
items.append(ActionSheetButtonItem(title: ids.count == 1 ? "Delete Shortcut" : "Delete Shortcuts", color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
for id in ids {
|
||||
self.selectedIds.remove(id)
|
||||
}
|
||||
self.contentListNode?.setPendingRemoveItems(itemIds: ids)
|
||||
component.context.engine.accountData.deleteMessageShortcuts(ids: ids)
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
self.environment?.controller()?.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
|
||||
private func updateNavigationBar(
|
||||
@@ -533,31 +670,43 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
deferScrollApplication: Bool
|
||||
) -> CGFloat {
|
||||
var rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>] = []
|
||||
if self.isEditing {
|
||||
rightButtons.append(AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .text(title: strings.Common_Done, isBold: true),
|
||||
pressed: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty {
|
||||
if self.isEditing {
|
||||
rightButtons.append(AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .text(title: strings.Common_Done, isBold: true),
|
||||
pressed: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isEditing = false
|
||||
self.selectedIds.removeAll()
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
self.isEditing = false
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
))))
|
||||
} else {
|
||||
rightButtons.append(AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .text(title: strings.Common_Edit, isBold: false),
|
||||
pressed: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
))))
|
||||
} else {
|
||||
rightButtons.append(AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .text(title: strings.Common_Edit, isBold: false),
|
||||
pressed: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isEditing = true
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
self.isEditing = true
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
))))
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
let titleText: String
|
||||
if !self.selectedIds.isEmpty {
|
||||
//TODO:localize
|
||||
titleText = "\(self.selectedIds.count) Selected"
|
||||
} else {
|
||||
titleText = "Quick Replies"
|
||||
}
|
||||
|
||||
let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content(
|
||||
title: "Quick Replies",
|
||||
title: titleText,
|
||||
navigationBackTitle: nil,
|
||||
titleComponent: nil,
|
||||
chatListTitle: nil,
|
||||
@@ -629,9 +778,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
|
||||
private func updateNavigationScrolling(navigationHeight: CGFloat, transition: Transition) {
|
||||
var mainOffset: CGFloat
|
||||
if self.items.isEmpty {
|
||||
mainOffset = navigationHeight
|
||||
} else {
|
||||
if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty {
|
||||
if let contentListNode = self.contentListNode {
|
||||
switch contentListNode.visibleContentOffset() {
|
||||
case .none:
|
||||
@@ -644,6 +791,8 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
} else {
|
||||
mainOffset = navigationHeight
|
||||
}
|
||||
} else {
|
||||
mainOffset = navigationHeight
|
||||
}
|
||||
|
||||
mainOffset = min(mainOffset, ChatListNavigationBar.searchScrollHeight)
|
||||
@@ -674,18 +823,20 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
|
||||
if self.component == nil {
|
||||
self.accountPeer = component.initialData.accountPeer
|
||||
self.items = component.initialData.shortcutMessages.shortcuts
|
||||
self.shortcutMessageList = component.initialData.shortcutMessageList
|
||||
|
||||
self.itemsDisposable = (component.context.engine.accountData.shortcutMessages()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessages in
|
||||
self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList()
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] shortcutMessageList in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.items = shortcutMessages.shortcuts
|
||||
self.shortcutMessageList = shortcutMessageList
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
|
||||
self.keepUpdatedDisposable = component.context.engine.accountData.keepShortcutMessageListUpdated().startStrict()
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
@@ -702,7 +853,12 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
self.backgroundColor = environment.theme.list.plainBackgroundColor
|
||||
}
|
||||
|
||||
if self.items.isEmpty {
|
||||
if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty {
|
||||
if let emptyState = self.emptyState {
|
||||
self.emptyState = nil
|
||||
emptyState.view?.removeFromSuperview()
|
||||
}
|
||||
} else {
|
||||
let emptyState: ComponentView<Empty>
|
||||
var emptyStateTransition = transition
|
||||
if let current = self.emptyState {
|
||||
@@ -713,18 +869,18 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
emptyStateTransition = emptyStateTransition.withAnimation(.none)
|
||||
}
|
||||
|
||||
let emptyStateFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight), size: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight))
|
||||
let emptyStateFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))
|
||||
let _ = emptyState.update(
|
||||
transition: emptyStateTransition,
|
||||
component: AnyComponent(QuickReplyEmptyStateComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right),
|
||||
insets: UIEdgeInsets(top: environment.navigationHeight, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openQuickReplyChat(shortcut: nil)
|
||||
self.openQuickReplyChat(shortcut: nil, shortcutId: nil)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@@ -736,13 +892,9 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
}
|
||||
emptyStateTransition.setFrame(view: emptyStateView, frame: emptyStateFrame)
|
||||
}
|
||||
} else {
|
||||
if let emptyState = self.emptyState {
|
||||
self.emptyState = nil
|
||||
emptyState.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
var listBottomInset = environment.safeInsets.bottom
|
||||
let navigationHeight = self.updateNavigationBar(
|
||||
component: component,
|
||||
theme: environment.theme,
|
||||
@@ -755,6 +907,78 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
)
|
||||
self.navigationHeight = navigationHeight
|
||||
|
||||
if !self.selectedIds.isEmpty {
|
||||
let selectionPanel: ComponentView<Empty>
|
||||
var selectionPanelTransition = transition
|
||||
if let current = self.selectionPanel {
|
||||
selectionPanel = current
|
||||
} else {
|
||||
selectionPanelTransition = selectionPanelTransition.withAnimation(.none)
|
||||
selectionPanel = ComponentView()
|
||||
self.selectionPanel = selectionPanel
|
||||
}
|
||||
|
||||
let buttonTitle: String
|
||||
if self.selectedIds.count == 1 {
|
||||
buttonTitle = "Delete 1 Quick Reply"
|
||||
} else {
|
||||
buttonTitle = "Delete \(self.selectedIds.count) Quick Replies"
|
||||
}
|
||||
|
||||
let selectionPanelSize = selectionPanel.update(
|
||||
transition: selectionPanelTransition,
|
||||
component: AnyComponent(BottomPanelComponent(
|
||||
theme: environment.theme,
|
||||
content: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: buttonTitle, font: Font.regular(17.0), textColor: environment.theme.list.itemDestructiveColor))
|
||||
)),
|
||||
background: nil,
|
||||
effectAlignment: .center,
|
||||
minSize: CGSize(width: availableSize.width - environment.safeInsets.left - environment.safeInsets.right, height: 44.0),
|
||||
contentInsets: UIEdgeInsets(),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.selectedIds.isEmpty {
|
||||
return
|
||||
}
|
||||
self.openDeleteShortcuts(ids: Array(self.selectedIds))
|
||||
},
|
||||
animateAlpha: true,
|
||||
animateScale: false,
|
||||
animateContents: false
|
||||
))),
|
||||
insets: UIEdgeInsets(top: 4.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let selectionPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize)
|
||||
listBottomInset = selectionPanelSize.height
|
||||
if let selectionPanelView = selectionPanel.view {
|
||||
var animateIn = false
|
||||
if selectionPanelView.superview == nil {
|
||||
animateIn = true
|
||||
self.addSubview(selectionPanelView)
|
||||
}
|
||||
selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame)
|
||||
if animateIn {
|
||||
transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelFrame.height), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let selectionPanel = self.selectionPanel {
|
||||
self.selectionPanel = nil
|
||||
if let selectionPanelView = selectionPanel.view {
|
||||
transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in
|
||||
selectionPanelView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let contentListNode: ContentListNode
|
||||
if let current = self.contentListNode {
|
||||
contentListNode = current
|
||||
@@ -773,7 +997,9 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: .immediate)
|
||||
}
|
||||
|
||||
if let navigationBarComponentView = self.navigationBarView.view {
|
||||
if let selectionPanelView = self.selectionPanel?.view {
|
||||
self.insertSubview(contentListNode.view, belowSubview: selectionPanelView)
|
||||
} else if let navigationBarComponentView = self.navigationBarView.view {
|
||||
self.insertSubview(contentListNode.view, belowSubview: navigationBarComponentView)
|
||||
} else {
|
||||
self.addSubview(contentListNode.view)
|
||||
@@ -781,18 +1007,22 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
transition.setFrame(view: contentListNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), transition: transition)
|
||||
contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: listBottomInset, right: environment.safeInsets.right), transition: transition)
|
||||
|
||||
var entries: [ContentEntry] = []
|
||||
if let accountPeer = self.accountPeer {
|
||||
if let shortcutMessageList = self.shortcutMessageList, let accountPeer = self.accountPeer {
|
||||
entries.append(.add)
|
||||
for item in self.items {
|
||||
entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing))
|
||||
for item in shortcutMessageList.items {
|
||||
entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: self.selectedIds.contains(item.id)))
|
||||
}
|
||||
}
|
||||
contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate)
|
||||
|
||||
contentListNode.isHidden = self.items.isEmpty
|
||||
if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty {
|
||||
contentListNode.isHidden = false
|
||||
} else {
|
||||
contentListNode.isHidden = true
|
||||
}
|
||||
|
||||
self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: transition)
|
||||
|
||||
@@ -817,14 +1047,14 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
public final class QuickReplySetupScreen: ViewControllerComponentContainer {
|
||||
public final class InitialData: QuickReplySetupScreenInitialData {
|
||||
let accountPeer: EnginePeer?
|
||||
let shortcutMessages: QuickReplyMessageShortcutsState
|
||||
let shortcutMessageList: ShortcutMessageList
|
||||
|
||||
init(
|
||||
accountPeer: EnginePeer?,
|
||||
shortcutMessages: QuickReplyMessageShortcutsState
|
||||
shortcutMessageList: ShortcutMessageList
|
||||
) {
|
||||
self.accountPeer = accountPeer
|
||||
self.shortcutMessages = shortcutMessages
|
||||
self.shortcutMessageList = shortcutMessageList
|
||||
}
|
||||
}
|
||||
|
||||
@@ -874,13 +1104,13 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer {
|
||||
context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
|
||||
),
|
||||
context.engine.accountData.shortcutMessages()
|
||||
context.engine.accountData.shortcutMessageList()
|
||||
|> take(1)
|
||||
)
|
||||
|> map { accountPeer, shortcutMessages -> QuickReplySetupScreenInitialData in
|
||||
|> map { accountPeer, shortcutMessageList -> QuickReplySetupScreenInitialData in
|
||||
return InitialData(
|
||||
accountPeer: accountPeer,
|
||||
shortcutMessages: shortcutMessages
|
||||
shortcutMessageList: shortcutMessageList
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user