mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Business fixes
This commit is contained in:
parent
ae998eb91e
commit
7966993955
@ -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.";
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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: {
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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: {},
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user