Compare commits

...

31 Commits

Author SHA1 Message Date
Diogo 3298caef28 quoted message alert 2024-11-02 00:08:47 +00:00
Diogo ec4bf4c1d9 new apis and types 2024-11-01 23:48:54 +00:00
Diogo ec95b14234 revert unread 2024-11-01 23:31:10 +00:00
Diogo 7722803586 Merge branch 'master' into dc/ios-chat-screen-infinite-scroll 2024-11-01 22:32:51 +00:00
Diogo 96800ab0b4 revert update stas 2024-10-23 10:17:25 +01:00
Diogo a360da4828 made open first unread more stable 2024-10-22 15:25:49 +01:00
Diogo bafaf0f0ee better request management 2024-10-22 15:13:34 +01:00
Diogo 0a9917de03 bad stash 2024-10-22 11:47:01 +01:00
Diogo 7d9e0dd041 trim and split sections 2024-10-22 11:45:11 +01:00
Diogo 37ba244e04 section poping 2024-10-21 16:57:44 +01:00
Diogo d21dc77566 improvements and more section logic 2024-10-21 12:26:08 +01:00
Diogo bd5a51c366 shared section model 2024-10-19 14:00:00 +01:00
Diogo 8c31bd5dd1 update min unread item in memory when app is running 2024-10-19 11:03:44 +01:00
Diogo 5a78f11c8b jump to first unread 2024-10-19 09:36:03 +01:00
Diogo 3044231eb6 section to drop fix 2024-10-19 09:05:46 +01:00
Diogo 192500341e merge master 2024-10-18 21:35:43 +01:00
Diogo 57a01fdce8 fix new items 2024-10-16 21:46:30 +01:00
Diogo 061dccbb98 comment 2024-10-16 16:22:50 +01:00
Diogo 4fe2b80705 simplify jump to quote 2024-10-16 16:20:01 +01:00
Diogo 1e419b8790 refactor 2024-10-16 15:39:41 +01:00
Diogo 8467d5ff17 reset sections 2024-10-16 10:04:40 +01:00
Diogo 04f9d53964 scroll all the way up and down 2024-10-15 23:52:25 +01:00
Diogo ee1d9fff36 track direction 2024-10-15 21:55:46 +01:00
Diogo 19a34b630c Merge branch 'master' into dc/ios-chat-screen-infinite-scroll 2024-10-15 16:32:06 +01:00
Diogo 7adfe631bf scroll to oldest working 2024-10-15 16:31:45 +01:00
Diogo a7844b3f7b getListState taking section into account 2024-10-15 11:28:06 +01:00
Diogo 9ee1d8d0b9 jump to quote jumping 2024-10-14 23:07:24 +01:00
Diogo 095cc286de Merge branch 'master' into dc/ios-chat-screen-infinite-scroll 2024-10-14 09:34:13 +01:00
Evgeny Poberezkin 1cb3c25478 docs: correction 2024-10-13 22:01:14 +01:00
Diogo c6b1413b78 simplify and pass direction 2024-10-08 15:26:55 +01:00
Diogo d4e460ac8c track scroll direction 2024-10-08 12:45:27 +01:00
7 changed files with 457 additions and 68 deletions
+2 -2
View File
@@ -322,13 +322,13 @@ let loadItemsPerPage = 50
func apiGetChat(type: ChatType, id: Int64, search: String = "") async throws -> Chat {
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search))
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
if case let .apiChat(_, chat, _) = r { return Chat.init(chat) }
throw r
}
func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] {
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search))
if case let .apiChat(_, chat) = r { return chat.chatItems }
if case let .apiChat(_, chat, _) = r { return chat.chatItems }
throw r
}
@@ -13,6 +13,7 @@ struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var scrollModel: ReverseListScrollModel
@EnvironmentObject var sectionModel: ReverseListSectionModel
@ObservedObject var chat: Chat
var chatItem: ChatItem
var preview: UIImage?
@@ -46,12 +47,43 @@ struct FramedItemView: View {
}
if let qi = chatItem.quotedItem {
let notFoundItemTitle = NSLocalizedString("Message no longer available", comment: "alert title")
let notFoundItemMessage = NSLocalizedString("The quoted message you are trying to view has been deleted.", comment: "alert message")
ciQuoteView(qi)
.onTapGesture {
if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) {
withAnimation {
scrollModel.scrollToItem(id: ci.id)
if let itemId = qi.itemId {
Task {
if let reversedPage = await loadItemsAround(chat.chatInfo, itemId) {
await MainActor.run {
let im = ItemsModel.shared
let reversedPageToAppend = self.sectionModel.handleSectionInsertion(
candidateSection: .destination,
reversedPage: reversedPage,
allItems: im.reversedChatItems
)
im.reversedChatItems.append(contentsOf: reversedPageToAppend)
withAnimation {
scrollModel.scrollToItem(id: itemId)
}
}
} else {
AlertManager.shared.showAlert(
Alert(
title: Text(notFoundItemTitle),
message: Text(notFoundItemMessage)
)
)
}
}
} else {
AlertManager.shared.showAlert(
Alert(
title: Text(notFoundItemTitle),
message: Text(notFoundItemMessage)
)
)
}
}
} else if let itemForwarded = chatItem.meta.itemForwarded {
+107 -31
View File
@@ -23,6 +23,7 @@ struct ChatView: View {
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
@StateObject private var scrollModel = ReverseListScrollModel()
@StateObject private var sectionModel = ReverseListSectionModel()
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@@ -31,7 +32,6 @@ struct ChatView: View {
@State private var customUserProfile: Profile?
@State private var connectionCode: String?
@State private var loadingItems = false
@State private var firstPage = false
@State private var revealedChatItem: ChatItem?
@State private var searchMode = false
@State private var searchText: String = ""
@@ -168,7 +168,6 @@ struct ChatView: View {
.onChange(of: chatModel.chatId) { cId in
showChatInfoSheet = false
selectedChatItems = nil
scrollModel.scrollToBottom()
stopAudioPlayer()
if let cId {
if let c = chatModel.getChat(cId) {
@@ -187,10 +186,11 @@ struct ChatView: View {
if !isLoading,
im.reversedChatItems.count <= loadItemsPerPage,
filtered(im.reversedChatItems).count < 10 {
loadChatItems(chat.chatInfo)
loadChatItems(chat.chatInfo, .toOldest, .bottom, nil)
}
}
.environmentObject(scrollModel)
.environmentObject(sectionModel)
.onDisappear {
VideoPlayerView.players.removeAll()
stopAudioPlayer()
@@ -363,6 +363,34 @@ struct ChatView: View {
}
}
ChatView.FloatingButtonModel.shared.totalUnread = chat.chatStats.unreadCount
sectionModel.resetSections(items: im.reversedChatItems)
let minUnreadItemId = chat.chatStats.minUnreadItemId
if minUnreadItemId > 0 {
if im.reversedChatItems.contains(where: { $0.id == minUnreadItemId }) {
withAnimation {
scrollModel.scrollToItem(id: minUnreadItemId, position: .middle)
}
} else {
Task {
if let reversedPage = await loadItemsAround(chat.chatInfo, minUnreadItemId) {
await MainActor.run {
let reversedPageToAppend = self.sectionModel.handleSectionInsertion(
candidateSection: .current,
reversedPage: reversedPage,
allItems: im.reversedChatItems
)
im.reversedChatItems.append(contentsOf: reversedPageToAppend)
withAnimation {
scrollModel.scrollToItem(id: minUnreadItemId, position: .middle, animated: false)
}
}
}
}
}
} else {
scrollModel.scrollToBottom()
}
}
private func searchToolbar() -> some View {
@@ -400,25 +428,11 @@ struct ChatView: View {
ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil
}
private func filtered(_ reversedChatItems: Array<ChatItem>) -> Array<ChatItem> {
reversedChatItems
.enumerated()
.filter { (index, chatItem) in
if let mergeCategory = chatItem.mergeCategory, index > 0 {
mergeCategory != reversedChatItems[index - 1].mergeCategory
} else {
true
}
}
.map { $0.element }
}
private func chatItemsList() -> some View {
let cInfo = chat.chatInfo
let mergedItems = filtered(im.reversedChatItems)
return GeometryReader { g in
ReverseList(items: mergedItems, scrollState: $scrollModel.state) { ci in
ReverseList(items: mergedItems, scrollState: $scrollModel.state, sectionModel: sectionModel) { ci in
let voiceNoFrame = voiceWithoutFrame(ci)
let maxWidth = cInfo.chatType == .group
? voiceNoFrame
@@ -438,18 +452,22 @@ struct ChatView: View {
forwardedChatItems: $forwardedChatItems
)
.id(ci.id) // Required to trigger `onAppear` on iOS15
} loadPage: {
loadChatItems(cInfo)
} loadPage: { (direction, section, chatItemId) in
loadChatItems(cInfo, direction, section, chatItemId)
}
.opacity(ItemsModel.shared.isLoading ? 0 : 1)
.padding(.vertical, -InvertedTableView.inset)
.onTapGesture { hideKeyboard() }
.onChange(of: searchText) { _ in
Task { await loadChat(chat: chat, search: searchText) }
Task {
await loadChat(chat: chat, search: searchText)
sectionModel.resetSections(items: im.reversedChatItems)
}
}
.onChange(of: im.itemAdded) { added in
if added {
im.itemAdded = false
sectionModel.resetSections(items: im.reversedChatItems)
if FloatingButtonModel.shared.isReallyNearBottom {
scrollModel.scrollToBottom()
}
@@ -838,10 +856,18 @@ struct ChatView: View {
await MainActor.run { forwardedChatItems = fci }
}
}
private func boundaryReached(_ direction: ChatScrollDirection) -> Bool {
return (direction == .toLatest && sectionModel.boundaries.latest) || (direction == .toOldest && sectionModel.boundaries.oldest)
}
private func setBoundary(_ direction: ChatScrollDirection) {
direction == .toLatest ? (sectionModel.boundaries.latest = true) : (sectionModel.boundaries.oldest = true)
}
private func loadChatItems(_ cInfo: ChatInfo) {
private func loadChatItems(_ cInfo: ChatInfo, _ direction: ChatScrollDirection, _ section: ChatSection, _ chatItem: ChatItem?) {
Task {
if loadingItems || firstPage { return }
if loadingItems || boundaryReached(direction) { return }
loadingItems = true
do {
var reversedPage = Array<ChatItem>()
@@ -849,11 +875,14 @@ struct ChatView: View {
// Load additional items until the page is +50 large after merging
while chatItemsAvailable && filtered(reversedPage).count < loadItemsPerPage {
let pagination: ChatPagination =
if let lastItem = reversedPage.last ?? im.reversedChatItems.last {
.before(chatItemId: lastItem.id, count: loadItemsPerPage)
} else {
.last(count: loadItemsPerPage)
}
if direction == .toOldest, let lastItem = chatItem ?? reversedPage.last ?? im.reversedChatItems.last {
.before(chatItemId: lastItem.id, count: loadItemsPerPage)
} else if direction == .toLatest, let firstItem = chatItem ?? reversedPage.first ?? im.reversedChatItems.first {
.after(chatItemId: firstItem.id, count: loadItemsPerPage)
} else {
.last(count: loadItemsPerPage)
}
let chatItems = try await apiGetChatItems(
type: cInfo.chatType,
id: cInfo.apiId,
@@ -863,12 +892,29 @@ struct ChatView: View {
chatItemsAvailable = !chatItems.isEmpty
reversedPage.append(contentsOf: chatItems.reversed())
}
await MainActor.run {
if reversedPage.count == 0 {
firstPage = true
if reversedPage.count == 0 {
setBoundary(direction)
} else {
im.reversedChatItems.append(contentsOf: reversedPage)
let reversedPageToAppend = self.sectionModel.handleSectionInsertion(
candidateSection: section,
reversedPage: reversedPage,
allItems: im.reversedChatItems
)
if direction == .toLatest {
let at = if let chatItemId = chatItem?.id, let index = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) {
index
} else {
0
}
im.reversedChatItems.insert(contentsOf: reversedPageToAppend, at: at)
} else {
im.reversedChatItems.append(contentsOf: reversedPageToAppend)
}
}
self.sectionModel.manageActiveSection(scrollDirection: direction)
loadingItems = false
}
} catch let error {
@@ -1886,6 +1932,36 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) {
}
}
}
private func filtered(_ reversedChatItems: Array<ChatItem>) -> Array<ChatItem> {
reversedChatItems
.enumerated()
.filter { (index, chatItem) in
if let mergeCategory = chatItem.mergeCategory, index > 0 {
mergeCategory != reversedChatItems[index - 1].mergeCategory
} else {
true
}
}
.map { $0.element }
}
func loadItemsAround(_ cInfo: ChatInfo, _ chatItemId: Int64) async -> [ChatItem]? {
do {
var reversedPage = Array<ChatItem>()
let pagination: ChatPagination = .around(chatItemId: chatItemId, count: loadItemsPerPage * 2)
var chatItems = try await apiGetChatItems(
type: cInfo.chatType,
id: cInfo.apiId,
pagination: pagination,
search: ""
)
return chatItems.reversed()
} catch let error {
logger.error("apiGetChat error: \(responseError(error))")
return nil
}
}
struct ChatView_Previews: PreviewProvider {
static var previews: some View {
+298 -28
View File
@@ -10,17 +10,29 @@ import SwiftUI
import Combine
import SimpleXChat
enum ChatScrollDirection {
case toLatest
case toOldest
case none
}
enum ChatSection: CaseIterable {
case current
case destination
case bottom
}
/// A List, which displays it's items in reverse order - from bottom to top
struct ReverseList<Content: View>: UIViewControllerRepresentable {
let items: Array<ChatItem>
@Binding var scrollState: ReverseListScrollModel.State
@ObservedObject var sectionModel: ReverseListSectionModel
/// Closure, that returns user interface for a given item
let content: (ChatItem) -> Content
let loadPage: () -> Void
let loadPage: (ChatScrollDirection, ChatSection, ChatItem?) -> Void
func makeUIViewController(context: Context) -> Controller {
Controller(representer: self)
}
@@ -32,10 +44,10 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
switch destination {
case .nextPage:
controller.scrollToNextPage()
case let .item(id):
controller.scroll(to: items.firstIndex(where: { $0.id == id }), position: .bottom)
case let .item(id, position, animated):
controller.scrollToItem(id: id, position: position, animated: animated)
case .bottom:
controller.scroll(to: 0, position: .top)
controller.scroll(to: 0, position: .top, section: .bottom, shouldTryAnimate: true)
}
} else {
controller.update(items: items)
@@ -44,13 +56,15 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
/// Controller, which hosts SwiftUI cells
class Controller: UITableViewController {
private enum Section { case main }
var representer: ReverseList
private var dataSource: UITableViewDiffableDataSource<Section, ChatItem>!
private var dataSource: UITableViewDiffableDataSource<ChatSection, ChatItem>!
private var itemCount: Int = 0
private let updateFloatingButtons = PassthroughSubject<Void, Never>()
private var bag = Set<AnyCancellable>()
private var lastContentOffset: CGFloat = 0
private var scrollDirection: ChatScrollDirection = .none
private var requestedRange = false
init(representer: ReverseList) {
self.representer = representer
super.init(style: .plain)
@@ -73,13 +87,24 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
forCellReuseIdentifier: cellReuseId
)
}
// 3. Configure data source
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
self.dataSource = UITableViewDiffableDataSource<ChatSection, ChatItem>(
tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in
if indexPath.item > self.itemCount - 8 {
self.representer.loadPage()
if let section = self.dataSource.sectionIdentifier(for: indexPath.section), self.representer.scrollState == .atDestination, !self.requestedRange {
if self.representer.sectionModel.activeSection == section {
let itemCount = self.getTotalItemsInItemSection(indexPath: indexPath)
if self.scrollDirection == .toOldest, indexPath.item > itemCount - 8 {
let lastItem = self.getLastItemInItemSection(indexPath: indexPath)
self.requestedRange = !self.representer.sectionModel.boundaries.oldest
self.representer.loadPage(.toOldest, section, lastItem)
} else if self.scrollDirection == .toLatest, indexPath.item < 8 {
self.requestedRange = !self.representer.sectionModel.boundaries.latest
let firstItem = self.getFirstItemInItemSection(indexPath: indexPath)
self.representer.loadPage(.toLatest, section, firstItem)
}
}
}
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
if #available(iOS 16.0, *) {
@@ -161,17 +186,35 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
)
Task { representer.scrollState = .atDestination }
}
/// Scrolls to a given item
func scrollToItem(id: Int64, position: UITableView.ScrollPosition, animated: Bool) {
if let loadedIndex = self.representer.items.firstIndex(where: { $0.id == id }) {
let ci = representer.items[loadedIndex]
if let indexPath = dataSource.indexPath(for: ci),
let section = dataSource.sectionIdentifier(for: indexPath.section) {
self.scroll(to: indexPath.row, position: position, section: section, shouldTryAnimate: animated)
}
}
}
/// Scrolls to Item at index path
/// - Parameter indexPath: Item to scroll to - will scroll to beginning of the list, if `nil`
func scroll(to index: Int?, position: UITableView.ScrollPosition) {
func scroll(to index: Int?, position: UITableView.ScrollPosition, section: ChatSection, shouldTryAnimate: Bool) {
let activeSectionBeforeScroll = representer.sectionModel.activeSection
if section != activeSectionBeforeScroll {
Task {
representer.sectionModel.changeActiveSection(section)
}
}
var animated = false
if #available(iOS 16.0, *) {
animated = true
animated = shouldTryAnimate
}
if let index, tableView.numberOfRows(inSection: 0) != 0 {
if let index, let sectionIndex = dataSource.index(for: section), tableView.numberOfRows(inSection: sectionIndex) != 0 {
tableView.scrollToRow(
at: IndexPath(row: index, section: 0),
at: IndexPath(row: index, section: sectionIndex),
at: position,
animated: animated
)
@@ -181,13 +224,25 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
animated: animated
)
}
Task { representer.scrollState = .atDestination }
Task {
representer.scrollState = .atDestination
if activeSectionBeforeScroll != section {
representer.sectionModel.maybeDropSection(activeSectionBeforeScroll)
}
}
}
func update(items: [ChatItem]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, ChatItem>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
requestedRange = false
var snapshot = NSDiffableDataSourceSnapshot<ChatSection, ChatItem>()
let sections = self.representer.sectionModel.getSectionsOrdered()
let itemsBySection = self.itemsBySection(items: items)
snapshot.appendSections(sections)
sections.forEach { sec in
if let sectionItems = itemsBySection[sec] {
snapshot.appendItems(sectionItems, toSection: sec)
}
}
dataSource.defaultRowAnimation = .none
dataSource.apply(
snapshot,
@@ -205,6 +260,15 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentOffset = scrollView.contentOffset.y
if currentOffset > lastContentOffset {
scrollDirection = .toOldest
} else if currentOffset < lastContentOffset {
scrollDirection = .toLatest
} else {
scrollDirection = .none
}
lastContentOffset = currentOffset
updateFloatingButtons.send()
}
@@ -212,15 +276,18 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
if let visibleRows = tableView.indexPathsForVisibleRows,
visibleRows.last?.item ?? 0 < representer.items.count {
let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset
let topItemDate: Date? =
if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) {
representer.items[lastVisible.item].meta.itemTs
if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }),
let cI = self.dataSource.itemIdentifier(for: lastVisible) {
cI.meta.itemTs
} else {
nil
}
let bottomItemId: ChatItem.ID? =
if let firstVisible = visibleRows.first(where: { isVisible(indexPath: $0) }) {
representer.items[firstVisible.item].id
if let firstVisible = visibleRows.first(where: { isVisible(indexPath: $0) }),
let cI = self.dataSource.itemIdentifier(for: firstVisible) {
cI.id
} else {
nil
}
@@ -238,6 +305,40 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
} else { false }
}
private func getTotalItemsInItemSection(indexPath: IndexPath) -> Int {
return self.tableView.numberOfRows(inSection: indexPath.section)
}
private func getLastItemInItemSection(indexPath: IndexPath) -> ChatItem? {
let numberOfRows = self.getTotalItemsInItemSection(indexPath: indexPath)
return if numberOfRows > 0,
let lastItem = self.dataSource.itemIdentifier(for: IndexPath(row: numberOfRows - 1, section: indexPath.section)) {
lastItem
} else {
nil
}
}
private func getFirstItemInItemSection(indexPath: IndexPath) -> ChatItem? {
let firstIndexPath = IndexPath(item: 0, section: indexPath.section)
return if let firstItem = self.dataSource.itemIdentifier(for: firstIndexPath) {
firstItem
} else {
nil
}
}
private func itemsBySection(items: [ChatItem]) -> [ChatSection: [ChatItem]] {
let itemsBySection = items.reduce(into: [ChatSection: [ChatItem]]()) { result, ci in
if let sec = self.representer.sectionModel.getItemSection(ci.id) {
result[sec, default: []].append(ci)
}
}
return itemsBySection
}
}
/// `UIHostingConfiguration` back-port for iOS14 and iOS15
@@ -292,7 +393,7 @@ class ReverseListScrollModel: ObservableObject {
enum State: Equatable {
enum Destination: Equatable {
case nextPage
case item(ChatItem.ID)
case item(ChatItem.ID, UITableView.ScrollPosition, Bool)
case bottom
}
@@ -310,8 +411,177 @@ class ReverseListScrollModel: ObservableObject {
state = .scrollingTo(.bottom)
}
func scrollToItem(id: ChatItem.ID) {
state = .scrollingTo(.item(id))
func scrollToItem(id: ChatItem.ID, position: UITableView.ScrollPosition? = .bottom, animated: Bool = true) {
state = .scrollingTo(.item(id, position ?? .bottom, animated))
}
}
struct SectionBoundaryReached {
var oldest: Bool
var latest: Bool
}
/// Manages ``ReverseList`` sections
class ReverseListSectionModel: ObservableObject {
static let shared = ReverseListSectionModel()
static let MAX_SECTION_SIZE = 500
static let MIN_SECTION_SIZE = 200
@Published private(set) var activeSection: ChatSection = .bottom
@Published var boundaries = SectionBoundaryReached(oldest: false, latest: false)
@Published private var itemSection: [Int64: ChatSection] = [:]
@Published private var sections = Set<ChatSection>()
func getItemSection(_ id: Int64) -> ChatSection? {
return itemSection[id]
}
func changeActiveSection(_ section: ChatSection) {
activeSection = section
boundaries.latest = false
boundaries.oldest = false
}
func maybeDropSection(_ section: ChatSection) {
if section == .bottom { return }
let im = ItemsModel.shared
sections.remove(section)
im.reversedChatItems = im.reversedChatItems.filter { it in itemSection[it.id] != section }
}
private func activeSectionItemsCount() -> Int {
// TODO: check
return itemSection.values.filter { $0 == self.activeSection }.count
}
private func splitSection(originalSection: ChatSection, newSection: ChatSection) {
var originalSectionCount = 0
let im = ItemsModel.shared
im.reversedChatItems.forEach { it in
if itemSection[it.id] == originalSection {
if originalSectionCount < ReverseListSectionModel.MIN_SECTION_SIZE {
originalSectionCount += 1
} else {
itemSection[it.id] = newSection
}
}
}
}
private func trimSection(scrollDirection: ChatScrollDirection, section: ChatSection) {
let im = ItemsModel.shared
var i = scrollDirection == .toLatest ? 0 : im.reversedChatItems.count - 1
let moveToNextIndex: () -> Bool = {
if scrollDirection == .toLatest {
if i < im.reversedChatItems.count - 1 {
i += 1
return false
} else {
return true
}
} else {
if i > 0 {
i -= 1
return false
} else {
return true
}
}
}
var sectionCount = 0
var completed = false
var toRemoveIndexes = IndexSet()
while (!completed) {
let it = im.reversedChatItems[i]
if itemSection[it.id] == section {
if sectionCount >= ReverseListSectionModel.MAX_SECTION_SIZE {
toRemoveIndexes.insert(i)
itemSection.removeValue(forKey: it.id)
} else {
sectionCount += 1
}
}
completed = moveToNextIndex()
}
if (!toRemoveIndexes.isEmpty) {
im.reversedChatItems.remove(atOffsets: toRemoveIndexes)
}
}
func manageActiveSection(scrollDirection: ChatScrollDirection) {
let activeSectionCount = activeSectionItemsCount()
switch self.activeSection {
case .bottom:
if activeSectionCount > ReverseListSectionModel.MAX_SECTION_SIZE {
if scrollDirection == .toLatest {
boundaries.oldest = false
self.trimSection(scrollDirection: scrollDirection, section: self.activeSection)
} else {
splitSection(originalSection: .bottom, newSection: .current)
changeActiveSection(.current)
sections.insert(.current)
}
}
break;
default:
if activeSectionCount > ReverseListSectionModel.MAX_SECTION_SIZE {
if scrollDirection == .toLatest {
boundaries.oldest = false
} else {
boundaries.latest = false
}
self.trimSection(scrollDirection: scrollDirection, section: self.activeSection)
}
}
}
func getSectionsOrdered() -> [ChatSection] {
var orderedSections: [ChatSection] = [.bottom]
if (sections.contains(.current)) {
orderedSections.append(.current)
}
if (sections.contains(.destination)) {
orderedSections.append(.destination)
}
return orderedSections
}
func resetSections(items: [ChatItem]) {
itemSection = Dictionary(uniqueKeysWithValues: items.map { ($0.id, .bottom) })
}
func handleSectionInsertion(candidateSection: ChatSection, reversedPage: [ChatItem], allItems: [ChatItem]) -> [ChatItem] {
var reversedPageToAppend = Array<ChatItem>()
var targetSection = candidateSection
if let sectionIntersectionItem = reversedPage.first(where: { itemSection[$0.id] != nil && itemSection[$0.id] != candidateSection }) {
let sectionToDrop = itemSection[sectionIntersectionItem.id] == .bottom ? candidateSection : (itemSection[sectionIntersectionItem.id] ?? .destination)
targetSection = itemSection[sectionIntersectionItem.id] == .bottom ? .bottom : candidateSection
itemSection = itemSection.mapValues { section in
section == sectionToDrop ? targetSection : section
}
if (sectionToDrop != targetSection) {
sections.remove(sectionToDrop)
}
}
reversedPage.forEach { ci in
if itemSection[ci.id] == nil {
reversedPageToAppend.append(ci)
itemSection[ci.id] = targetSection
}
}
sections.insert(targetSection)
self.activeSection = targetSection
return reversedPageToAppend
}
}
+4 -2
View File
@@ -213,8 +213,10 @@ public func chatResponse(_ s: String) -> ChatResponse {
if let jApiChat = jResp["apiChat"] as? NSDictionary,
let user: UserRef = try? decodeObject(jApiChat["user"] as Any),
let jChat = jApiChat["chat"] as? NSDictionary,
let chat = try? parseChatData(jChat) {
return .apiChat(user: user, chat: chat)
let jSection = jApiChat["section"] as? String,
let chat = try? parseChatData(jChat),
let section = ChatLandingSection(rawValue: jSection) {
return .apiChat(user: user, chat: chat, section: section)
}
} else if type == "chatCmdError" {
if let jError = jResp["chatCmdError"] as? NSDictionary {
+6 -2
View File
@@ -546,7 +546,7 @@ public enum ChatResponse: Decodable, Error {
case chatStopped
case chatSuspended
case apiChats(user: UserRef, chats: [ChatData])
case apiChat(user: UserRef, chat: ChatData)
case apiChat(user: UserRef, chat: ChatData, section: ChatLandingSection)
case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo)
case userProtoServers(user: UserRef, servers: UserProtoServers)
case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?)
@@ -888,7 +888,7 @@ public enum ChatResponse: Decodable, Error {
case .chatStopped: return noDetails
case .chatSuspended: return noDetails
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
case let .apiChat(u, chat): return withUser(u, String(describing: chat))
case let .apiChat(u, chat, section): return withUser(u, "section: \(String(describing: section))\n\(String(describing: chat))")
case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))")
case let .userProtoServers(u, servers): return withUser(u, "servers: \(String(describing: servers))")
case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))")
@@ -1133,12 +1133,16 @@ public enum ChatPagination {
case last(count: Int)
case after(chatItemId: Int64, count: Int)
case before(chatItemId: Int64, count: Int)
case around(chatItemId: Int64, count: Int)
case initial(count: Int)
var cmdString: String {
switch self {
case let .last(count): return "count=\(count)"
case let .after(chatItemId, count): return "after=\(chatItemId) count=\(count)"
case let .before(chatItemId, count): return "before=\(chatItemId) count=\(count)"
case let .around(chatItemId, count): return "around=\(chatItemId) count=\(count)"
case let .initial(count): return "initial=\(count)"
}
}
}
+5
View File
@@ -1619,6 +1619,11 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
)
}
public enum ChatLandingSection: String, Decodable, Hashable {
case latest = "latest"
case unread = "unread"
}
public enum ContactStatus: String, Decodable, Hashable {
case active = "active"
case deleted = "deleted"