diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 6bf25bd809..818a3f3439 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7090,6 +7090,7 @@ Sorry for the inconvenience."; "Time.HoursAgo_many" = "%@ hours ago"; "Time.HoursAgo_0" = "%@ hours ago"; "Time.AtDate" = "%@"; +"Time.AtPreciseDate" = "%@ at %@"; "Stickers.ShowMore" = "Show More"; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index d005b452a1..ccc3d5bb4e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2051,7 +2051,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - if let orderedStorySubscriptions = self.orderedStorySubscriptions, !orderedStorySubscriptions.items.isEmpty { + if case .chatList(groupId: .root) = self.location, let orderedStorySubscriptions = self.orderedStorySubscriptions, !orderedStorySubscriptions.items.isEmpty { let _ = (ApplicationSpecificNotice.displayChatListStoriesTooltip(accountManager: self.context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] didDisplay in guard let self else { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index eae7f41900..4f4cb6ec88 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -434,7 +434,11 @@ private final class ChatListContainerItemNode: ASDisplayNode { if case .forum = location { subject = .forum(hasGeneral: hasOnlyGeneralThread) } else { - subject = .chats(hasArchive: hasOnlyArchive) + if case .chatList(groupId: .archive) = location { + subject = .archive + } else { + subject = .chats(hasArchive: hasOnlyArchive) + } } } diff --git a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift index f4b8cbd48f..200b8d985d 100644 --- a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift +++ b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift @@ -13,6 +13,7 @@ import AccountContext final class ChatListEmptyNode: ASDisplayNode { enum Subject { case chats(hasArchive: Bool) + case archive case filter(showEdit: Bool) case forum(hasGeneral: Bool) } @@ -132,11 +133,14 @@ final class ChatListEmptyNode: ASDisplayNode { func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { let text: String var descriptionText = "" - let buttonText: String + let buttonText: String? switch self.subject { case let .chats(hasArchive): text = hasArchive ? strings.ChatList_EmptyChatListWithArchive : strings.ChatList_EmptyChatList buttonText = strings.ChatList_EmptyChatListNewMessage + case .archive: + text = strings.ChatList_EmptyChatList + buttonText = nil case .filter: text = strings.ChatList_EmptyChatListFilterTitle descriptionText = strings.ChatList_EmptyChatListFilterText @@ -152,7 +156,12 @@ final class ChatListEmptyNode: ASDisplayNode { self.textNode.attributedText = string self.descriptionNode.attributedText = descriptionString - self.buttonNode.title = buttonText + if let buttonText { + self.buttonNode.title = buttonText + self.buttonNode.isHidden = false + } else { + self.buttonNode.isHidden = true + } self.activityIndicator.type = .custom(theme.list.itemAccentColor, 22.0, 1.0, false) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 16140b6b2e..0a155daf89 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -569,6 +569,16 @@ struct ChatListContactPeer { } func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer], accountPeerId: EnginePeer.Id) -> (entries: [ChatListNodeEntry], loading: Bool) { + var groupItems = view.groupItems + if state.archiveStoryState != nil && groupItems.isEmpty { + groupItems.append(EngineChatList.GroupItem( + id: .archive, + topMessage: nil, + items: [], + unreadCount: 0 + )) + } + var result: [ChatListNodeEntry] = [] if !view.hasEarlier { @@ -588,7 +598,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, if !view.hasLater, case .chatList = mode { var groupEntryCount = 0 - for _ in view.groupItems { + for _ in groupItems { groupEntryCount += 1 } pinnedIndexOffset += UInt16(groupEntryCount) @@ -831,7 +841,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, } if !view.hasLater, case .chatList = mode { - for groupReference in view.groupItems { + for groupReference in groupItems { let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: EnginePeer.Id(0), namespace: 0, id: 0), timestamp: 1) var mappedStoryState: ChatListNodeState.StoryState? if let archiveStoryState = state.archiveStoryState { diff --git a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift index 5627ab6f09..f69fc0775a 100644 --- a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift @@ -362,7 +362,7 @@ public func stringForRelativeLiveLocationUpdateTimestamp(strings: PresentationSt } } -public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String { +public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String { let difference = timestamp - relativeTimestamp if difference < 60 { return strings.Time_JustNow @@ -392,6 +392,8 @@ public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dat day = .yesterday } return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min).string + } else if preciseTime { + return strings.Time_AtPreciseDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat), stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)).string } else { return strings.Time_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).string } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift index 93883488f1..ed343a9dfc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift @@ -8,15 +8,22 @@ import TelegramStringFormatting import MultilineTextComponent final class StoryAuthorInfoComponent: Component { + struct Counters: Equatable { + var position: Int + var totalCount: Int + } + let context: AccountContext let peer: EnginePeer? let timestamp: Int32 + let counters: Counters? let isEdited: Bool - init(context: AccountContext, peer: EnginePeer?, timestamp: Int32, isEdited: Bool) { + init(context: AccountContext, peer: EnginePeer?, timestamp: Int32, counters: Counters?, isEdited: Bool) { self.context = context self.peer = peer self.timestamp = timestamp + self.counters = counters self.isEdited = isEdited } @@ -30,6 +37,9 @@ final class StoryAuthorInfoComponent: Component { if lhs.timestamp != rhs.timestamp { return false } + if lhs.counters != rhs.counters { + return false + } if lhs.isEdited != rhs.isEdited { return false } @@ -39,6 +49,7 @@ final class StoryAuthorInfoComponent: Component { final class View: UIView { private let title = ComponentView() private let subtitle = ComponentView() + private var counterLabel: ComponentView? private var component: StoryAuthorInfoComponent? private weak var state: EmptyComponentState? @@ -71,7 +82,7 @@ final class StoryAuthorInfoComponent: Component { } let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - var subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: component.timestamp, relativeTo: timestamp) + var subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, preciseTime: true, relativeTimestamp: component.timestamp, relativeTo: timestamp) if component.isEdited { subtitle.append(" • ") @@ -117,6 +128,36 @@ final class StoryAuthorInfoComponent: Component { } transition.setFrame(view: subtitleView, frame: subtitleFrame) } + + let countersSpacing: CGFloat = 5.0 + if let counters = component.counters { + let counterLabel: ComponentView + if let current = self.counterLabel { + counterLabel = current + } else { + counterLabel = ComponentView() + self.counterLabel = counterLabel + } + let counterSize = counterLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "\(counters.position + 1)/\(counters.totalCount)", font: Font.regular(11.0), textColor: UIColor(white: 1.0, alpha: 0.43))), + truncationType: .end, + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: max(1.0, availableSize.width - titleSize.width - countersSpacing), height: 100.0) + ) + if let counterLabelView = counterLabel.view { + if counterLabelView.superview == nil { + self.addSubview(counterLabelView) + } + counterLabelView.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + countersSpacing, y: titleFrame.minY + 1.0 + floorToScreenPixels((titleFrame.height - counterSize.height) * 0.5)), size: counterSize) + } + } else if let counterLabel = self.counterLabel { + self.counterLabel = nil + counterLabel.view?.removeFromSuperview() + } return size } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index b0483abbb1..fe271baf37 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -269,6 +269,7 @@ public final class StoryContentContextImpl: StoryContentContext { let allItems = mappedItems.map { item in return StoryContentItem( position: nil, + dayCounters: nil, peerId: peer.id, storyItem: item, entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles) @@ -281,6 +282,7 @@ public final class StoryContentContextImpl: StoryContentContext { additionalPeerData: additionalPeerData, item: StoryContentItem( position: mappedFocusedIndex ?? focusedIndex, + dayCounters: nil, peerId: peer.id, storyItem: mappedItem, entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles) @@ -1011,6 +1013,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { let mainItem = StoryContentItem( position: 0, + dayCounters: nil, peerId: peer.id, storyItem: mappedItem, entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles) @@ -1153,20 +1156,58 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } } + struct DayIndex: Hashable { + var year: Int32 + var day: Int32 + + init(timestamp: Int32) { + var time: time_t = time_t(timestamp) + var timeinfo: tm = tm() + localtime_r(&time, &timeinfo) + + self.year = timeinfo.tm_year + self.day = timeinfo.tm_yday + } + } + let stateValue: StoryContentContextState if let focusedIndex = focusedIndex { let item = state.items[focusedIndex] self.focusedId = item.id var allItems: [StoryContentItem] = [] + + var dayCounts: [DayIndex: Int] = [:] + var itemDayIndices: [Int32: (Int, DayIndex)] = [:] + for i in 0 ..< state.items.count { let stateItem = state.items[i] allItems.append(StoryContentItem( position: i, + dayCounters: nil, peerId: peer.id, storyItem: stateItem, entityFiles: extractItemEntityFiles(item: stateItem, allEntityFiles: state.allEntityFiles) )) + + let day = DayIndex(timestamp: stateItem.timestamp) + let dayCount: Int + if let current = dayCounts[day] { + dayCount = current + 1 + dayCounts[day] = dayCount + } else { + dayCount = 1 + dayCounts[day] = dayCount + } + itemDayIndices[stateItem.id] = (dayCount - 1, day) + } + + var dayCounters: StoryContentItem.DayCounters? + if let (offset, day) = itemDayIndices[item.id], let dayCount = dayCounts[day] { + dayCounters = StoryContentItem.DayCounters( + position: offset, + totalCount: dayCount + ) } stateValue = StoryContentContextState( @@ -1175,6 +1216,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { additionalPeerData: additionalPeerData, item: StoryContentItem( position: focusedIndex, + dayCounters: dayCounters, peerId: peer.id, storyItem: item, entityFiles: extractItemEntityFiles(item: item, allEntityFiles: state.allEntityFiles) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index b5447bb87e..fa15a54460 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -19,6 +19,16 @@ public final class StoryContentItem: Equatable { case off } + public struct DayCounters: Equatable { + public var position: Int + public var totalCount: Int + + public init(position: Int, totalCount: Int) { + self.position = position + self.totalCount = totalCount + } + } + public final class SharedState { public init() { } @@ -78,17 +88,20 @@ public final class StoryContentItem: Equatable { } public let position: Int? + public let dayCounters: DayCounters? public let peerId: EnginePeer.Id? public let storyItem: EngineStoryItem public let entityFiles: [EngineMedia.Id: TelegramMediaFile] public init( position: Int?, + dayCounters: DayCounters?, peerId: EnginePeer.Id?, storyItem: EngineStoryItem, entityFiles: [EngineMedia.Id: TelegramMediaFile] ) { self.position = position + self.dayCounters = dayCounters self.peerId = peerId self.storyItem = storyItem self.entityFiles = entityFiles @@ -98,6 +111,9 @@ public final class StoryContentItem: Equatable { if lhs.position != rhs.position { return false } + if lhs.dayCounters != rhs.dayCounters { + return false + } if lhs.peerId != rhs.peerId { return false } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 518ccec45b..c4d5620efa 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2445,8 +2445,22 @@ public final class StoryItemSetContainerComponent: Component { } var currentCenterInfoItem: InfoItem? - if focusedItem != nil { - let centerInfoComponent = AnyComponent(StoryAuthorInfoComponent(context: component.context, peer: component.slice.peer, timestamp: component.slice.item.storyItem.timestamp, isEdited: component.slice.item.storyItem.isEdited)) + if let focusedItem { + var counters: StoryAuthorInfoComponent.Counters? + if focusedItem.dayCounters != nil, let position = focusedItem.position { + counters = StoryAuthorInfoComponent.Counters( + position: position, + totalCount: component.slice.totalCount + ) + } + + let centerInfoComponent = AnyComponent(StoryAuthorInfoComponent( + context: component.context, + peer: component.slice.peer, + timestamp: component.slice.item.storyItem.timestamp, + counters: counters, + isEdited: component.slice.item.storyItem.isEdited + )) if let centerInfoItem = self.centerInfoItem, centerInfoItem.component == centerInfoComponent { currentCenterInfoItem = centerInfoItem } else { @@ -2987,11 +3001,18 @@ public final class StoryItemSetContainerComponent: Component { let navigationStripSideInset: CGFloat = 8.0 let navigationStripTopInset: CGFloat = 8.0 + var index = max(0, min(index, component.slice.totalCount - 1)) + var count = component.slice.totalCount + if let dayCounters = focusedItem.dayCounters { + index = dayCounters.position + count = dayCounters.totalCount + } + let _ = self.navigationStrip.update( transition: transition, component: AnyComponent(MediaNavigationStripComponent( - index: max(0, min(index, component.slice.totalCount - 1)), - count: component.slice.totalCount + index: index, + count: count )), environment: { MediaNavigationStripComponent.EnvironmentType(