Business fixes

This commit is contained in:
Isaac 2024-03-05 15:32:59 +04:00
parent ae998eb91e
commit 7966993955
26 changed files with 503 additions and 105 deletions

View File

@ -11424,6 +11424,8 @@ to respond to messages faster.";
"PeerInfo.BusinessHours.StatusOpensInHours_1" = "Opens in one hour";
"PeerInfo.BusinessHours.StatusOpensInHours_any" = "Opens in %d hours";
"PeerInfo.BusinessHours.StatusOpensOnDate" = "Opens %@";
"PeerInfo.BusinessHours.StatusOpensTodayAt" = "Opens today at %@";
"PeerInfo.BusinessHours.StatusOpensTomorrowAt" = "Opens tomorrow at %@";
"PeerInfo.BusinessHours.TimezoneSwitchMy" = "my time";
"PeerInfo.BusinessHours.TimezoneSwitchBusiness" = "local time";
"PeerInfo.BusinessHours.Label" = "business hours";
@ -11567,6 +11569,9 @@ to respond to messages faster.";
"BusinessLocationSetup.ErrorAddressEmpty.Text" = "Address can't be empty.";
"BusinessLocationSetup.ErrorAddressEmpty.ResetAction" = "Delete";
"BusinessLocationSetup.AlertUnsavedChanges.Text" = "You have unsaved changes.";
"BusinessLocationSetup.AlertUnsavedChanges.ResetAction" = "Revert";
"ChatbotSetup.Title" = "Chatbots";
"ChatbotSetup.Text" = "Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More >]()";
"ChatbotSetup.TextLink" = "https://telegram.org";
@ -11588,3 +11593,6 @@ to respond to messages faster.";
"Chat.QuickReply.ServiceHeader1" = "To edit or delete your quick reply, tap an hold on it.";
"Chat.QuickReply.ServiceHeader2" = "To use this quick reply in a chat, type / and select the shortcut from the list.";
"Chat.QuickReplyMediaMessageLimitReachedText_1" = "There can be at most %d message in this chat.";
"Chat.QuickReplyMediaMessageLimitReachedText_any" = "There can be at most %d messages in this chat.";

View File

@ -340,6 +340,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let filtersWithCounts = Promise<[(ChatListFilter, Int)]>()
filtersWithCounts.set(filtersWithCountsSignal)
let animateNextShowHideTagsTransition = Atomic<Bool?>(value: nil)
let arguments = ChatListFilterPresetListControllerArguments(context: context,
addSuggestedPressed: { title, data in
let _ = combineLatest(
@ -580,6 +582,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
let previousDisplayTags = Atomic<Bool?>(value: nil)
let limits = context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
@ -668,6 +672,11 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
rightNavigationButton = nil
}
let previousDisplayTagsValue = previousDisplayTags.swap(displayTags)
if let previousDisplayTagsValue, previousDisplayTagsValue != displayTags {
let _ = animateNextShowHideTagsTransition.swap(displayTags)
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true)
@ -787,6 +796,29 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
})
})
controller.afterTransactionCompleted = { [weak controller] in
guard let toggleDirection = animateNextShowHideTagsTransition.swap(nil) else {
return
}
guard let controller else {
return
}
var presetItemNodes: [ChatListFilterPresetListItemNode] = []
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatListFilterPresetListItemNode {
presetItemNodes.append(itemNode)
}
}
var delay: Double = 0.0
for itemNode in presetItemNodes.reversed() {
if toggleDirection {
itemNode.animateTagColorIn(delay: delay)
}
delay += 0.02
}
}
return controller
}

View File

@ -112,7 +112,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
private let titleFont = Font.regular(17.0)
private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@ -415,7 +415,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
if item.tagColor != nil {
sharedIconFrame.origin.x -= 34.0
}
strongSelf.sharedIconNode.frame = sharedIconFrame
if strongSelf.sharedIconNode.bounds.isEmpty {
strongSelf.sharedIconNode.frame = sharedIconFrame
} else {
transition.updateFrame(node: strongSelf.sharedIconNode, frame: sharedIconFrame)
}
}
var isShared = false
if case let .filter(_, _, _, data) = item.preset, data.isShared {
@ -425,9 +429,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
if let tagColor = item.tagColor {
let tagIconView: UIImageView
var tagIconTransition = transition
if let current = strongSelf.tagIconView {
tagIconView = current
} else {
tagIconTransition = .immediate
tagIconView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 24.0, color: .white)?.withRenderingMode(.alwaysTemplate))
strongSelf.tagIconView = tagIconView
strongSelf.containerNode.view.addSubview(tagIconView)
@ -435,13 +441,16 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
tagIconView.tintColor = tagColor
let tagIconFrame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX - 2.0 - 24.0, y: floorToScreenPixels((layout.contentSize.height - 24.0) / 2.0)), size: CGSize(width: 24.0, height: 24.0))
tagIconView.frame = tagIconFrame
transition.updateAlpha(layer: tagIconView.layer, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
tagIconTransition.updateAlpha(layer: tagIconView.layer, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
tagIconTransition.updateFrame(view: tagIconView, frame: tagIconFrame)
} else {
if let tagIconView = strongSelf.tagIconView {
strongSelf.tagIconView = nil
tagIconView.removeFromSuperview()
transition.updateAlpha(layer: tagIconView.layer, alpha: 0.0, completion: { [weak tagIconView] _ in
tagIconView?.removeFromSuperview()
})
transition.updateTransformScale(layer: tagIconView.layer, scale: 0.001)
}
}
@ -458,6 +467,13 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}
}
func animateTagColorIn(delay: Double) {
if let tagIconView = self.tagIconView {
tagIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12, delay: delay)
tagIconView.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: delay)
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)

View File

@ -251,6 +251,13 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
public var willDisappear: ((Bool) -> Void)?
public var didDisappear: ((Bool) -> Void)?
public var afterTransactionCompleted: (() -> Void)? {
didSet {
if self.isNodeLoaded {
(self.displayNode as! ItemListControllerNode).afterTransactionCompleted = self.afterTransactionCompleted
}
}
}
public init<ItemGenerationArguments>(presentationData: ItemListPresentationData, updatedPresentationData: Signal<ItemListPresentationData, NoError>, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal<ItemListControllerTabBarItem, NoError>?) {
self.state = state
@ -486,6 +493,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
displayNode.searchActivated = self.searchActivated
displayNode.reorderEntry = self.reorderEntry
displayNode.reorderCompleted = self.reorderCompleted
displayNode.afterTransactionCompleted = self.afterTransactionCompleted
displayNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem
displayNode.listNode.didScrollWithOffset = self.didScrollWithOffset
displayNode.requestLayout = { [weak self] transition in

View File

@ -285,6 +285,7 @@ open class ItemListControllerNode: ASDisplayNode {
public var searchActivated: ((Bool) -> Void)?
public var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Signal<Bool, NoError>)?
public var reorderCompleted: (([ItemListNodeAnyEntry]) -> Void)?
public var afterTransactionCompleted: (() -> Void)?
public var requestLayout: ((ContainedViewLayoutTransition) -> Void)?
public var enableInteractiveDismiss = false {
@ -920,6 +921,8 @@ open class ItemListControllerNode: ASDisplayNode {
}
}
}
strongSelf.afterTransactionCompleted?()
}
})
var updateEmptyStateItem = false

View File

@ -3617,6 +3617,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
}
navigationController.pushViewController(peerSelectionController)
}
if case .business = mode {
context.account.viewTracker.keepQuickRepliesApproximatelyUpdated()
}
}
required public init(coder aDecoder: NSCoder) {

View File

@ -2016,6 +2016,35 @@ public final class AccountViewTracker {
})
}
public func pendingQuickReplyMessagesViewForLocation(shortcut: String) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> {
guard let account = self.account else {
return .never()
}
let chatLocation: ChatLocationInput = .peer(peerId: account.peerId, threadId: nil)
let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just([Namespaces.Message.QuickReplyLocal]), orderStatistics: [], additionalData: [])
|> map { view, update, initialData in
var entries: [MessageHistoryEntry] = []
for entry in view.entries {
var matches = false
inner: for attribute in entry.message.attributes {
if let attribute = attribute as? OutgoingQuickReplyMessageAttribute {
if attribute.shortcut == shortcut {
matches = true
}
break inner
}
}
if matches {
entries.append(entry)
}
}
let mappedView = MessageHistoryView(tag: nil, namespaces: .just([Namespaces.Message.QuickReplyLocal]), entries: entries, holeEarlier: false, holeLater: false, isLoading: false)
return (mappedView, update, initialData)
}
return signal
}
public func aroundMessageOfInterestHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange<Int32>? = nil, count: Int, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> {
if let account = self.account {
let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>

View File

@ -162,8 +162,8 @@ public extension TelegramEngine {
|> then(remoteApply)
}
public func shortcutMessageList() -> Signal<ShortcutMessageList, NoError> {
return _internal_shortcutMessageList(account: self.account)
public func shortcutMessageList(onlyRemote: Bool) -> Signal<ShortcutMessageList, NoError> {
return _internal_shortcutMessageList(account: self.account, onlyRemote: onlyRemote)
}
public func keepShortcutMessageListUpdated() -> Signal<Never, NoError> {

View File

@ -48,12 +48,12 @@ struct QuickReplyMessageShortcutsState: Codable, Equatable {
public final class ShortcutMessageList: Equatable {
public final class Item: Equatable {
public let id: Int32
public let id: Int32?
public let shortcut: String
public let topMessage: EngineMessage
public let totalCount: Int
public init(id: Int32, shortcut: String, topMessage: EngineMessage, totalCount: Int) {
public init(id: Int32?, shortcut: String, topMessage: EngineMessage, totalCount: Int) {
self.id = id
self.shortcut = shortcut
self.topMessage = topMessage
@ -117,12 +117,15 @@ func _internal_quickReplyMessageShortcutsState(account: Account) -> Signal<Quick
}
func _internal_keepShortcutMessagesUpdated(account: Account) -> Signal<Never, NoError> {
let updateSignal = _internal_shortcutMessageList(account: account)
let updateSignal = _internal_shortcutMessageList(account: account, onlyRemote: true)
|> take(1)
|> mapToSignal { list -> Signal<Never, NoError> in
var acc: UInt64 = 0
for item in list.items {
combineInt64Hash(&acc, with: UInt64(item.id))
guard let itemId = item.id else {
continue
}
combineInt64Hash(&acc, with: UInt64(itemId))
combineInt64Hash(&acc, with: md5StringHash(item.shortcut))
combineInt64Hash(&acc, with: UInt64(item.topMessage.id.id))
@ -204,10 +207,42 @@ func _internal_keepShortcutMessagesUpdated(account: Account) -> Signal<Never, No
return updateSignal
}
func _internal_shortcutMessageList(account: Account) -> Signal<ShortcutMessageList, NoError> {
return _internal_quickReplyMessageShortcutsState(account: account)
|> distinctUntilChanged
|> mapToSignal { state -> Signal<ShortcutMessageList, NoError> in
func _internal_shortcutMessageList(account: Account, onlyRemote: Bool) -> Signal<ShortcutMessageList, NoError> {
let pendingShortcuts: Signal<[String: EngineMessage], NoError>
if onlyRemote {
pendingShortcuts = .single([:])
} else {
pendingShortcuts = account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 100, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Set([Namespaces.Message.QuickReplyLocal])), orderStatistics: [])
|> map { view , _, _ -> [String: EngineMessage] in
var topMessages: [String: EngineMessage] = [:]
for entry in view.entries {
var shortcut: String?
inner: for attribute in entry.message.attributes {
if let attribute = attribute as? OutgoingQuickReplyMessageAttribute {
shortcut = attribute.shortcut
break inner
}
}
if let shortcut {
if let currentTopMessage = topMessages[shortcut] {
if entry.message.index < currentTopMessage.index {
topMessages[shortcut] = EngineMessage(entry.message)
}
} else {
topMessages[shortcut] = EngineMessage(entry.message)
}
}
}
return topMessages
}
|> distinctUntilChanged
}
return combineLatest(queue: .mainQueue(),
_internal_quickReplyMessageShortcutsState(account: account) |> distinctUntilChanged,
pendingShortcuts
)
|> mapToSignal { state, pendingShortcuts -> Signal<ShortcutMessageList, NoError> in
guard let state else {
return .single(ShortcutMessageList(items: [], isLoading: true))
}
@ -238,6 +273,7 @@ func _internal_shortcutMessageList(account: Account) -> Signal<ShortcutMessageLi
)
|> map { views -> ShortcutMessageList in
var items: [ShortcutMessageList.Item] = []
for shortcut in state.shortcuts {
guard let historyViewKey = historyViewKeys[shortcut.id], let historyView = views.views[historyViewKey] as? MessageHistoryView else {
continue
@ -254,6 +290,18 @@ func _internal_shortcutMessageList(account: Account) -> Signal<ShortcutMessageLi
items.append(ShortcutMessageList.Item(id: shortcut.id, shortcut: shortcut.shortcut, topMessage: EngineMessage(entry.message), totalCount: totalCount))
}
}
for (shortcut, message) in pendingShortcuts.sorted(by: { $0.key < $1.key }) {
if !items.contains(where: { $0.shortcut == shortcut }) {
items.append(ShortcutMessageList.Item(
id: nil,
shortcut: shortcut,
topMessage: message,
totalCount: 1
))
}
}
return ShortcutMessageList(items: items, isLoading: false)
}
|> distinctUntilChanged

View File

@ -978,6 +978,22 @@ func _internal_updateChatListFiltersDisplayTagsInteractively(postbox: Postbox, d
var state = entry?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default
if displayTags != state.displayTags {
state.displayTags = displayTags
if state.displayTags {
for i in 0 ..< state.filters.count {
switch state.filters[i] {
case .allChats:
break
case let .filter(id, title, emoticon, data):
if data.color == nil {
var data = data
data.color = PeerNameColor(rawValue: Int32.random(in: 0 ... 7))
state.filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: data)
}
}
}
}
hasUpdates = true
}

View File

@ -81,6 +81,7 @@ public final class ChatInlineSearchResultsListComponent: Component {
public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal<MessageHistoryView, NoError>?
public let getSearchResult: () -> Signal<SearchMessagesResult?, NoError>?
public let getSavedPeers: (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?
public let loadMoreSearchResults: () -> Void
public init(
context: AccountContext,
@ -92,7 +93,8 @@ public final class ChatInlineSearchResultsListComponent: Component {
peerSelected: @escaping (EnginePeer) -> Void,
loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal<MessageHistoryView, NoError>?,
getSearchResult: @escaping () -> Signal<SearchMessagesResult?, NoError>?,
getSavedPeers: @escaping (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?
getSavedPeers: @escaping (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?,
loadMoreSearchResults: @escaping () -> Void
) {
self.context = context
self.presentation = presentation
@ -104,6 +106,7 @@ public final class ChatInlineSearchResultsListComponent: Component {
self.loadTagMessages = loadTagMessages
self.getSearchResult = getSearchResult
self.getSavedPeers = getSavedPeers
self.loadMoreSearchResults = loadMoreSearchResults
}
public static func ==(lhs: ChatInlineSearchResultsListComponent, rhs: ChatInlineSearchResultsListComponent) -> Bool {
@ -377,6 +380,19 @@ public final class ChatInlineSearchResultsListComponent: Component {
break
}
}
} else if let (currentIndex, disposable) = self.searchContents {
if let loadAroundIndex, loadAroundIndex != currentIndex {
switch component.contents {
case .empty:
break
case .tag:
break
case .search:
self.searchContents = (loadAroundIndex, disposable)
component.loadMoreSearchResults()
}
}
}
}
@ -511,7 +527,7 @@ public final class ChatInlineSearchResultsListComponent: Component {
contentId: .search(query),
entries: entries,
messages: messages,
hasEarlier: false,
hasEarlier: !(result?.completed ?? true),
hasLater: false
)
if !self.isUpdating {

View File

@ -45,6 +45,7 @@ public final class ListActionItemComponent: Component {
public enum Accessory: Equatable {
case arrow
case toggle(Toggle)
case activity
}
public enum IconInsets: Equatable {
@ -123,6 +124,7 @@ public final class ListActionItemComponent: Component {
private var arrowView: UIImageView?
private var switchNode: SwitchNode?
private var iconSwitchNode: IconSwitchNode?
private var activityIndicatorView: UIActivityIndicatorView?
private var component: ListActionItemComponent?
@ -191,6 +193,8 @@ public final class ListActionItemComponent: Component {
contentRightInset = 30.0
case .toggle:
contentRightInset = 76.0
case .activity:
contentRightInset = 76.0
}
var contentHeight: CGFloat = 0.0
@ -428,6 +432,40 @@ public final class ListActionItemComponent: Component {
}
}
if case .activity = component.accessory {
let activityIndicatorView: UIActivityIndicatorView
var activityIndicatorTransition = transition
if let current = self.activityIndicatorView {
activityIndicatorView = current
} else {
activityIndicatorTransition = activityIndicatorTransition.withAnimation(.none)
if #available(iOS 13.0, *) {
activityIndicatorView = UIActivityIndicatorView(style: .medium)
} else {
activityIndicatorView = UIActivityIndicatorView(style: .gray)
}
self.activityIndicatorView = activityIndicatorView
self.addSubview(activityIndicatorView)
activityIndicatorView.sizeToFit()
}
let activityIndicatorSize = activityIndicatorView.bounds.size
let activityIndicatorFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - activityIndicatorSize.width, y: floor((min(60.0, contentHeight) - activityIndicatorSize.height) * 0.5)), size: activityIndicatorSize)
activityIndicatorView.tintColor = component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.5)
activityIndicatorTransition.setFrame(view: activityIndicatorView, frame: activityIndicatorFrame)
if !activityIndicatorView.isAnimating {
activityIndicatorView.startAnimating()
}
} else {
if let activityIndicatorView = self.activityIndicatorView {
self.activityIndicatorView = nil
activityIndicatorView.removeFromSuperview()
}
}
self.separatorInset = contentLeftInset
return CGSize(width: availableSize.width, height: contentHeight)

View File

@ -38,6 +38,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
public let autocapitalizationType: UITextAutocapitalizationType
public let autocorrectionType: UITextAutocorrectionType
public let characterLimit: Int?
public let allowEmptyLines: Bool
public let updated: ((String) -> Void)?
public let textUpdateTransition: Transition
public let tag: AnyObject?
@ -53,6 +54,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default,
characterLimit: Int? = nil,
allowEmptyLines: Bool = true,
updated: ((String) -> Void)?,
textUpdateTransition: Transition = .immediate,
tag: AnyObject? = nil
@ -67,6 +69,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.characterLimit = characterLimit
self.allowEmptyLines = allowEmptyLines
self.updated = updated
self.textUpdateTransition = textUpdateTransition
self.tag = tag
@ -103,6 +106,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
if lhs.characterLimit != rhs.characterLimit {
return false
}
if lhs.allowEmptyLines != rhs.allowEmptyLines {
return false
}
if (lhs.updated == nil) != (rhs.updated == nil) {
return false
}
@ -212,6 +218,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
},
isOneLineWhenUnfocused: false,
characterLimit: component.characterLimit,
allowEmptyLines: component.allowEmptyLines,
formatMenuAvailability: .none,
lockedFormatAction: {
},

View File

@ -345,19 +345,20 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: openTimestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat(
dateFormatString: { value in
return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: [])
let text = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: [])
return presentationData.strings.PeerInfo_BusinessHours_StatusOpensOnDate(text.string)
},
tomorrowFormatString: { value in
return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: [])
return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTomorrowAt(value).string, ranges: [])
},
todayFormatString: { value in
return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: [])
return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTodayAt(value).string, ranges: [])
},
yesterdayFormatString: { value in
return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_YesterdayAt(value).string, ranges: [])
return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTodayAt(value).string, ranges: [])
}
)).string
currentDayStatusText = presentationData.strings.PeerInfo_BusinessHours_StatusOpensOnDate(dateText).string
currentDayStatusText = dateText
}
break
}

View File

@ -26,6 +26,7 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
private var pendingMessages: [PendingMessageContext] = []
private var historyViewDisposable: Disposable?
private var pendingHistoryViewDisposable: Disposable?
let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>()
private var nextUpdateIsHoleFill: Bool = false
@ -43,10 +44,14 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
context.disposable.dispose()
}
self.historyViewDisposable?.dispose()
self.pendingHistoryViewDisposable?.dispose()
}
private func updateHistoryViewRequest(reload: Bool) {
if let shortcutId = self.shortcutId {
self.pendingHistoryViewDisposable?.dispose()
self.pendingHistoryViewDisposable = nil
if self.historyViewDisposable == nil || reload {
self.historyViewDisposable?.dispose()
@ -74,11 +79,28 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
})
}
} else {
if self.sourceHistoryView == nil {
self.historyViewDisposable?.dispose()
self.historyViewDisposable = nil
self.pendingHistoryViewDisposable = (self.context.account.viewTracker.pendingQuickReplyMessagesViewForLocation(shortcut: self.shortcut)
|> deliverOn(self.queue)).start(next: { [weak self] view, _, _ in
guard let self else {
return
}
let nextUpdateIsHoleFill = self.nextUpdateIsHoleFill
self.nextUpdateIsHoleFill = false
self.sourceHistoryView = view
self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic)
})
/*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)
}
}*/
}
}

View File

@ -257,9 +257,9 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
switch component.mode {
case .greeting:
var greetingMessage: TelegramBusinessGreetingMessage?
if self.isOn, let currentShortcut = self.currentShortcut {
if self.isOn, let currentShortcut = self.currentShortcut, let shortcutId = currentShortcut.id {
greetingMessage = TelegramBusinessGreetingMessage(
shortcutId: currentShortcut.id,
shortcutId: shortcutId,
recipients: recipients,
inactivityDays: self.inactivityDays
)
@ -267,7 +267,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
let _ = component.context.engine.accountData.updateBusinessGreetingMessage(greetingMessage: greetingMessage).startStandalone()
case .away:
var awayMessage: TelegramBusinessAwayMessage?
if self.isOn, let currentShortcut = self.currentShortcut {
if self.isOn, let currentShortcut = self.currentShortcut, let shortcutId = currentShortcut.id {
let mappedSchedule: TelegramBusinessAwayMessage.Schedule
switch self.schedule {
case .always:
@ -282,7 +282,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
}
}
awayMessage = TelegramBusinessAwayMessage(
shortcutId: currentShortcut.id,
shortcutId: shortcutId,
recipients: recipients,
schedule: mappedSchedule,
sendWhenOffline: self.sendWhenOffline
@ -654,7 +654,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
self.currentShortcut = component.initialData.shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
self.currentShortcutDisposable = (component.context.engine.accountData.shortcutMessageList()
self.currentShortcutDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false)
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in
guard let self else {
return
@ -1623,7 +1623,7 @@ public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentC
TelegramEngine.EngineData.Item.Peer.BusinessAwayMessage(id: context.account.peerId),
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: context.account.peerId)
),
context.engine.accountData.shortcutMessageList()
context.engine.accountData.shortcutMessageList(onlyRemote: true)
|> take(1)
)
|> mapToSignal { data, shortcutMessageList -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> in

View File

@ -54,6 +54,7 @@ final class QuickReplySetupScreenComponent: Component {
enum Id: Hashable {
case add
case item(Int32)
case pendingItem(String)
}
var stableId: Id {
@ -61,7 +62,11 @@ final class QuickReplySetupScreenComponent: Component {
case .add:
return .add
case let .item(item, _, _, _, _):
return .item(item.id)
if let itemId = item.id {
return .item(itemId)
} else {
return .pendingItem(item.shortcut)
}
}
}
@ -126,13 +131,17 @@ final class QuickReplySetupScreenComponent: Component {
guard let listNode, let parentView = listNode.parentView else {
return
}
parentView.toggleShortcutSelection(id: item.id)
if let itemId = item.id {
parentView.toggleShortcutSelection(id: itemId)
}
},
togglePeersSelection: { [weak listNode] _, _ in
guard let listNode, let parentView = listNode.parentView else {
return
}
parentView.toggleShortcutSelection(id: item.id)
if let itemId = item.id {
parentView.toggleShortcutSelection(id: itemId)
}
},
additionalCategorySelected: { _ in
},
@ -158,7 +167,9 @@ final class QuickReplySetupScreenComponent: Component {
guard let listNode, let parentView = listNode.parentView else {
return
}
parentView.openDeleteShortcuts(ids: [item.id])
if let itemId = item.id {
parentView.openDeleteShortcuts(ids: [itemId])
}
},
deletePeerThread: { _, _ in
},
@ -208,7 +219,9 @@ final class QuickReplySetupScreenComponent: Component {
guard let listNode, let parentView = listNode.parentView else {
return
}
parentView.openEditShortcut(id: item.id, currentValue: item.shortcut)
if let itemId = item.id {
parentView.openEditShortcut(id: itemId, currentValue: item.shortcut)
}
}
)
@ -406,6 +419,8 @@ final class QuickReplySetupScreenComponent: Component {
return true
case let .item(id):
return !pendingRemoveItems.contains(id)
case .pendingItem:
return true
}
}
}
@ -874,7 +889,7 @@ final class QuickReplySetupScreenComponent: Component {
self.accountPeer = component.initialData.accountPeer
self.shortcutMessageList = component.initialData.shortcutMessageList
self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList()
self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false)
|> deliverOnMainQueue).startStrict(next: { [weak self] shortcutMessageList in
guard let self else {
return
@ -937,7 +952,11 @@ final class QuickReplySetupScreenComponent: Component {
)
if let emptyStateView = emptyState.view {
if emptyStateView.superview == nil {
self.addSubview(emptyStateView)
if let navigationBarComponentView = self.navigationBarView.view {
self.insertSubview(emptyStateView, belowSubview: navigationBarComponentView)
} else {
self.addSubview(emptyStateView)
}
}
emptyStateTransition.setFrame(view: emptyStateView, frame: emptyStateFrame)
}
@ -1168,7 +1187,11 @@ final class QuickReplySetupScreenComponent: Component {
continue
}
}
entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: self.selectedIds.contains(item.id)))
var isItemSelected = false
if let itemId = item.id {
isItemSelected = self.selectedIds.contains(itemId)
}
entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: isItemSelected))
}
}
contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate)
@ -1198,7 +1221,11 @@ final class QuickReplySetupScreenComponent: Component {
let emptySearchStateFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - emptySearchStateSize.width) * 0.5), y: navigationHeight + floor((availableSize.height - emptySearchStateBottomInset - navigationHeight) * 0.5)), size: emptySearchStateSize)
if let emptySearchStateView = emptySearchState.view {
if emptySearchStateView.superview == nil {
self.addSubview(emptySearchStateView)
if let navigationBarComponentView = self.navigationBarView.view {
self.insertSubview(emptySearchStateView, belowSubview: navigationBarComponentView)
} else {
self.addSubview(emptySearchStateView)
}
}
emptySearchStateTransition.containedViewLayoutTransition.updatePosition(layer: emptySearchStateView.layer, position: emptySearchStateFrame.center)
emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size)
@ -1327,7 +1354,7 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer, Atta
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
),
context.engine.accountData.shortcutMessageList()
context.engine.accountData.shortcutMessageList(onlyRemote: false)
|> take(1)
)
|> map { accountPeer, shortcutMessageList -> QuickReplySetupScreenInitialData in

View File

@ -121,8 +121,10 @@ final class BusinessDaySetupScreenComponent: Component {
return true
}
if self.intersectingRanges.isEmpty {
return true
if self.isOpen {
if self.intersectingRanges.isEmpty {
return true
}
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }

View File

@ -79,11 +79,13 @@ final class BusinessLocationSetupScreenComponent: Component {
private var resetAddressText: String?
private var isLoadingGeocodedAddress: Bool = false
private var geocodeAddressState: (address: String, disposable: Disposable)?
private var geocodeDisposable: Disposable?
private var mapCoordinates: TelegramBusinessLocation.Coordinates?
private var mapCoordinatesManuallySet: Bool = false
private var applyButtonItem: UIBarButtonItem?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -110,7 +112,7 @@ final class BusinessLocationSetupScreenComponent: Component {
}
deinit {
self.geocodeAddressState?.disposable.dispose()
self.geocodeDisposable?.dispose()
}
func scrollToTop() {
@ -122,24 +124,14 @@ final class BusinessLocationSetupScreenComponent: Component {
return true
}
var address = ""
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
address = textView.currentText
}
let businessLocation = self.currentBusinessLocation()
var businessLocation: TelegramBusinessLocation?
if !address.isEmpty || self.mapCoordinates != nil {
businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates)
}
if businessLocation != nil && address.isEmpty {
if businessLocation != component.initialValue {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_Text, actions: [
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_Text, actions: [
TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .destructiveAction, title: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_ResetAction, action: {
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: nil).startStandalone()
TextAlertAction(type: .destructiveAction, title: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_ResetAction, action: {
complete()
})
]), in: .window(.root))
@ -147,8 +139,6 @@ final class BusinessLocationSetupScreenComponent: Component {
return false
}
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone()
return true
}
@ -186,16 +176,51 @@ final class BusinessLocationSetupScreenComponent: Component {
}
}
private func currentBusinessLocation() -> TelegramBusinessLocation? {
var address = ""
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
address = textView.currentText
}
var businessLocation: TelegramBusinessLocation?
if !address.isEmpty || self.mapCoordinates != nil {
businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates)
}
return businessLocation
}
private func openLocationPicker() {
var initialLocation: CLLocationCoordinate2D?
var initialGeocodedLocation: String?
if let mapCoordinates = self.mapCoordinates {
initialLocation = CLLocationCoordinate2D(latitude: mapCoordinates.latitude, longitude: mapCoordinates.longitude)
} else if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View, textView.currentText.count >= 2 {
initialGeocodedLocation = textView.currentText
}
if let initialGeocodedLocation {
self.isLoadingGeocodedAddress = true
self.state?.updated(transition: .immediate)
self.geocodeDisposable?.dispose()
self.geocodeDisposable = (geocodeLocation(address: initialGeocodedLocation)
|> deliverOnMainQueue).startStrict(next: { [weak self] venues in
guard let self else {
return
}
self.isLoadingGeocodedAddress = false
self.state?.updated(transition: .immediate)
self.presentLocationPicker(initialLocation: venues?.first?.location?.coordinate)
})
} else {
self.presentLocationPicker(initialLocation: initialLocation)
}
}
private func presentLocationPicker(initialLocation: CLLocationCoordinate2D?) {
guard let component = self.component else {
return
}
var initialLocation: CLLocationCoordinate2D?
if let mapCoordinates = self.mapCoordinates {
initialLocation = CLLocationCoordinate2D(latitude: mapCoordinates.latitude, longitude: mapCoordinates.longitude)
}
let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, initialLocation: initialLocation, completion: { [weak self] location, _, _, address, _ in
guard let self else {
return
@ -212,41 +237,30 @@ final class BusinessLocationSetupScreenComponent: Component {
self.environment?.controller()?.push(controller)
}
private func updateGeocodedAddress(string: String) {
let addressValue: String?
if self.mapCoordinates != nil && self.mapCoordinatesManuallySet {
addressValue = nil
} else if string.count < 3 {
addressValue = nil
} else {
addressValue = string
@objc private func savePressed() {
guard let component = self.component, let environment = self.environment else {
return
}
if let current = self.geocodeAddressState, current.address == addressValue {
} else {
self.geocodeAddressState?.disposable.dispose()
self.geocodeAddressState = nil
if let addressValue {
let disposable = MetaDisposable()
self.geocodeAddressState = (string, disposable)
disposable.set((
geocodeLocation(address: addressValue, locale: Locale.current)
|> delay(0.4, queue: .mainQueue())
|> deliverOnMainQueue
).start(next: { [weak self] result in
guard let self else {
return
}
if let location = result?.first?.location, !self.mapCoordinatesManuallySet {
self.mapCoordinates = TelegramBusinessLocation.Coordinates(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
self.state?.updated(transition: .immediate)
}
}))
}
var address = ""
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
address = textView.currentText
}
let businessLocation = self.currentBusinessLocation()
if businessLocation != nil && address.isEmpty {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_Text, actions: [
TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {
})
]), in: .window(.root))
return
}
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone()
environment.controller()?.dismiss()
}
func update(component: BusinessLocationSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
@ -391,7 +405,8 @@ final class BusinessLocationSetupScreenComponent: Component {
placeholder: environment.strings.BusinessLocationSetup_AddressPlaceholder,
autocapitalizationType: .none,
autocorrectionType: .no,
characterLimit: 64,
characterLimit: 256,
allowEmptyLines: false,
updated: { _ in
},
textUpdateTransition: .spring(duration: 0.4),
@ -422,6 +437,14 @@ final class BusinessLocationSetupScreenComponent: Component {
contentHeight += sectionSpacing
var mapSectionItems: [AnyComponentWithIdentity<Empty>] = []
let mapSelectionAccessory: ListActionItemComponent.Accessory?
if self.isLoadingGeocodedAddress {
mapSelectionAccessory = .activity
} else {
mapSelectionAccessory = .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.mapCoordinates != nil, isInteractive: self.mapCoordinates != nil))
}
mapSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
@ -434,7 +457,7 @@ final class BusinessLocationSetupScreenComponent: Component {
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.mapCoordinates != nil, isInteractive: self.mapCoordinates != nil)),
accessory: mapSelectionAccessory,
action: { [weak self] _ in
guard let self else {
return
@ -573,6 +596,26 @@ final class BusinessLocationSetupScreenComponent: Component {
self.updateScrolling(transition: transition)
if let controller = environment.controller() as? BusinessLocationSetupScreen {
let businessLocation = self.currentBusinessLocation()
if businessLocation == component.initialValue {
if controller.navigationItem.rightBarButtonItem != nil {
controller.navigationItem.setRightBarButton(nil, animated: true)
}
} else {
let applyButtonItem: UIBarButtonItem
if let current = self.applyButtonItem {
applyButtonItem = current
} else {
applyButtonItem = UIBarButtonItem(title: environment.strings.Common_Save, style: .done, target: self, action: #selector(self.savePressed))
}
if controller.navigationItem.rightBarButtonItem !== applyButtonItem {
controller.navigationItem.setRightBarButton(applyButtonItem, animated: true)
}
}
}
return availableSize
}
}

View File

@ -97,6 +97,7 @@ public final class TextFieldComponent: Component {
public let resetText: NSAttributedString?
public let isOneLineWhenUnfocused: Bool
public let characterLimit: Int?
public let allowEmptyLines: Bool
public let formatMenuAvailability: FormatMenuAvailability
public let lockedFormatAction: () -> Void
public let present: (ViewController) -> Void
@ -114,6 +115,7 @@ public final class TextFieldComponent: Component {
resetText: NSAttributedString?,
isOneLineWhenUnfocused: Bool,
characterLimit: Int? = nil,
allowEmptyLines: Bool = true,
formatMenuAvailability: FormatMenuAvailability,
lockedFormatAction: @escaping () -> Void,
present: @escaping (ViewController) -> Void,
@ -130,6 +132,7 @@ public final class TextFieldComponent: Component {
self.resetText = resetText
self.isOneLineWhenUnfocused = isOneLineWhenUnfocused
self.characterLimit = characterLimit
self.allowEmptyLines = allowEmptyLines
self.formatMenuAvailability = formatMenuAvailability
self.lockedFormatAction = lockedFormatAction
self.present = present
@ -167,6 +170,9 @@ public final class TextFieldComponent: Component {
if lhs.characterLimit != rhs.characterLimit {
return false
}
if lhs.allowEmptyLines != rhs.allowEmptyLines {
return false
}
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
return false
}
@ -553,6 +559,13 @@ public final class TextFieldComponent: Component {
return false
}
}
if !component.allowEmptyLines {
let string = self.inputState.inputText.string as NSString
let updatedString = string.replacingCharacters(in: range, with: text)
if updatedString.range(of: "\n\n") != nil {
return false
}
}
return true
}

View File

@ -13984,6 +13984,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
}
if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject, let messageLimit = customChatContents.messageLimit {
if let originalHistoryView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, originalHistoryView.entries.count + mappedMessages.count > messageLimit {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_QuickReplyMediaMessageLimitReachedText(Int32(messageLimit)), actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
]), in: .window(.root))
return
}
}
let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime)
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject

View File

@ -34,7 +34,7 @@ extension ChatControllerImpl {
return
}
let _ = (self.context.engine.accountData.shortcutMessageList()
let _ = (self.context.engine.accountData.shortcutMessageList(onlyRemote: false)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in
guard let self else {

View File

@ -271,6 +271,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
private var derivedLayoutState: ChatControllerNodeDerivedLayoutState?
private var loadMoreSearchResultsDisposable: Disposable?
private var isLoadingValue: Bool = false
private var isLoadingEarlier: Bool = false
private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) {
@ -889,6 +891,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.displayVideoUnmuteTipDisposable?.dispose()
self.inputMediaNodeDataDisposable?.dispose()
self.inlineSearchResultsReadyDisposable?.dispose()
self.loadMoreSearchResultsDisposable?.dispose()
}
override func didLoad() {
@ -1636,6 +1639,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
isSelectionEnabled = false
} else if self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState != nil {
isSelectionEnabled = false
} else if case .customChatContents = self.chatLocation {
isSelectionEnabled = false
}
self.historyNode.isSelectionGestureEnabled = isSelectionEnabled
@ -2693,6 +2698,51 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
}
return foundLocalPeers
},
loadMoreSearchResults: { [weak self] in
guard let self, let controller = self.controller else {
return
}
guard let currentSearchState = controller.searchState, let currentResultsState = controller.presentationInterfaceState.search?.resultsState else {
return
}
self.loadMoreSearchResultsDisposable?.dispose()
self.loadMoreSearchResultsDisposable = (self.context.engine.messages.searchMessages(location: currentSearchState.location, query: currentSearchState.query, state: currentResultsState.state)
|> deliverOnMainQueue).startStrict(next: { [weak self] results, updatedState in
guard let self, let controller = self.controller else {
return
}
controller.searchResult.set(.single((results, updatedState, currentSearchState.location)))
var navigateIndex: MessageIndex?
controller.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
if let data = current.search {
let messageIndices = results.messages.map({ $0.index }).sorted()
var currentIndex = messageIndices.last
if let previousResultId = data.resultsState?.currentId {
for index in messageIndices {
if index.id >= previousResultId {
currentIndex = index
break
}
}
}
navigateIndex = currentIndex
return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: updatedState, totalCount: results.totalCount, completed: results.completed)))
} else {
return current
}
})
if let navigateIndex = navigateIndex {
switch controller.chatLocation {
case .peer, .replyThread, .customChatContents:
controller.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true)
}
}
controller.updateItemNodesSearchTextHighlightStates()
})
}
)),
environment: {},

View File

@ -135,7 +135,7 @@ extension ChatControllerImpl {
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted {
buttons = combineLatest(
self.context.engine.messages.attachMenuBots(),
self.context.engine.accountData.shortcutMessageList() |> take(1)
self.context.engine.accountData.shortcutMessageList(onlyRemote: true) |> take(1)
)
|> map { attachMenuBots, shortcutMessageList in
var buttons = availableButtons

View File

@ -237,7 +237,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
if let user = peer as? TelegramUser, user.botInfo == nil {
context.account.viewTracker.keepQuickRepliesApproximatelyUpdated()
shortcuts = context.engine.accountData.shortcutMessageList()
shortcuts = context.engine.accountData.shortcutMessageList(onlyRemote: true)
|> map { shortcutMessageList -> [ShortcutMessageList.Item] in
return shortcutMessageList.items.filter { item in
return item.shortcut.hasPrefix(normalizedQuery)

View File

@ -66,7 +66,11 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable {
case let .command(command):
return .command(command)
case let .shortcut(shortcut):
return .shortcut(shortcut.id)
if let shortcutId = shortcut.id {
return .shortcut(shortcutId)
} else {
return .shortcut(0)
}
}
}
}
@ -375,7 +379,9 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
}
}
case let .shortcut(shortcut):
interfaceInteraction.sendShortcut(shortcut.id)
if let shortcutId = shortcut.id {
interfaceInteraction.sendShortcut(shortcutId)
}
}
}, openEditShortcuts: { [weak self] in
guard let self, let interfaceInteraction = self.interfaceInteraction else {