mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
6d14d5aeb0
@ -11422,6 +11422,8 @@ Sorry for the inconvenience.";
|
|||||||
"PeerInfo.BusinessHours.StatusOpensInHours_1" = "Opens in one hour";
|
"PeerInfo.BusinessHours.StatusOpensInHours_1" = "Opens in one hour";
|
||||||
"PeerInfo.BusinessHours.StatusOpensInHours_any" = "Opens in %d hours";
|
"PeerInfo.BusinessHours.StatusOpensInHours_any" = "Opens in %d hours";
|
||||||
"PeerInfo.BusinessHours.StatusOpensOnDate" = "Opens %@";
|
"PeerInfo.BusinessHours.StatusOpensOnDate" = "Opens %@";
|
||||||
|
"PeerInfo.BusinessHours.StatusOpensTodayAt" = "Opens today at %@";
|
||||||
|
"PeerInfo.BusinessHours.StatusOpensTomorrowAt" = "Opens tomorrow at %@";
|
||||||
"PeerInfo.BusinessHours.TimezoneSwitchMy" = "my time";
|
"PeerInfo.BusinessHours.TimezoneSwitchMy" = "my time";
|
||||||
"PeerInfo.BusinessHours.TimezoneSwitchBusiness" = "local time";
|
"PeerInfo.BusinessHours.TimezoneSwitchBusiness" = "local time";
|
||||||
"PeerInfo.BusinessHours.Label" = "business hours";
|
"PeerInfo.BusinessHours.Label" = "business hours";
|
||||||
@ -11565,6 +11567,9 @@ Sorry for the inconvenience.";
|
|||||||
"BusinessLocationSetup.ErrorAddressEmpty.Text" = "Address can't be empty.";
|
"BusinessLocationSetup.ErrorAddressEmpty.Text" = "Address can't be empty.";
|
||||||
"BusinessLocationSetup.ErrorAddressEmpty.ResetAction" = "Delete";
|
"BusinessLocationSetup.ErrorAddressEmpty.ResetAction" = "Delete";
|
||||||
|
|
||||||
|
"BusinessLocationSetup.AlertUnsavedChanges.Text" = "You have unsaved changes.";
|
||||||
|
"BusinessLocationSetup.AlertUnsavedChanges.ResetAction" = "Revert";
|
||||||
|
|
||||||
"ChatbotSetup.Title" = "Chatbots";
|
"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.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";
|
"ChatbotSetup.TextLink" = "https://telegram.org";
|
||||||
@ -11586,3 +11591,6 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"Chat.QuickReply.ServiceHeader1" = "To edit or delete your quick reply, tap an hold on it.";
|
"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.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)]>()
|
let filtersWithCounts = Promise<[(ChatListFilter, Int)]>()
|
||||||
filtersWithCounts.set(filtersWithCountsSignal)
|
filtersWithCounts.set(filtersWithCountsSignal)
|
||||||
|
|
||||||
|
let animateNextShowHideTagsTransition = Atomic<Bool?>(value: nil)
|
||||||
|
|
||||||
let arguments = ChatListFilterPresetListControllerArguments(context: context,
|
let arguments = ChatListFilterPresetListControllerArguments(context: context,
|
||||||
addSuggestedPressed: { title, data in
|
addSuggestedPressed: { title, data in
|
||||||
let _ = combineLatest(
|
let _ = combineLatest(
|
||||||
@ -580,6 +582,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||||||
|
|
||||||
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
|
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
|
||||||
|
|
||||||
|
let previousDisplayTags = Atomic<Bool?>(value: nil)
|
||||||
|
|
||||||
let limits = context.engine.data.get(
|
let limits = context.engine.data.get(
|
||||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
||||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
||||||
@ -668,6 +672,11 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||||||
rightNavigationButton = nil
|
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 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 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)
|
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
|
return controller
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
|
|||||||
|
|
||||||
private let titleFont = Font.regular(17.0)
|
private let titleFont = Font.regular(17.0)
|
||||||
|
|
||||||
private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
|
final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
|
||||||
private let backgroundNode: ASDisplayNode
|
private let backgroundNode: ASDisplayNode
|
||||||
private let topStripeNode: ASDisplayNode
|
private let topStripeNode: ASDisplayNode
|
||||||
private let bottomStripeNode: ASDisplayNode
|
private let bottomStripeNode: ASDisplayNode
|
||||||
@ -415,7 +415,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
|
|||||||
if item.tagColor != nil {
|
if item.tagColor != nil {
|
||||||
sharedIconFrame.origin.x -= 34.0
|
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
|
var isShared = false
|
||||||
if case let .filter(_, _, _, data) = item.preset, data.isShared {
|
if case let .filter(_, _, _, data) = item.preset, data.isShared {
|
||||||
@ -425,9 +429,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
|
|||||||
|
|
||||||
if let tagColor = item.tagColor {
|
if let tagColor = item.tagColor {
|
||||||
let tagIconView: UIImageView
|
let tagIconView: UIImageView
|
||||||
|
var tagIconTransition = transition
|
||||||
if let current = strongSelf.tagIconView {
|
if let current = strongSelf.tagIconView {
|
||||||
tagIconView = current
|
tagIconView = current
|
||||||
} else {
|
} else {
|
||||||
|
tagIconTransition = .immediate
|
||||||
tagIconView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 24.0, color: .white)?.withRenderingMode(.alwaysTemplate))
|
tagIconView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 24.0, color: .white)?.withRenderingMode(.alwaysTemplate))
|
||||||
strongSelf.tagIconView = tagIconView
|
strongSelf.tagIconView = tagIconView
|
||||||
strongSelf.containerNode.view.addSubview(tagIconView)
|
strongSelf.containerNode.view.addSubview(tagIconView)
|
||||||
@ -435,13 +441,16 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
|
|||||||
tagIconView.tintColor = tagColor
|
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))
|
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 {
|
} else {
|
||||||
if let tagIconView = strongSelf.tagIconView {
|
if let tagIconView = strongSelf.tagIconView {
|
||||||
strongSelf.tagIconView = nil
|
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) {
|
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||||
|
|
||||||
|
@ -1440,7 +1440,8 @@ open class TextNode: ASDisplayNode {
|
|||||||
let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount))
|
let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount))
|
||||||
var lineAscent: CGFloat = 0.0
|
var lineAscent: CGFloat = 0.0
|
||||||
var lineDescent: CGFloat = 0.0
|
var lineDescent: CGFloat = 0.0
|
||||||
let lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil)
|
var lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil)
|
||||||
|
lineWidth = min(lineWidth, constrainedSegmentWidth - additionalSegmentRightInset)
|
||||||
|
|
||||||
var isRTL = false
|
var isRTL = false
|
||||||
let glyphRuns = CTLineGetGlyphRuns(line) as NSArray
|
let glyphRuns = CTLineGetGlyphRuns(line) as NSArray
|
||||||
|
@ -251,6 +251,13 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
|
|||||||
|
|
||||||
public var willDisappear: ((Bool) -> Void)?
|
public var willDisappear: ((Bool) -> Void)?
|
||||||
public var didDisappear: ((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>?) {
|
public init<ItemGenerationArguments>(presentationData: ItemListPresentationData, updatedPresentationData: Signal<ItemListPresentationData, NoError>, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal<ItemListControllerTabBarItem, NoError>?) {
|
||||||
self.state = state
|
self.state = state
|
||||||
@ -486,6 +493,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
|
|||||||
displayNode.searchActivated = self.searchActivated
|
displayNode.searchActivated = self.searchActivated
|
||||||
displayNode.reorderEntry = self.reorderEntry
|
displayNode.reorderEntry = self.reorderEntry
|
||||||
displayNode.reorderCompleted = self.reorderCompleted
|
displayNode.reorderCompleted = self.reorderCompleted
|
||||||
|
displayNode.afterTransactionCompleted = self.afterTransactionCompleted
|
||||||
displayNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem
|
displayNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem
|
||||||
displayNode.listNode.didScrollWithOffset = self.didScrollWithOffset
|
displayNode.listNode.didScrollWithOffset = self.didScrollWithOffset
|
||||||
displayNode.requestLayout = { [weak self] transition in
|
displayNode.requestLayout = { [weak self] transition in
|
||||||
|
@ -285,6 +285,7 @@ open class ItemListControllerNode: ASDisplayNode {
|
|||||||
public var searchActivated: ((Bool) -> Void)?
|
public var searchActivated: ((Bool) -> Void)?
|
||||||
public var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Signal<Bool, NoError>)?
|
public var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Signal<Bool, NoError>)?
|
||||||
public var reorderCompleted: (([ItemListNodeAnyEntry]) -> Void)?
|
public var reorderCompleted: (([ItemListNodeAnyEntry]) -> Void)?
|
||||||
|
public var afterTransactionCompleted: (() -> Void)?
|
||||||
public var requestLayout: ((ContainedViewLayoutTransition) -> Void)?
|
public var requestLayout: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
public var enableInteractiveDismiss = false {
|
public var enableInteractiveDismiss = false {
|
||||||
@ -920,6 +921,8 @@ open class ItemListControllerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strongSelf.afterTransactionCompleted?()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
var updateEmptyStateItem = false
|
var updateEmptyStateItem = false
|
||||||
|
@ -3621,6 +3621,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
|
|||||||
}
|
}
|
||||||
navigationController.pushViewController(peerSelectionController)
|
navigationController.pushViewController(peerSelectionController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if case .business = mode {
|
||||||
|
context.account.viewTracker.keepQuickRepliesApproximatelyUpdated()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init(coder aDecoder: NSCoder) {
|
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> {
|
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 {
|
if let account = self.account {
|
||||||
let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>
|
let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>
|
||||||
|
@ -162,8 +162,8 @@ public extension TelegramEngine {
|
|||||||
|> then(remoteApply)
|
|> then(remoteApply)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func shortcutMessageList() -> Signal<ShortcutMessageList, NoError> {
|
public func shortcutMessageList(onlyRemote: Bool) -> Signal<ShortcutMessageList, NoError> {
|
||||||
return _internal_shortcutMessageList(account: self.account)
|
return _internal_shortcutMessageList(account: self.account, onlyRemote: onlyRemote)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func keepShortcutMessageListUpdated() -> Signal<Never, NoError> {
|
public func keepShortcutMessageListUpdated() -> Signal<Never, NoError> {
|
||||||
|
@ -48,12 +48,12 @@ struct QuickReplyMessageShortcutsState: Codable, Equatable {
|
|||||||
|
|
||||||
public final class ShortcutMessageList: Equatable {
|
public final class ShortcutMessageList: Equatable {
|
||||||
public final class Item: Equatable {
|
public final class Item: Equatable {
|
||||||
public let id: Int32
|
public let id: Int32?
|
||||||
public let shortcut: String
|
public let shortcut: String
|
||||||
public let topMessage: EngineMessage
|
public let topMessage: EngineMessage
|
||||||
public let totalCount: Int
|
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.id = id
|
||||||
self.shortcut = shortcut
|
self.shortcut = shortcut
|
||||||
self.topMessage = topMessage
|
self.topMessage = topMessage
|
||||||
@ -117,12 +117,15 @@ func _internal_quickReplyMessageShortcutsState(account: Account) -> Signal<Quick
|
|||||||
}
|
}
|
||||||
|
|
||||||
func _internal_keepShortcutMessagesUpdated(account: Account) -> Signal<Never, NoError> {
|
func _internal_keepShortcutMessagesUpdated(account: Account) -> Signal<Never, NoError> {
|
||||||
let updateSignal = _internal_shortcutMessageList(account: account)
|
let updateSignal = _internal_shortcutMessageList(account: account, onlyRemote: true)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> mapToSignal { list -> Signal<Never, NoError> in
|
|> mapToSignal { list -> Signal<Never, NoError> in
|
||||||
var acc: UInt64 = 0
|
var acc: UInt64 = 0
|
||||||
for item in list.items {
|
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: md5StringHash(item.shortcut))
|
||||||
combineInt64Hash(&acc, with: UInt64(item.topMessage.id.id))
|
combineInt64Hash(&acc, with: UInt64(item.topMessage.id.id))
|
||||||
|
|
||||||
@ -204,10 +207,42 @@ func _internal_keepShortcutMessagesUpdated(account: Account) -> Signal<Never, No
|
|||||||
return updateSignal
|
return updateSignal
|
||||||
}
|
}
|
||||||
|
|
||||||
func _internal_shortcutMessageList(account: Account) -> Signal<ShortcutMessageList, NoError> {
|
func _internal_shortcutMessageList(account: Account, onlyRemote: Bool) -> Signal<ShortcutMessageList, NoError> {
|
||||||
return _internal_quickReplyMessageShortcutsState(account: account)
|
let pendingShortcuts: Signal<[String: EngineMessage], NoError>
|
||||||
|> distinctUntilChanged
|
if onlyRemote {
|
||||||
|> mapToSignal { state -> Signal<ShortcutMessageList, NoError> in
|
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 {
|
guard let state else {
|
||||||
return .single(ShortcutMessageList(items: [], isLoading: true))
|
return .single(ShortcutMessageList(items: [], isLoading: true))
|
||||||
}
|
}
|
||||||
@ -238,6 +273,7 @@ func _internal_shortcutMessageList(account: Account) -> Signal<ShortcutMessageLi
|
|||||||
)
|
)
|
||||||
|> map { views -> ShortcutMessageList in
|
|> map { views -> ShortcutMessageList in
|
||||||
var items: [ShortcutMessageList.Item] = []
|
var items: [ShortcutMessageList.Item] = []
|
||||||
|
|
||||||
for shortcut in state.shortcuts {
|
for shortcut in state.shortcuts {
|
||||||
guard let historyViewKey = historyViewKeys[shortcut.id], let historyView = views.views[historyViewKey] as? MessageHistoryView else {
|
guard let historyViewKey = historyViewKeys[shortcut.id], let historyView = views.views[historyViewKey] as? MessageHistoryView else {
|
||||||
continue
|
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))
|
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)
|
return ShortcutMessageList(items: items, isLoading: false)
|
||||||
}
|
}
|
||||||
|> distinctUntilChanged
|
|> distinctUntilChanged
|
||||||
|
@ -978,6 +978,22 @@ func _internal_updateChatListFiltersDisplayTagsInteractively(postbox: Postbox, d
|
|||||||
var state = entry?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default
|
var state = entry?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default
|
||||||
if displayTags != state.displayTags {
|
if displayTags != state.displayTags {
|
||||||
state.displayTags = 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
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +81,7 @@ public final class ChatInlineSearchResultsListComponent: Component {
|
|||||||
public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal<MessageHistoryView, NoError>?
|
public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal<MessageHistoryView, NoError>?
|
||||||
public let getSearchResult: () -> Signal<SearchMessagesResult?, NoError>?
|
public let getSearchResult: () -> Signal<SearchMessagesResult?, NoError>?
|
||||||
public let getSavedPeers: (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?
|
public let getSavedPeers: (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?
|
||||||
|
public let loadMoreSearchResults: () -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
@ -92,7 +93,8 @@ public final class ChatInlineSearchResultsListComponent: Component {
|
|||||||
peerSelected: @escaping (EnginePeer) -> Void,
|
peerSelected: @escaping (EnginePeer) -> Void,
|
||||||
loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal<MessageHistoryView, NoError>?,
|
loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal<MessageHistoryView, NoError>?,
|
||||||
getSearchResult: @escaping () -> Signal<SearchMessagesResult?, 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.context = context
|
||||||
self.presentation = presentation
|
self.presentation = presentation
|
||||||
@ -104,6 +106,7 @@ public final class ChatInlineSearchResultsListComponent: Component {
|
|||||||
self.loadTagMessages = loadTagMessages
|
self.loadTagMessages = loadTagMessages
|
||||||
self.getSearchResult = getSearchResult
|
self.getSearchResult = getSearchResult
|
||||||
self.getSavedPeers = getSavedPeers
|
self.getSavedPeers = getSavedPeers
|
||||||
|
self.loadMoreSearchResults = loadMoreSearchResults
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: ChatInlineSearchResultsListComponent, rhs: ChatInlineSearchResultsListComponent) -> Bool {
|
public static func ==(lhs: ChatInlineSearchResultsListComponent, rhs: ChatInlineSearchResultsListComponent) -> Bool {
|
||||||
@ -377,6 +380,19 @@ public final class ChatInlineSearchResultsListComponent: Component {
|
|||||||
break
|
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),
|
contentId: .search(query),
|
||||||
entries: entries,
|
entries: entries,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
hasEarlier: false,
|
hasEarlier: !(result?.completed ?? true),
|
||||||
hasLater: false
|
hasLater: false
|
||||||
)
|
)
|
||||||
if !self.isUpdating {
|
if !self.isUpdating {
|
||||||
|
@ -45,6 +45,7 @@ public final class ListActionItemComponent: Component {
|
|||||||
public enum Accessory: Equatable {
|
public enum Accessory: Equatable {
|
||||||
case arrow
|
case arrow
|
||||||
case toggle(Toggle)
|
case toggle(Toggle)
|
||||||
|
case activity
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum IconInsets: Equatable {
|
public enum IconInsets: Equatable {
|
||||||
@ -123,6 +124,7 @@ public final class ListActionItemComponent: Component {
|
|||||||
private var arrowView: UIImageView?
|
private var arrowView: UIImageView?
|
||||||
private var switchNode: SwitchNode?
|
private var switchNode: SwitchNode?
|
||||||
private var iconSwitchNode: IconSwitchNode?
|
private var iconSwitchNode: IconSwitchNode?
|
||||||
|
private var activityIndicatorView: UIActivityIndicatorView?
|
||||||
|
|
||||||
private var component: ListActionItemComponent?
|
private var component: ListActionItemComponent?
|
||||||
|
|
||||||
@ -191,6 +193,8 @@ public final class ListActionItemComponent: Component {
|
|||||||
contentRightInset = 30.0
|
contentRightInset = 30.0
|
||||||
case .toggle:
|
case .toggle:
|
||||||
contentRightInset = 76.0
|
contentRightInset = 76.0
|
||||||
|
case .activity:
|
||||||
|
contentRightInset = 76.0
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentHeight: CGFloat = 0.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
|
self.separatorInset = contentLeftInset
|
||||||
|
|
||||||
return CGSize(width: availableSize.width, height: contentHeight)
|
return CGSize(width: availableSize.width, height: contentHeight)
|
||||||
|
@ -38,6 +38,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
public let autocapitalizationType: UITextAutocapitalizationType
|
public let autocapitalizationType: UITextAutocapitalizationType
|
||||||
public let autocorrectionType: UITextAutocorrectionType
|
public let autocorrectionType: UITextAutocorrectionType
|
||||||
public let characterLimit: Int?
|
public let characterLimit: Int?
|
||||||
|
public let allowEmptyLines: Bool
|
||||||
public let updated: ((String) -> Void)?
|
public let updated: ((String) -> Void)?
|
||||||
public let textUpdateTransition: Transition
|
public let textUpdateTransition: Transition
|
||||||
public let tag: AnyObject?
|
public let tag: AnyObject?
|
||||||
@ -53,6 +54,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
||||||
autocorrectionType: UITextAutocorrectionType = .default,
|
autocorrectionType: UITextAutocorrectionType = .default,
|
||||||
characterLimit: Int? = nil,
|
characterLimit: Int? = nil,
|
||||||
|
allowEmptyLines: Bool = true,
|
||||||
updated: ((String) -> Void)?,
|
updated: ((String) -> Void)?,
|
||||||
textUpdateTransition: Transition = .immediate,
|
textUpdateTransition: Transition = .immediate,
|
||||||
tag: AnyObject? = nil
|
tag: AnyObject? = nil
|
||||||
@ -67,6 +69,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
self.autocapitalizationType = autocapitalizationType
|
self.autocapitalizationType = autocapitalizationType
|
||||||
self.autocorrectionType = autocorrectionType
|
self.autocorrectionType = autocorrectionType
|
||||||
self.characterLimit = characterLimit
|
self.characterLimit = characterLimit
|
||||||
|
self.allowEmptyLines = allowEmptyLines
|
||||||
self.updated = updated
|
self.updated = updated
|
||||||
self.textUpdateTransition = textUpdateTransition
|
self.textUpdateTransition = textUpdateTransition
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
@ -103,6 +106,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
if lhs.characterLimit != rhs.characterLimit {
|
if lhs.characterLimit != rhs.characterLimit {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.allowEmptyLines != rhs.allowEmptyLines {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (lhs.updated == nil) != (rhs.updated == nil) {
|
if (lhs.updated == nil) != (rhs.updated == nil) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -212,6 +218,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
},
|
},
|
||||||
isOneLineWhenUnfocused: false,
|
isOneLineWhenUnfocused: false,
|
||||||
characterLimit: component.characterLimit,
|
characterLimit: component.characterLimit,
|
||||||
|
allowEmptyLines: component.allowEmptyLines,
|
||||||
formatMenuAvailability: .none,
|
formatMenuAvailability: .none,
|
||||||
lockedFormatAction: {
|
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(
|
let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: openTimestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat(
|
||||||
dateFormatString: { value in
|
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
|
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
|
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
|
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
|
)).string
|
||||||
currentDayStatusText = presentationData.strings.PeerInfo_BusinessHours_StatusOpensOnDate(dateText).string
|
currentDayStatusText = dateText
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
|
|||||||
|
|
||||||
private var pendingMessages: [PendingMessageContext] = []
|
private var pendingMessages: [PendingMessageContext] = []
|
||||||
private var historyViewDisposable: Disposable?
|
private var historyViewDisposable: Disposable?
|
||||||
|
private var pendingHistoryViewDisposable: Disposable?
|
||||||
let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>()
|
let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>()
|
||||||
private var nextUpdateIsHoleFill: Bool = false
|
private var nextUpdateIsHoleFill: Bool = false
|
||||||
|
|
||||||
@ -43,10 +44,14 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
|
|||||||
context.disposable.dispose()
|
context.disposable.dispose()
|
||||||
}
|
}
|
||||||
self.historyViewDisposable?.dispose()
|
self.historyViewDisposable?.dispose()
|
||||||
|
self.pendingHistoryViewDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateHistoryViewRequest(reload: Bool) {
|
private func updateHistoryViewRequest(reload: Bool) {
|
||||||
if let shortcutId = self.shortcutId {
|
if let shortcutId = self.shortcutId {
|
||||||
|
self.pendingHistoryViewDisposable?.dispose()
|
||||||
|
self.pendingHistoryViewDisposable = nil
|
||||||
|
|
||||||
if self.historyViewDisposable == nil || reload {
|
if self.historyViewDisposable == nil || reload {
|
||||||
self.historyViewDisposable?.dispose()
|
self.historyViewDisposable?.dispose()
|
||||||
|
|
||||||
@ -74,11 +79,28 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
let sourceHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: [], holeEarlier: false, holeLater: false, isLoading: false)
|
||||||
self.sourceHistoryView = sourceHistoryView
|
self.sourceHistoryView = sourceHistoryView
|
||||||
self.updateHistoryView(updateType: .Initial)
|
self.updateHistoryView(updateType: .Initial)
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,9 +257,9 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
|||||||
switch component.mode {
|
switch component.mode {
|
||||||
case .greeting:
|
case .greeting:
|
||||||
var greetingMessage: TelegramBusinessGreetingMessage?
|
var greetingMessage: TelegramBusinessGreetingMessage?
|
||||||
if self.isOn, let currentShortcut = self.currentShortcut {
|
if self.isOn, let currentShortcut = self.currentShortcut, let shortcutId = currentShortcut.id {
|
||||||
greetingMessage = TelegramBusinessGreetingMessage(
|
greetingMessage = TelegramBusinessGreetingMessage(
|
||||||
shortcutId: currentShortcut.id,
|
shortcutId: shortcutId,
|
||||||
recipients: recipients,
|
recipients: recipients,
|
||||||
inactivityDays: self.inactivityDays
|
inactivityDays: self.inactivityDays
|
||||||
)
|
)
|
||||||
@ -267,7 +267,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
|||||||
let _ = component.context.engine.accountData.updateBusinessGreetingMessage(greetingMessage: greetingMessage).startStandalone()
|
let _ = component.context.engine.accountData.updateBusinessGreetingMessage(greetingMessage: greetingMessage).startStandalone()
|
||||||
case .away:
|
case .away:
|
||||||
var awayMessage: TelegramBusinessAwayMessage?
|
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
|
let mappedSchedule: TelegramBusinessAwayMessage.Schedule
|
||||||
switch self.schedule {
|
switch self.schedule {
|
||||||
case .always:
|
case .always:
|
||||||
@ -282,7 +282,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
awayMessage = TelegramBusinessAwayMessage(
|
awayMessage = TelegramBusinessAwayMessage(
|
||||||
shortcutId: currentShortcut.id,
|
shortcutId: shortcutId,
|
||||||
recipients: recipients,
|
recipients: recipients,
|
||||||
schedule: mappedSchedule,
|
schedule: mappedSchedule,
|
||||||
sendWhenOffline: self.sendWhenOffline
|
sendWhenOffline: self.sendWhenOffline
|
||||||
@ -654,7 +654,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
|||||||
|
|
||||||
self.currentShortcut = component.initialData.shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
|
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
|
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -1623,7 +1623,7 @@ public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentC
|
|||||||
TelegramEngine.EngineData.Item.Peer.BusinessAwayMessage(id: context.account.peerId),
|
TelegramEngine.EngineData.Item.Peer.BusinessAwayMessage(id: context.account.peerId),
|
||||||
TelegramEngine.EngineData.Item.Peer.BusinessHours(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)
|
|> take(1)
|
||||||
)
|
)
|
||||||
|> mapToSignal { data, shortcutMessageList -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> in
|
|> mapToSignal { data, shortcutMessageList -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> in
|
||||||
|
@ -54,6 +54,7 @@ final class QuickReplySetupScreenComponent: Component {
|
|||||||
enum Id: Hashable {
|
enum Id: Hashable {
|
||||||
case add
|
case add
|
||||||
case item(Int32)
|
case item(Int32)
|
||||||
|
case pendingItem(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
var stableId: Id {
|
var stableId: Id {
|
||||||
@ -61,7 +62,11 @@ final class QuickReplySetupScreenComponent: Component {
|
|||||||
case .add:
|
case .add:
|
||||||
return .add
|
return .add
|
||||||
case let .item(item, _, _, _, _):
|
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 {
|
guard let listNode, let parentView = listNode.parentView else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
parentView.toggleShortcutSelection(id: item.id)
|
if let itemId = item.id {
|
||||||
|
parentView.toggleShortcutSelection(id: itemId)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
togglePeersSelection: { [weak listNode] _, _ in
|
togglePeersSelection: { [weak listNode] _, _ in
|
||||||
guard let listNode, let parentView = listNode.parentView else {
|
guard let listNode, let parentView = listNode.parentView else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
parentView.toggleShortcutSelection(id: item.id)
|
if let itemId = item.id {
|
||||||
|
parentView.toggleShortcutSelection(id: itemId)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
additionalCategorySelected: { _ in
|
additionalCategorySelected: { _ in
|
||||||
},
|
},
|
||||||
@ -158,7 +167,9 @@ final class QuickReplySetupScreenComponent: Component {
|
|||||||
guard let listNode, let parentView = listNode.parentView else {
|
guard let listNode, let parentView = listNode.parentView else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
parentView.openDeleteShortcuts(ids: [item.id])
|
if let itemId = item.id {
|
||||||
|
parentView.openDeleteShortcuts(ids: [itemId])
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deletePeerThread: { _, _ in
|
deletePeerThread: { _, _ in
|
||||||
},
|
},
|
||||||
@ -208,7 +219,9 @@ final class QuickReplySetupScreenComponent: Component {
|
|||||||
guard let listNode, let parentView = listNode.parentView else {
|
guard let listNode, let parentView = listNode.parentView else {
|
||||||
return
|
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
|
return true
|
||||||
case let .item(id):
|
case let .item(id):
|
||||||
return !pendingRemoveItems.contains(id)
|
return !pendingRemoveItems.contains(id)
|
||||||
|
case .pendingItem:
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -874,7 +889,7 @@ final class QuickReplySetupScreenComponent: Component {
|
|||||||
self.accountPeer = component.initialData.accountPeer
|
self.accountPeer = component.initialData.accountPeer
|
||||||
self.shortcutMessageList = component.initialData.shortcutMessageList
|
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
|
|> deliverOnMainQueue).startStrict(next: { [weak self] shortcutMessageList in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -937,7 +952,11 @@ final class QuickReplySetupScreenComponent: Component {
|
|||||||
)
|
)
|
||||||
if let emptyStateView = emptyState.view {
|
if let emptyStateView = emptyState.view {
|
||||||
if emptyStateView.superview == nil {
|
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)
|
emptyStateTransition.setFrame(view: emptyStateView, frame: emptyStateFrame)
|
||||||
}
|
}
|
||||||
@ -1168,7 +1187,11 @@ final class QuickReplySetupScreenComponent: Component {
|
|||||||
continue
|
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)
|
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)
|
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 let emptySearchStateView = emptySearchState.view {
|
||||||
if emptySearchStateView.superview == nil {
|
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)
|
emptySearchStateTransition.containedViewLayoutTransition.updatePosition(layer: emptySearchStateView.layer, position: emptySearchStateFrame.center)
|
||||||
emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size)
|
emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size)
|
||||||
@ -1327,7 +1354,7 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer, Atta
|
|||||||
context.engine.data.get(
|
context.engine.data.get(
|
||||||
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
|
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
|
||||||
),
|
),
|
||||||
context.engine.accountData.shortcutMessageList()
|
context.engine.accountData.shortcutMessageList(onlyRemote: false)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
)
|
)
|
||||||
|> map { accountPeer, shortcutMessageList -> QuickReplySetupScreenInitialData in
|
|> map { accountPeer, shortcutMessageList -> QuickReplySetupScreenInitialData in
|
||||||
|
@ -121,8 +121,10 @@ final class BusinessDaySetupScreenComponent: Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.intersectingRanges.isEmpty {
|
if self.isOpen {
|
||||||
return true
|
if self.intersectingRanges.isEmpty {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
@ -79,11 +79,13 @@ final class BusinessLocationSetupScreenComponent: Component {
|
|||||||
private var resetAddressText: String?
|
private var resetAddressText: String?
|
||||||
|
|
||||||
private var isLoadingGeocodedAddress: Bool = false
|
private var isLoadingGeocodedAddress: Bool = false
|
||||||
private var geocodeAddressState: (address: String, disposable: Disposable)?
|
private var geocodeDisposable: Disposable?
|
||||||
|
|
||||||
private var mapCoordinates: TelegramBusinessLocation.Coordinates?
|
private var mapCoordinates: TelegramBusinessLocation.Coordinates?
|
||||||
private var mapCoordinatesManuallySet: Bool = false
|
private var mapCoordinatesManuallySet: Bool = false
|
||||||
|
|
||||||
|
private var applyButtonItem: UIBarButtonItem?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.scrollView = ScrollView()
|
self.scrollView = ScrollView()
|
||||||
self.scrollView.showsVerticalScrollIndicator = true
|
self.scrollView.showsVerticalScrollIndicator = true
|
||||||
@ -110,7 +112,7 @@ final class BusinessLocationSetupScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.geocodeAddressState?.disposable.dispose()
|
self.geocodeDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollToTop() {
|
func scrollToTop() {
|
||||||
@ -122,24 +124,14 @@ final class BusinessLocationSetupScreenComponent: Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
var address = ""
|
let businessLocation = self.currentBusinessLocation()
|
||||||
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
|
|
||||||
address = textView.currentText
|
|
||||||
}
|
|
||||||
|
|
||||||
var businessLocation: TelegramBusinessLocation?
|
if businessLocation != component.initialValue {
|
||||||
if !address.isEmpty || self.mapCoordinates != nil {
|
|
||||||
businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates)
|
|
||||||
}
|
|
||||||
|
|
||||||
if businessLocation != nil && address.isEmpty {
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
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: .genericAction, title: environment.strings.Common_Cancel, action: {
|
||||||
}),
|
}),
|
||||||
TextAlertAction(type: .destructiveAction, title: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_ResetAction, action: {
|
TextAlertAction(type: .destructiveAction, title: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_ResetAction, action: {
|
||||||
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: nil).startStandalone()
|
|
||||||
|
|
||||||
complete()
|
complete()
|
||||||
})
|
})
|
||||||
]), in: .window(.root))
|
]), in: .window(.root))
|
||||||
@ -147,8 +139,6 @@ final class BusinessLocationSetupScreenComponent: Component {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone()
|
|
||||||
|
|
||||||
return true
|
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() {
|
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 {
|
guard let component = self.component else {
|
||||||
return
|
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
|
let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, initialLocation: initialLocation, completion: { [weak self] location, _, _, address, _ in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -212,41 +237,30 @@ final class BusinessLocationSetupScreenComponent: Component {
|
|||||||
self.environment?.controller()?.push(controller)
|
self.environment?.controller()?.push(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateGeocodedAddress(string: String) {
|
@objc private func savePressed() {
|
||||||
let addressValue: String?
|
guard let component = self.component, let environment = self.environment else {
|
||||||
if self.mapCoordinates != nil && self.mapCoordinatesManuallySet {
|
return
|
||||||
addressValue = nil
|
|
||||||
} else if string.count < 3 {
|
|
||||||
addressValue = nil
|
|
||||||
} else {
|
|
||||||
addressValue = string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let current = self.geocodeAddressState, current.address == addressValue {
|
var address = ""
|
||||||
} else {
|
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
|
||||||
self.geocodeAddressState?.disposable.dispose()
|
address = textView.currentText
|
||||||
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)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
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,
|
placeholder: environment.strings.BusinessLocationSetup_AddressPlaceholder,
|
||||||
autocapitalizationType: .none,
|
autocapitalizationType: .none,
|
||||||
autocorrectionType: .no,
|
autocorrectionType: .no,
|
||||||
characterLimit: 64,
|
characterLimit: 256,
|
||||||
|
allowEmptyLines: false,
|
||||||
updated: { _ in
|
updated: { _ in
|
||||||
},
|
},
|
||||||
textUpdateTransition: .spring(duration: 0.4),
|
textUpdateTransition: .spring(duration: 0.4),
|
||||||
@ -422,6 +437,14 @@ final class BusinessLocationSetupScreenComponent: Component {
|
|||||||
contentHeight += sectionSpacing
|
contentHeight += sectionSpacing
|
||||||
|
|
||||||
var mapSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
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(
|
mapSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
title: AnyComponent(VStack([
|
title: AnyComponent(VStack([
|
||||||
@ -434,7 +457,7 @@ final class BusinessLocationSetupScreenComponent: Component {
|
|||||||
maximumNumberOfLines: 1
|
maximumNumberOfLines: 1
|
||||||
))),
|
))),
|
||||||
], alignment: .left, spacing: 2.0)),
|
], 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
|
action: { [weak self] _ in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -573,6 +596,26 @@ final class BusinessLocationSetupScreenComponent: Component {
|
|||||||
|
|
||||||
self.updateScrolling(transition: transition)
|
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
|
return availableSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,7 @@ public final class TextFieldComponent: Component {
|
|||||||
public let resetText: NSAttributedString?
|
public let resetText: NSAttributedString?
|
||||||
public let isOneLineWhenUnfocused: Bool
|
public let isOneLineWhenUnfocused: Bool
|
||||||
public let characterLimit: Int?
|
public let characterLimit: Int?
|
||||||
|
public let allowEmptyLines: Bool
|
||||||
public let formatMenuAvailability: FormatMenuAvailability
|
public let formatMenuAvailability: FormatMenuAvailability
|
||||||
public let lockedFormatAction: () -> Void
|
public let lockedFormatAction: () -> Void
|
||||||
public let present: (ViewController) -> Void
|
public let present: (ViewController) -> Void
|
||||||
@ -114,6 +115,7 @@ public final class TextFieldComponent: Component {
|
|||||||
resetText: NSAttributedString?,
|
resetText: NSAttributedString?,
|
||||||
isOneLineWhenUnfocused: Bool,
|
isOneLineWhenUnfocused: Bool,
|
||||||
characterLimit: Int? = nil,
|
characterLimit: Int? = nil,
|
||||||
|
allowEmptyLines: Bool = true,
|
||||||
formatMenuAvailability: FormatMenuAvailability,
|
formatMenuAvailability: FormatMenuAvailability,
|
||||||
lockedFormatAction: @escaping () -> Void,
|
lockedFormatAction: @escaping () -> Void,
|
||||||
present: @escaping (ViewController) -> Void,
|
present: @escaping (ViewController) -> Void,
|
||||||
@ -130,6 +132,7 @@ public final class TextFieldComponent: Component {
|
|||||||
self.resetText = resetText
|
self.resetText = resetText
|
||||||
self.isOneLineWhenUnfocused = isOneLineWhenUnfocused
|
self.isOneLineWhenUnfocused = isOneLineWhenUnfocused
|
||||||
self.characterLimit = characterLimit
|
self.characterLimit = characterLimit
|
||||||
|
self.allowEmptyLines = allowEmptyLines
|
||||||
self.formatMenuAvailability = formatMenuAvailability
|
self.formatMenuAvailability = formatMenuAvailability
|
||||||
self.lockedFormatAction = lockedFormatAction
|
self.lockedFormatAction = lockedFormatAction
|
||||||
self.present = present
|
self.present = present
|
||||||
@ -167,6 +170,9 @@ public final class TextFieldComponent: Component {
|
|||||||
if lhs.characterLimit != rhs.characterLimit {
|
if lhs.characterLimit != rhs.characterLimit {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.allowEmptyLines != rhs.allowEmptyLines {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
|
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -553,6 +559,13 @@ public final class TextFieldComponent: Component {
|
|||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
@ -13981,6 +13981,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 messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime)
|
||||||
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||||
|
@ -34,7 +34,7 @@ extension ChatControllerImpl {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = (self.context.engine.accountData.shortcutMessageList()
|
let _ = (self.context.engine.accountData.shortcutMessageList(onlyRemote: false)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in
|
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
|
@ -271,6 +271,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
private var derivedLayoutState: ChatControllerNodeDerivedLayoutState?
|
private var derivedLayoutState: ChatControllerNodeDerivedLayoutState?
|
||||||
|
|
||||||
|
private var loadMoreSearchResultsDisposable: Disposable?
|
||||||
|
|
||||||
private var isLoadingValue: Bool = false
|
private var isLoadingValue: Bool = false
|
||||||
private var isLoadingEarlier: Bool = false
|
private var isLoadingEarlier: Bool = false
|
||||||
private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) {
|
private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) {
|
||||||
@ -889,6 +891,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
self.displayVideoUnmuteTipDisposable?.dispose()
|
self.displayVideoUnmuteTipDisposable?.dispose()
|
||||||
self.inputMediaNodeDataDisposable?.dispose()
|
self.inputMediaNodeDataDisposable?.dispose()
|
||||||
self.inlineSearchResultsReadyDisposable?.dispose()
|
self.inlineSearchResultsReadyDisposable?.dispose()
|
||||||
|
self.loadMoreSearchResultsDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
@ -1636,6 +1639,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
isSelectionEnabled = false
|
isSelectionEnabled = false
|
||||||
} else if self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState != nil {
|
} else if self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState != nil {
|
||||||
isSelectionEnabled = false
|
isSelectionEnabled = false
|
||||||
|
} else if case .customChatContents = self.chatLocation {
|
||||||
|
isSelectionEnabled = false
|
||||||
}
|
}
|
||||||
self.historyNode.isSelectionGestureEnabled = isSelectionEnabled
|
self.historyNode.isSelectionGestureEnabled = isSelectionEnabled
|
||||||
|
|
||||||
@ -2693,6 +2698,51 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return foundLocalPeers
|
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: {},
|
environment: {},
|
||||||
|
@ -135,7 +135,7 @@ extension ChatControllerImpl {
|
|||||||
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted {
|
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted {
|
||||||
buttons = combineLatest(
|
buttons = combineLatest(
|
||||||
self.context.engine.messages.attachMenuBots(),
|
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
|
|> map { attachMenuBots, shortcutMessageList in
|
||||||
var buttons = availableButtons
|
var buttons = availableButtons
|
||||||
|
@ -237,7 +237,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
|||||||
if let user = peer as? TelegramUser, user.botInfo == nil {
|
if let user = peer as? TelegramUser, user.botInfo == nil {
|
||||||
context.account.viewTracker.keepQuickRepliesApproximatelyUpdated()
|
context.account.viewTracker.keepQuickRepliesApproximatelyUpdated()
|
||||||
|
|
||||||
shortcuts = context.engine.accountData.shortcutMessageList()
|
shortcuts = context.engine.accountData.shortcutMessageList(onlyRemote: true)
|
||||||
|> map { shortcutMessageList -> [ShortcutMessageList.Item] in
|
|> map { shortcutMessageList -> [ShortcutMessageList.Item] in
|
||||||
return shortcutMessageList.items.filter { item in
|
return shortcutMessageList.items.filter { item in
|
||||||
return item.shortcut.hasPrefix(normalizedQuery)
|
return item.shortcut.hasPrefix(normalizedQuery)
|
||||||
|
@ -66,7 +66,11 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable {
|
|||||||
case let .command(command):
|
case let .command(command):
|
||||||
return .command(command)
|
return .command(command)
|
||||||
case let .shortcut(shortcut):
|
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):
|
case let .shortcut(shortcut):
|
||||||
interfaceInteraction.sendShortcut(shortcut.id)
|
if let shortcutId = shortcut.id {
|
||||||
|
interfaceInteraction.sendShortcut(shortcutId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, openEditShortcuts: { [weak self] in
|
}, openEditShortcuts: { [weak self] in
|
||||||
guard let self, let interfaceInteraction = self.interfaceInteraction else {
|
guard let self, let interfaceInteraction = self.interfaceInteraction else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user