[WIP] Business

This commit is contained in:
Isaac 2024-02-23 23:12:28 +04:00
parent a603c138e9
commit 50339b3c4c
22 changed files with 411 additions and 67 deletions

View File

@ -94,11 +94,13 @@ public enum ChatListItemContent {
public var commandPrefix: String?
public var searchQuery: String?
public var messageCount: Int?
public var hideSeparator: Bool
public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?) {
public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?, hideSeparator: Bool) {
self.commandPrefix = commandPrefix
self.searchQuery = searchQuery
self.messageCount = messageCount
self.hideSeparator = hideSeparator
}
}
@ -1166,6 +1168,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
private var currentItemHeight: CGFloat?
let forwardedIconNode: ASImageNode
let textNode: TextNodeWithEntities
var trailingTextBadgeNode: TextNode?
var trailingTextBadgeBackground: UIImageView?
var dustNode: InvisibleInkDustNode?
let inputActivitiesNode: ChatListInputActivitiesNode
let dateNode: TextNode
@ -1437,6 +1441,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.textNode = TextNodeWithEntities()
self.textNode.textNode.isUserInteractionEnabled = false
self.textNode.textNode.displaysAsynchronously = true
self.textNode.textNode.layer.anchorPoint = CGPoint()
self.inputActivitiesNode = ChatListInputActivitiesNode()
self.inputActivitiesNode.isUserInteractionEnabled = false
@ -1823,6 +1828,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let makeTrailingTextBadgeLayout = TextNode.asyncLayout(self.trailingTextBadgeNode)
let titleLayout = TextNode.asyncLayout(self.titleNode)
let authorLayout = self.authorNode.asyncLayout()
let makeMeasureLayout = TextNode.asyncLayout(self.measureNode)
@ -2073,7 +2079,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
avatarDiameter = 40.0
avatarLeftInset = 18.0 + avatarDiameter
avatarLeftInset = 17.0 + avatarDiameter
} else {
if item.interaction.isInlineMode {
avatarLeftInset = 12.0
@ -2666,7 +2672,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
topIndex = peerData.messages.first?.index
}
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if let messageCount = customMessageListData.messageCount {
if let messageCount = customMessageListData.messageCount, customMessageListData.commandPrefix == nil {
dateText = "\(messageCount)"
} else {
dateText = " "
@ -2990,9 +2996,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let (authorLayout, authorApply) = authorLayout(item.context, rawContentWidth - badgeSize, item.presentationData.theme, effectiveAuthorTitle, forumThreads)
var textBottomRightCutout: CGFloat = 0.0
let trailingTextBadgeInsets = UIEdgeInsets(top: 2.0 - UIScreenPixel, left: 5.0, bottom: 2.0 - UIScreenPixel, right: 5.0)
var trailingTextBadgeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil, let messageCount = customMessageListData.messageCount, messageCount > 1 {
let trailingText: String
//TODO:localize
trailingText = "+\(messageCount - 1) MORE"
let trailingAttributedText = NSAttributedString(string: trailingText, font: Font.regular(12.0), textColor: theme.messageTextColor)
let (layout, apply) = makeTrailingTextBadgeLayout(TextNodeLayoutArguments(attributedString: trailingAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
trailingTextBadgeLayoutAndApply = (layout, apply)
textBottomRightCutout += layout.size.width + 4.0 + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right
}
var textCutout: TextNodeCutout?
if !textLeftCutout.isZero {
textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: nil)
if !textLeftCutout.isZero || !textBottomRightCutout.isZero {
textCutout = TextNodeCutout(topLeft: textLeftCutout.isZero ? nil : CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: textBottomRightCutout.isZero ? nil : CGSize(width: textBottomRightCutout, height: 10.0))
}
var textMaxWidth = rawContentWidth - badgeSize
@ -3552,6 +3572,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let _ = measureApply()
let _ = dateApply()
var currentTextSnapshotView: UIView?
if transition.isAnimated, let currentItem, currentItem.editing != item.editing, strongSelf.textNode.textNode.cachedLayout?.linesRects() != textLayout.linesRects() {
if let textSnapshotView = strongSelf.textNode.textNode.view.snapshotContentTree() {
textSnapshotView.layer.anchorPoint = CGPoint()
currentTextSnapshotView = textSnapshotView
strongSelf.textNode.textNode.view.superview?.insertSubview(textSnapshotView, aboveSubview: strongSelf.textNode.textNode.view)
textSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSnapshotView] _ in
textSnapshotView?.removeFromSuperview()
})
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
}
let _ = textApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.interaction.animationCache,
@ -3572,7 +3605,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var dateFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil {
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil, customMessageListData.commandPrefix == nil {
dateFrame.origin.x -= 10.0
let dateDisclosureIconView: UIImageView
@ -3818,6 +3851,60 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.authorNode.assignParentNode(parentNode: nil)
}
if let currentTextSnapshotView {
transition.updatePosition(layer: currentTextSnapshotView.layer, position: textNodeFrame.origin)
}
if let trailingTextBadgeLayoutAndApply {
let badgeSize = CGSize(width: trailingTextBadgeLayoutAndApply.0.size.width + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right, height: trailingTextBadgeLayoutAndApply.0.size.height + trailingTextBadgeInsets.top + trailingTextBadgeInsets.bottom - UIScreenPixel)
var badgeFrame: CGRect
if textLayout.numberOfLines > 1 {
badgeFrame = CGRect(origin: CGPoint(x: textLayout.trailingLineWidth, y: textNodeFrame.height - 3.0 - badgeSize.height), size: badgeSize)
} else {
let firstLineFrame = textLayout.linesRects().first ?? CGRect(origin: CGPoint(), size: textNodeFrame.size)
badgeFrame = CGRect(origin: CGPoint(x: 0.0, y: firstLineFrame.height + 5.0), size: badgeSize)
}
if badgeFrame.origin.x + badgeFrame.width >= textNodeFrame.width - 2.0 - 10.0 {
badgeFrame.origin.x = textNodeFrame.width - 2.0 - badgeFrame.width
}
let trailingTextBadgeBackground: UIImageView
if let current = strongSelf.trailingTextBadgeBackground {
trailingTextBadgeBackground = current
} else {
trailingTextBadgeBackground = UIImageView(image: tagBackgroundImage)
strongSelf.trailingTextBadgeBackground = trailingTextBadgeBackground
strongSelf.textNode.textNode.view.addSubview(trailingTextBadgeBackground)
}
trailingTextBadgeBackground.tintColor = theme.pinnedItemBackgroundColor.mixedWith(theme.unreadBadgeInactiveBackgroundColor, alpha: 0.1)
trailingTextBadgeBackground.frame = badgeFrame
let trailingTextBadgeFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + trailingTextBadgeInsets.left, y: badgeFrame.minY + trailingTextBadgeInsets.top), size: trailingTextBadgeLayoutAndApply.0.size)
let trailingTextBadgeNode = trailingTextBadgeLayoutAndApply.1()
if strongSelf.trailingTextBadgeNode !== trailingTextBadgeNode {
strongSelf.trailingTextBadgeNode?.removeFromSupernode()
strongSelf.trailingTextBadgeNode = trailingTextBadgeNode
strongSelf.textNode.textNode.addSubnode(trailingTextBadgeNode)
trailingTextBadgeNode.layer.anchorPoint = CGPoint()
}
trailingTextBadgeNode.frame = trailingTextBadgeFrame
} else {
if let trailingTextBadgeNode = strongSelf.trailingTextBadgeNode {
strongSelf.trailingTextBadgeNode = nil
trailingTextBadgeNode.removeFromSupernode()
}
if let trailingTextBadgeBackground = strongSelf.trailingTextBadgeBackground {
strongSelf.trailingTextBadgeBackground = nil
trailingTextBadgeBackground.removeFromSuperview()
}
}
if !itemTags.isEmpty {
let itemTagListFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.maxY - 12.0), size: CGSize(width: contentRect.width, height: 20.0))
@ -4127,7 +4214,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if customMessageListData.messageCount != nil {
if customMessageListData.hideSeparator {
strongSelf.separatorNode.isHidden = true
}
}

View File

@ -433,7 +433,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
},
requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging,
displayAsTopicList: peerEntry.displayAsTopicList,
tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
)),
editing: editing,
hasActiveRevealControls: hasActiveRevealControls,
@ -811,7 +811,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
},
requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging,
displayAsTopicList: peerEntry.displayAsTopicList,
tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
)),
editing: editing,
hasActiveRevealControls: hasActiveRevealControls,
@ -4206,7 +4206,11 @@ func hideChatListContacts(context: AccountContext) {
let _ = ApplicationSpecificNotice.setDisplayChatListContacts(accountManager: context.sharedContext.accountManager).startStandalone()
}
func chatListItemTags(accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] {
func chatListItemTags(location: ChatListControllerLocation, accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] {
if case .chatList = location {
} else {
return []
}
guard let chatListFilters, !chatListFilters.isEmpty else {
return []
}

View File

@ -1202,6 +1202,8 @@ public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bo
} else {
return false
}
} else if case .customChatContents = state.chatLocation {
return true
} else {
return false
}

View File

@ -816,6 +816,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false) {
self.currentMessage = message
var displayInfo = displayInfo
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
displayInfo = false
}
var canDelete: Bool
var canShare = !message.containsSecretMedia

View File

@ -285,6 +285,7 @@ private enum PreferencesKeyValues: Int32 {
case didCacheSavedMessageTagsPrefix = 34
case displaySavedChatsAsTopics = 35
case shortcutMessages = 37
case timezoneList = 38
}
public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey {
@ -474,6 +475,12 @@ public struct PreferencesKeys {
key.setInt32(0, value: PreferencesKeyValues.shortcutMessages.rawValue)
return key
}
public static func timezoneList() -> ValueBoxKey {
let key = ValueBoxKey(length: 4)
key.setInt32(0, value: PreferencesKeyValues.timezoneList.rawValue)
return key
}
}
private enum SharedDataKeyValues: Int32 {

View File

@ -185,5 +185,13 @@ public extension TelegramEngine {
public func sendMessageShortcut(peerId: EnginePeer.Id, id: Int32) {
let _ = _internal_sendMessageShortcut(account: self.account, peerId: peerId, id: id).startStandalone()
}
public func cachedTimeZoneList() -> Signal<TimeZoneList?, NoError> {
return _internal_cachedTimeZoneList(account: self.account)
}
public func keepCachedTimeZoneListUpdated() -> Signal<Never, NoError> {
return _internal_keepCachedTimeZoneListUpdated(account: self.account)
}
}
}

View File

@ -229,7 +229,7 @@ func _internal_shortcutMessageList(account: Account) -> Signal<ShortcutMessageLi
historyViewKeys[shortcut.id] = historyViewKey
keys.append(historyViewKey)
let summaryKey: PostboxViewKey = .historyTagSummaryView(tag: [], peerId: account.peerId, threadId: Int64(shortcut.id), namespace: Namespaces.Message.ScheduledCloud, customTag: nil)
let summaryKey: PostboxViewKey = .historyTagSummaryView(tag: [], peerId: account.peerId, threadId: Int64(shortcut.id), namespace: Namespaces.Message.QuickReplyCloud, customTag: nil)
summaryKeys[shortcut.id] = summaryKey
keys.append(summaryKey)
}

View File

@ -0,0 +1,106 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public final class TimeZoneList: Codable, Equatable {
public final class Item: Codable, Equatable {
public let id: String
public let title: String
public let utcOffset: Int32
public init(id: String, title: String, utcOffset: Int32) {
self.id = id
self.title = title
self.utcOffset = utcOffset
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs === rhs {
return true
}
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.utcOffset != rhs.utcOffset {
return false
}
return true
}
}
public let items: [Item]
public let hashValue: Int32
public init(items: [Item], hashValue: Int32) {
self.items = items
self.hashValue = hashValue
}
public static func ==(lhs: TimeZoneList, rhs: TimeZoneList) -> Bool {
if lhs === rhs {
return true
}
if lhs.items != rhs.items {
return false
}
if lhs.hashValue != rhs.hashValue {
return false
}
return true
}
}
func _internal_cachedTimeZoneList(account: Account) -> Signal<TimeZoneList?, NoError> {
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.timezoneList()]))
return account.postbox.combinedView(keys: [viewKey])
|> map { views -> TimeZoneList? in
guard let view = views.views[viewKey] as? PreferencesView else {
return nil
}
guard let value = view.values[PreferencesKeys.timezoneList()]?.get(TimeZoneList.self) else {
return nil
}
return value
}
}
func _internal_keepCachedTimeZoneListUpdated(account: Account) -> Signal<Never, NoError> {
let updateSignal = _internal_cachedTimeZoneList(account: account)
|> take(1)
|> mapToSignal { list -> Signal<Never, NoError> in
return account.network.request(Api.functions.help.getTimezonesList(hash: list?.hashValue ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.help.TimezonesList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
guard let result else {
return .complete()
}
return account.postbox.transaction { transaction in
switch result {
case let .timezonesList(timezones, hash):
var items: [TimeZoneList.Item] = []
for item in timezones {
switch item {
case let .timezone(id, name, utcOffset):
items.append(TimeZoneList.Item(id: id, title: name, utcOffset: utcOffset))
}
}
transaction.setPreferencesEntry(key: PreferencesKeys.timezoneList(), value: PreferencesEntry(TimeZoneList(items: items, hashValue: hash)))
case .timezonesListNotModified:
break
}
}
|> ignoreValues
}
}
return updateSignal
}

View File

@ -267,7 +267,9 @@ public final class ChatListHeaderComponent: Component {
}
func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: Transition) -> CGSize {
self.titleView.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)
let titleText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)
let titleTextUpdated = self.titleView.attributedText != titleText
self.titleView.attributedText = titleText
let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0))
self.accessibilityLabel = title
@ -287,7 +289,12 @@ public final class ChatListHeaderComponent: Component {
transition.setPosition(view: self.arrowView, position: arrowFrame.center)
transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size))
transition.setFrame(view: self.titleView, frame: CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize))
let titleFrame = CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)
if titleTextUpdated {
self.titleView.frame = titleFrame
} else {
transition.setFrame(view: self.titleView, frame: titleFrame)
}
return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height)
}
@ -479,7 +486,9 @@ public final class ChatListHeaderComponent: Component {
transition.setPosition(view: self.titleScaleContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setBounds(view: self.titleScaleContainer, bounds: CGRect(origin: self.titleScaleContainer.bounds.origin, size: size))
self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
let titleText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
let titleTextUpdated = self.titleTextView.attributedText != titleText
self.titleTextView.attributedText = titleText
let buttonSpacing: CGFloat = 8.0
@ -616,7 +625,11 @@ public final class ChatListHeaderComponent: Component {
let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height))
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleTextSize.width) / 2.0) + sideContentWidth, y: floor((size.height - titleTextSize.height) / 2.0)), size: titleTextSize)
transition.setFrame(view: self.titleTextView, frame: titleFrame)
if titleTextUpdated {
self.titleTextView.frame = titleFrame
} else {
transition.setFrame(view: self.titleTextView, frame: titleFrame)
}
if let titleComponent = content.titleComponent {
var titleContentTransition = transition

View File

@ -227,7 +227,12 @@ public final class PlainButtonComponent: Component {
}
let contentFrame = CGRect(origin: CGPoint(x: component.contentInsets.left + floor((size.width - component.contentInsets.left - component.contentInsets.right - contentSize.width) * 0.5), y: component.contentInsets.top + floor((size.height - component.contentInsets.top - component.contentInsets.bottom - contentSize.height) * 0.5)), size: contentSize)
contentTransition.setPosition(view: contentView, position: CGPoint(x: contentFrame.minX + contentFrame.width * contentView.layer.anchorPoint.x, y: contentFrame.minY + contentFrame.height * contentView.layer.anchorPoint.y))
let contentPosition = CGPoint(x: contentFrame.minX + contentFrame.width * contentView.layer.anchorPoint.x, y: contentFrame.minY + contentFrame.height * contentView.layer.anchorPoint.y)
if !component.animateContents && (abs(contentView.center.x - contentPosition.x) <= 2.0 && abs(contentView.center.y - contentPosition.y) <= 2.0){
contentView.center = contentPosition
} else {
contentTransition.setPosition(view: contentView, position: contentPosition)
}
if component.animateContents {
contentTransition.setBounds(view: contentView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))

View File

@ -238,7 +238,8 @@ final class GreetingMessageListItemComponent: Component {
customMessageListData: ChatListItemContent.CustomMessageListData(
commandPrefix: nil,
searchQuery: nil,
messageCount: component.count
messageCount: component.count,
hideSeparator: true
)
)),
editing: false,

View File

@ -109,7 +109,7 @@ final class QuickReplyEmptyStateComponent: Component {
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "WriteEmoji"),
loop: false
loop: true
)),
environment: {},
containerSize: CGSize(width: 120.0, height: 120.0)

View File

@ -250,7 +250,8 @@ final class QuickReplySetupScreenComponent: Component {
customMessageListData: ChatListItemContent.CustomMessageListData(
commandPrefix: "/\(item.shortcut)",
searchQuery: nil,
messageCount: nil
messageCount: item.totalCount,
hideSeparator: false
)
)),
editing: isEditing,
@ -744,13 +745,14 @@ final class QuickReplySetupScreenComponent: Component {
tabsNodeIsSearch: false,
accessoryPanelContainer: nil,
accessoryPanelContainerHeight: 0.0,
activateSearch: { [weak self] searchContentNode in
activateSearch: { [weak self] _ in
guard let self else {
return
}
self.isSearchDisplayControllerActive = true
self.state?.updated(transition: .spring(duration: 0.4))
let _ = self
//self.isSearchDisplayControllerActive = true
//self.state?.updated(transition: .spring(duration: 0.4))
},
openStatusSetup: { _ in
},

View File

@ -82,11 +82,14 @@ final class BusinessHoursSetupScreenComponent: Component {
}
var timezoneId: String
var days: [Day]
private(set) var days: [Day]
private(set) var intersectingDays = Set<Int>()
init(timezoneId: String, days: [Day]) {
self.timezoneId = timezoneId
self.days = days
self.validate()
}
init(businessHours: TelegramBusinessHours) {
@ -107,6 +110,19 @@ final class BusinessHoursSetupScreenComponent: Component {
})
}
}
self.validate()
}
mutating func validate() {
self.intersectingDays.removeAll()
}
mutating func update(days: [Day]) {
self.days = days
self.validate()
}
func asBusinessHours() throws -> TelegramBusinessHours {
@ -165,6 +181,10 @@ final class BusinessHoursSetupScreenComponent: Component {
private var showHours: Bool = false
private var daysState = DaysState(timezoneId: "", days: [])
private var timeZoneList: TimeZoneList?
private var timezonesDisposable: Disposable?
private var keepTimezonesUpdatedDisposable: Disposable?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -191,6 +211,8 @@ final class BusinessHoursSetupScreenComponent: Component {
}
deinit {
self.timezonesDisposable?.dispose()
self.keepTimezonesUpdatedDisposable?.dispose()
}
func scrollToTop() {
@ -210,6 +232,21 @@ final class BusinessHoursSetupScreenComponent: Component {
} catch let error {
let _ = error
//TODO:localize
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Business hours are intersecting. Reset?", actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {
}),
TextAlertAction(type: .defaultAction, title: "Reset", action: { [weak self] in
guard let self else {
return
}
let _ = self
complete()
})
]), in: .window(.root))
return false
}
} else {
@ -271,10 +308,20 @@ final class BusinessHoursSetupScreenComponent: Component {
} else {
self.showHours = false
self.daysState.timezoneId = TimeZone.current.identifier
self.daysState.days = (0 ..< 7).map { _ in
self.daysState.update(days: (0 ..< 7).map { _ in
return Day(ranges: [])
}
})
}
self.timezonesDisposable = (component.context.engine.accountData.cachedTimeZoneList()
|> deliverOnMainQueue).start(next: { [weak self] timeZoneList in
guard let self else {
return
}
self.timeZoneList = timeZoneList
self.state?.updated(transition: .immediate)
})
self.keepTimezonesUpdatedDisposable = component.context.engine.accountData.keepCachedTimeZoneListUpdated().startStrict()
}
let environment = environment[EnvironmentType.self].value
@ -508,11 +555,13 @@ final class BusinessHoursSetupScreenComponent: Component {
return
}
if dayIndex < self.daysState.days.count {
if self.daysState.days[dayIndex].ranges == nil {
self.daysState.days[dayIndex].ranges = []
var days = self.daysState.days
if days[dayIndex].ranges == nil {
days[dayIndex].ranges = []
} else {
self.daysState.days[dayIndex].ranges = nil
days[dayIndex].ranges = nil
}
self.daysState.update(days: days)
}
self.state?.updated(transition: .immediate)
})),
@ -529,7 +578,9 @@ final class BusinessHoursSetupScreenComponent: Component {
return
}
if self.daysState.days[dayIndex] != day {
self.daysState.days[dayIndex] = day
var days = self.daysState.days
days[dayIndex] = day
self.daysState.update(days: days)
self.state?.updated(transition: .immediate)
}
}
@ -569,6 +620,18 @@ final class BusinessHoursSetupScreenComponent: Component {
daysContentHeight += daysSectionSize.height
daysContentHeight += sectionSpacing
let timezoneValueText: String
if let timeZoneList = self.timeZoneList {
if let item = timeZoneList.items.first(where: { $0.id == self.daysState.timezoneId }) {
timezoneValueText = item.title
} else {
timezoneValueText = TimeZone(identifier: self.daysState.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? " "
}
} else {
//TODO:localize
timezoneValueText = "Loading..."
}
let timezoneSectionSize = self.timezoneSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
@ -588,7 +651,7 @@ final class BusinessHoursSetupScreenComponent: Component {
)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: TimeZone(identifier: self.daysState.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? self.daysState.timezoneId,
string: timezoneValueText,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemSecondaryTextColor
)),

View File

@ -58,8 +58,7 @@ private func preparedLanguageListSearchContainerTransition(presentationData: Pre
}
private final class TimezoneListSearchContainerNode: SearchDisplayControllerContentNode {
private let timezoneData: TimezoneData
private let timeZoneList: TimeZoneList
private let dimNode: ASDisplayNode
private let listNode: ListView
@ -78,8 +77,8 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont
return true
}
init(context: AccountContext, timezoneData: TimezoneData, action: @escaping (String) -> Void) {
self.timezoneData = timezoneData
init(context: AccountContext, timeZoneList: TimeZoneList, action: @escaping (String) -> Void) {
self.timeZoneList = timeZoneList
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
@ -102,16 +101,18 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont
self.addSubnode(self.dimNode)
self.addSubnode(self.listNode)
let querySplitCharacterSet: CharacterSet = CharacterSet(charactersIn: " /.+")
let foundItems = self.searchQuery.get()
|> mapToSignal { query -> Signal<[TimezoneData.Item]?, NoError> in
|> mapToSignal { query -> Signal<[TimeZoneList.Item]?, NoError> in
if let query, !query.isEmpty {
let query = query.lowercased()
return .single(timezoneData.items.filter { item in
return .single(timeZoneList.items.filter { item in
if item.id.lowercased().hasPrefix(query) {
return true
}
if item.title.lowercased().split(separator: " ").contains(where: { $0.hasPrefix(query) }) {
if item.title.lowercased().components(separatedBy: querySplitCharacterSet).contains(where: { $0.hasPrefix(query) }) {
return true
}
@ -132,7 +133,7 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont
for item in items {
entries.append(TimezoneListEntry(
id: item.id,
offset: item.offset,
offset: Int(item.utcOffset),
title: item.title
))
}
@ -301,7 +302,7 @@ final class TimezoneSelectionScreenNode: ViewControllerTracingNode {
private let requestDeactivateSearch: () -> Void
private let present: (ViewController, Any?) -> Void
private let push: (ViewController) -> Void
private let timezoneData: TimezoneData
private var timeZoneList: TimeZoneList?
private var didSetReady = false
let _ready = ValuePromise<Bool>()
@ -331,28 +332,32 @@ final class TimezoneSelectionScreenNode: ViewControllerTracingNode {
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
let timezoneData = TimezoneData()
self.timezoneData = timezoneData
super.init()
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.listNode)
let previousEntriesHolder = Atomic<([TimezoneListEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.listDisposable = (self.presentationDataValue.get()
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
self.listDisposable = (combineLatest(queue: .mainQueue(),
self.presentationDataValue.get(),
context.engine.accountData.cachedTimeZoneList()
)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, timeZoneList in
guard let strongSelf = self else {
return
}
strongSelf.timeZoneList = timeZoneList
var entries: [TimezoneListEntry] = []
for item in timezoneData.items {
entries.append(TimezoneListEntry(
id: item.id,
offset: item.offset,
title: item.title
))
if let timeZoneList {
for item in timeZoneList.items {
entries.append(TimezoneListEntry(
id: item.id,
offset: Int(item.utcOffset),
title: item.title
))
}
}
entries.sort()
@ -447,8 +452,11 @@ final class TimezoneSelectionScreenNode: ViewControllerTracingNode {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else {
return
}
guard let timeZoneList = self.timeZoneList else {
return
}
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: TimezoneListSearchContainerNode(context: self.context, timezoneData: self.timezoneData, action: self.action), inline: true, cancel: { [weak self] in
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: TimezoneListSearchContainerNode(context: self.context, timeZoneList: timeZoneList, action: self.action), inline: true, cancel: { [weak self] in
self?.requestDeactivateSearch()
})

View File

@ -104,6 +104,9 @@ extension ChatControllerImpl {
if case let .peer(peerId) = self.chatLocation, messageLocation.peerId == peerId, !isPinnedMessages, !isScheduledMessages {
forceInCurrentChat = true
}
if case .customChatContents = self.chatLocation {
forceInCurrentChat = true
}
if isPinnedMessages, let messageId = messageLocation.messageId {
let _ = (combineLatest(
@ -205,6 +208,7 @@ extension ChatControllerImpl {
if case let .id(_, params) = messageLocation {
quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) }
}
self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition)
if delayCompletion {

View File

@ -1883,7 +1883,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
overlayNavigationBar.updateLayout(size: barFrame.size, transition: transition)
}
var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left)
var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top + 6.0, right: containerInsets.left)
let listScrollIndicatorInsets = UIEdgeInsets(top: containerInsets.bottom + inputPanelsHeight, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left)
var childContentInsets: UIEdgeInsets = containerInsets

View File

@ -546,12 +546,12 @@ func chatHistoryEntriesForView(
let message = Message(
stableId: UInt32.max - 1001 - UInt32(i),
stableVersion: 0,
id: MessageId(peerId: context.account.peerId, namespace: Namespaces.Message.Local, id: 123 - Int32(i)),
id: MessageId(peerId: context.account.peerId, namespace: Namespaces.Message.Local, id: Int32.max - 100 - Int32(i)),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: Int32(i),
timestamp: -Int32(i),
flags: [.Incoming],
tags: [],
globalTags: [],

View File

@ -1283,10 +1283,15 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
return true
})
|> mapToSignal { _ in
|> mapToSignal { location, _ -> Signal<((MessageHistoryView, ViewUpdateType), ChatHistoryLocationInput?), NoError> in
return historyView
|> map { historyView in
return (historyView, location)
}
}
|> map { view, update in
|> map { viewAndUpdate, location in
let (view, update) = viewAndUpdate
let version = currentViewVersion.modify({ value in
if let value = value {
return value + 1
@ -1295,11 +1300,21 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
})!
var scrollPositionValue: ChatHistoryViewScrollPosition?
if let location {
switch location.content {
case let .Scroll(subject, _, _, scrollPosition, animated, highlight):
scrollPositionValue = .index(subject: subject, position: scrollPosition, directionHint: .Up, animated: animated, highlight: highlight, displayLink: false)
default:
break
}
}
return (
ChatHistoryViewUpdate.HistoryView(
view: view,
type: .Generic(type: update),
scrollPosition: nil,
scrollPosition: scrollPositionValue,
flashIndicators: false,
originalScrollPosition: nil,
initialData: ChatHistoryCombinedInitialData(
@ -1309,10 +1324,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
cachedDataMessages: nil,
readStateData: nil
),
id: 0
id: location?.id ?? 0
),
version,
nil,
location,
nil
)
}

View File

@ -76,12 +76,21 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
replyPanelNode.interfaceInteraction = interfaceInteraction
replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return replyPanelNode
} else if let peerId = chatPresentationInterfaceState.chatLocation.peerId {
let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: peerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer)
panelNode.interfaceInteraction = interfaceInteraction
return panelNode
} else {
return nil
var chatPeerId: EnginePeer.Id?
if let peerId = chatPresentationInterfaceState.chatLocation.peerId {
chatPeerId = peerId
} else if case .customChatContents = chatPresentationInterfaceState.chatLocation {
chatPeerId = context.account.peerId
}
if let chatPeerId {
let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: chatPeerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer)
panelNode.interfaceInteraction = interfaceInteraction
return panelNode
} else {
return nil
}
}
} else {
return nil

View File

@ -271,6 +271,10 @@ private func canViewReadStats(message: Message, participantCount: Int?, isMessag
}
func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, accountPeerId: PeerId) -> Bool {
if case .customChatContents = chatPresentationInterfaceState.chatLocation {
return true
}
guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else {
return false
}
@ -338,7 +342,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
case .replyThread:
canReply = true
case .customChatContents:
canReply = false
canReply = true
}
return canReply
}

View File

@ -220,7 +220,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable {
customMessageListData: ChatListItemContent.CustomMessageListData(
commandPrefix: "/\(shortcut.shortcut)",
searchQuery: command.searchQuery.flatMap { "/\($0)"},
messageCount: nil
messageCount: shortcut.totalCount,
hideSeparator: false
)
)),
editing: false,