more changes

This commit is contained in:
Avently
2024-12-18 08:27:11 -08:00
parent a7ab5d135b
commit 7631485796
6 changed files with 142 additions and 111 deletions
+1 -1
View File
@@ -322,7 +322,7 @@ let loadItemsPerPage = 50
func apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) {
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search))
if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo) }
if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) }
throw r
}
@@ -15,7 +15,7 @@ struct MergedItems {
// chat item id, index in list
let indexInParentItems: Dictionary<Int64, Int>
static func create(_ items: [ChatItem], _ unreadCount: Binding<Int>, _ revealedItems: Set<Int64>, _ chatState: ActiveChatState) -> MergedItems {
static func create(_ items: [ChatItem], _ unreadCount: Int, _ revealedItems: Set<Int64>, _ chatState: ActiveChatState) -> MergedItems {
if items.isEmpty {
return MergedItems(items: [], splits: [], indexInParentItems: [:])
}
@@ -30,7 +30,7 @@ struct MergedItems {
var unclosedSplitIndex: Int? = nil
var unclosedSplitIndexInParent: Int? = nil
var visibleItemIndexInParent = -1
var unreadBefore = unreadCount.wrappedValue - chatState.unreadAfterNewestLoaded
var unreadBefore = unreadCount - chatState.unreadAfterNewestLoaded
var lastRevealedIdsInMergedItems: BoxedValue<[Int64]>? = nil
var lastRangeInReversedForMergedItems: BoxedValue<ClosedRange<Int>>? = nil
var recent: MergedItem? = nil
@@ -42,14 +42,14 @@ struct MergedItems {
let itemIsSplit = itemSplits.contains(item.id)
if item.id == unreadAfterItemId {
unreadBefore = unreadCount.wrappedValue - chatState.unreadAfter
unreadBefore = unreadCount - chatState.unreadAfter
}
if item.isRcvNew {
unreadBefore -= 1
}
let revealed = item.mergeCategory == nil || revealedItems.contains(item.id)
if recent != nil, case let .grouped(items, _, _, _, mergeCategory, unreadIds, _) = recent, mergeCategory == category, let first = items.first, !revealedItems.contains(first.item.id) && !itemIsSplit {
if recent != nil, case let .grouped(items, _, _, _, mergeCategory, _, _) = recent, mergeCategory == category, let first = items.first, !revealedItems.contains(first.item.id) && !itemIsSplit {
let listItem = ListItem(item: item, prevItem: prev, nextItem: next, unreadBefore: unreadBefore)
recent!.appendItem(listItem)
@@ -67,11 +67,11 @@ struct MergedItems {
let listItem = ListItem(item: item, prevItem: prev, nextItem: next, unreadBefore: unreadBefore)
if item.mergeCategory != nil {
if item.mergeCategory != prev?.mergeCategory || lastRevealedIdsInMergedItems == nil {
lastRevealedIdsInMergedItems?.boxedValue = revealedItems.contains(item.id) ? [item.id] : []
} else if revealed, var lastRevealedIdsInMergedItems {
lastRevealedIdsInMergedItems = BoxedValue(revealedItems.contains(item.id) ? [item.id] : [])
} else if revealed, let lastRevealedIdsInMergedItems {
lastRevealedIdsInMergedItems.boxedValue.append(item.id)
}
lastRangeInReversedForMergedItems?.boxedValue = index ... index
lastRangeInReversedForMergedItems = BoxedValue(index ... index)
recent = MergedItem.grouped(
items: [listItem],
revealed: revealed,
@@ -114,7 +114,7 @@ struct MergedItems {
}
enum MergedItem {
enum MergedItem: Hashable {
// the item that is always single, cannot be grouped and always revealed
case single(
item: ListItem,
@@ -158,8 +158,8 @@ enum MergedItem {
}
}
func reveal(reveal: Bool, revealedItems: Binding<Set<Int64>>) {
if case .grouped(let items, let revealed, var revealedIdsWithinGroup, let rangeInReversed, let mergeCategory, let unreadIds, let startIndexInReversedItems) = self {
func reveal(_ reveal: Bool, _ revealedItems: Binding<Set<Int64>>) {
if case .grouped(let items, _, let revealedIdsWithinGroup, _, _, _, _) = self {
var newRevealed = revealedItems.wrappedValue
var i = 0
if reveal {
@@ -226,7 +226,7 @@ struct SplitRange {
let indexRangeInParentItems: ClosedRange<Int>
}
struct ListItem {
struct ListItem: Hashable {
let item: ChatItem
let prevItem: ChatItem?
let nextItem: ChatItem?
@@ -278,7 +278,15 @@ class ActiveChatState {
}
}
class BoxedValue<T> {
class BoxedValue<T: Hashable>: Hashable {
static func == (lhs: BoxedValue<T>, rhs: BoxedValue<T>) -> Bool {
lhs.boxedValue == rhs.boxedValue
}
func hash(into hasher: inout Hasher) {
hasher.combine("\(self)")
}
var boxedValue : T
init(_ value: T) {
self.boxedValue = value
@@ -286,7 +294,7 @@ class BoxedValue<T> {
}
extension ReverseList.Controller {
func visibleItemIndexesNonReversed(_ mergedItems: Binding<MergedItems>, _ scrollModel: ReverseListScrollModel) -> ClosedRange<Int> {
func visibleItemIndexesNonReversed(_ mergedItems: Binding<MergedItems>) -> ClosedRange<Int> {
let zero = 0 ... 0
if itemCount == 0 {
return zero
+87 -81
View File
@@ -16,6 +16,7 @@ private let memberImageSize: CGFloat = 34
struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var im = ItemsModel.shared
@State var revealedItems: Set<Int64> = Set()
@State var theme: AppTheme = buildTheme()
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@@ -32,7 +33,6 @@ struct ChatView: View {
@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 = ""
@FocusState private var searchFocussed
@@ -168,11 +168,13 @@ struct ChatView: View {
}
.onAppear {
selectedChatItems = nil
revealedItems = Set()
initChatView()
}
.onChange(of: chatModel.chatId) { cId in
showChatInfoSheet = false
selectedChatItems = nil
revealedItems = Set()
scrollModel.scrollToBottom()
stopAudioPlayer()
if let cId {
@@ -185,14 +187,20 @@ struct ChatView: View {
dismiss()
}
}
.onChange(of: revealedChatItem) { _ in
.onChange(of: revealedItems) { _ in
NotificationCenter.postReverseListNeedsLayout()
}
.onChange(of: im.isLoading) { isLoading in
if !isLoading,
im.reversedChatItems.count <= loadItemsPerPage,
filtered(im.reversedChatItems).count < 10 {
loadChatItems(chat.chatInfo)
let pagination: ChatPagination =
if let lastItem = im.reversedChatItems.last {
.before(chatItemId: lastItem.id, count: loadItemsPerPage)
} else {
.last(count: loadItemsPerPage)
}
loadChatItems(chat.chatInfo, pagination)
}
}
.environmentObject(scrollModel)
@@ -425,7 +433,11 @@ struct ChatView: 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, revealedItems: $revealedItems, unreadCount: Binding.constant(chat.chatStats.unreadCount), scrollState: $scrollModel.state) { index, mergedItem in
let ci = switch mergedItem {
case let .single(item, _): item.item
case let .grouped(items, _, _, _, _, _, _): items.last!.item
}
let voiceNoFrame = voiceWithoutFrame(ci)
let maxWidth = cInfo.chatType == .group
? voiceNoFrame
@@ -435,19 +447,22 @@ struct ChatView: View {
? (g.size.width - 32)
: (g.size.width - 32) * 0.84
return ChatItemWithMenu(
index: index,
isLastItem: index == mergedItems.items.count - 1,
chat: $chat,
chatItem: ci,
item: mergedItem,
maxWidth: maxWidth,
composeState: $composeState,
selectedMember: $selectedMember,
showChatInfoSheet: $showChatInfoSheet,
revealedChatItem: $revealedChatItem,
revealedItems: $revealedItems,
selectedChatItems: $selectedChatItems,
forwardedChatItems: $forwardedChatItems
)
.id(ci.id) // Required to trigger `onAppear` on iOS15
} loadPage: {
loadChatItems(cInfo)
} loadItems: { pagination, visibleItemIndexesNonReversed in
loadChatItems(cInfo, pagination, visibleItemIndexesNonReversed)
}
.opacity(ItemsModel.shared.isLoading ? 0 : 1)
.padding(.vertical, -InvertedTableView.inset)
@@ -847,41 +862,31 @@ struct ChatView: View {
}
}
private func loadChatItems(_ cInfo: ChatInfo) {
private func loadChatItems(_ cInfo: ChatInfo, _ pagination: ChatPagination, _ visibleItemIndexesNonReversed: @escaping () -> ClosedRange<Int> = { 0 ... 0 }) {
Task {
if loadingItems || firstPage { return }
loadingItems = true
do {
var reversedPage = Array<ChatItem>()
var chatItemsAvailable = true
// 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)
}
let chatItems = try await apiGetChatItems(
type: cInfo.chatType,
id: cInfo.apiId,
pagination: pagination,
search: searchText
)
chatItemsAvailable = !chatItems.isEmpty
reversedPage.append(contentsOf: chatItems.reversed())
var chatItemsAvailable = true
var itemsCountChanged = false
// Load additional items until the page is +50 large after merging
while chatItemsAvailable && filtered(im.reversedChatItems).count < loadItemsPerPage {
let oldCount = im.reversedChatItems.count
await apiLoadMessages(
cInfo.chatType,
cInfo.apiId,
pagination,
im.chatState,
searchText,
visibleItemIndexesNonReversed
)
itemsCountChanged = im.reversedChatItems.count != oldCount
chatItemsAvailable = itemsCountChanged
}
await MainActor.run {
if !itemsCountChanged {
firstPage = true
}
await MainActor.run {
if reversedPage.count == 0 {
firstPage = true
} else {
im.reversedChatItems.append(contentsOf: reversedPage)
}
loadingItems = false
}
} catch let error {
logger.error("apiGetChat error: \(responseError(error))")
await MainActor.run { loadingItems = false }
loadingItems = false
}
}
}
@@ -897,12 +902,15 @@ struct ChatView: View {
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner
@Binding @ObservedObject var chat: Chat
@ObservedObject var dummyModel: ChatItemDummyModel = .shared
let index: Int
let isLastItem: Bool
let chatItem: ChatItem
let merged: MergedItem
let maxWidth: CGFloat
@Binding var composeState: ComposeState
@Binding var selectedMember: GMember?
@Binding var showChatInfoSheet: Bool
@Binding var revealedChatItem: ChatItem?
@Binding var revealedItems: Set<Int64>
@State private var deletingItem: ChatItem? = nil
@State private var showDeleteMessage = false
@@ -918,7 +926,7 @@ struct ChatView: View {
@State private var allowMenu: Bool = true
@State private var markedRead = false
var revealed: Bool { chatItem == revealedChatItem }
var revealed: Bool { revealedItems.contains(chatItem.id) }
typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?)
@@ -940,46 +948,44 @@ struct ChatView: View {
var body: some View {
let currIndex = m.getChatItemIndex(chatItem)
let ciCategory = chatItem.mergeCategory
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
let range = itemsRange(currIndex, prevHidden)
let timeSeparation = getItemSeparation(chatItem, at: currIndex)
let im = ItemsModel.shared
let last = isLastItem ? im.reversedChatItems.last : nil
let listItem = merged.newest()
let item = listItem.item
let range: ClosedRange<Int>? = if case let .grouped(_, _, _, rangeInReversed, _, _, _) = merged {
rangeInReversed.boxedValue
} else {
nil
}
let itemSeparation: ItemSeparation
let prevItemSeparationLargeGap: Boolean
let single = switch merged { case .single: true; default: false }
// if single || revealed {
// let prev = listItem.prevItem
// itemSeparation = getItemSeparation(item, prev)
// let nextForGap = if (item.mergeCategory != nil && item.mergeCategory == prev?.mergeCategory) || isLastItem { nil } else { listItem.nextItem }
// prevItemSeparationLargeGap = if nextForGap == nil { false } else { getItemSeparationLargeGap(nextForGap, item) }
// } else {
// itemSeparation = getItemSeparation(item, nil)
// prevItemSeparationLargeGap = false
// }
Group {
if revealed, let range = range {
let items = Array(zip(Array(range), im.reversedChatItems[range]))
VStack(spacing: 0) {
ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
chatItemView(ci, nil, prev, getItemSeparation(ci, at: i))
.overlay {
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
let checked = selected.contains(ci.id)
selectUnselectChatItem(select: !checked, ci)
}
VStack(spacing: 0) {
chatItemView(item, range, listItem.prevItem, itemSeparation)
// if let date = timeSeparation.date {
// DateSeparator(date: date).padding(8)
// }
.overlay {
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
let checked = selected.contains(chatItem.id)
selectUnselectChatItem(select: !checked, chatItem)
}
}
}
}
}
} else {
VStack(spacing: 0) {
chatItemView(chatItem, range, prevItem, timeSeparation)
if let date = timeSeparation.date {
DateSeparator(date: date).padding(8)
}
}
.overlay {
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
let checked = selected.contains(chatItem.id)
selectUnselectChatItem(select: !checked, chatItem)
}
}
}
}
}
.onAppear {
@@ -1072,7 +1078,7 @@ struct ChatView: View {
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?, _ itemSeparation: ItemSeparation) -> some View {
let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2
if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
case .group = chat.chatInfo {
let (prevMember, memCount): (GroupMember?, Int) =
if let range = range {
m.getPrevHiddenMember(member, range)
@@ -1597,7 +1603,7 @@ struct ChatView: View {
private func hideButton() -> Button<some View> {
Button {
withConditionalAnimation {
revealedChatItem = nil
merged.reveal(false, $revealedItems)
}
} label: {
Label(
@@ -1672,7 +1678,7 @@ struct ChatView: View {
private func revealButton(_ ci: ChatItem) -> Button<some View> {
Button {
withConditionalAnimation {
revealedChatItem = ci
merged.reveal(true, $revealedItems)
}
} label: {
Label(
@@ -1685,7 +1691,7 @@ struct ChatView: View {
private func expandButton() -> Button<some View> {
Button {
withConditionalAnimation {
revealedChatItem = chatItem
merged.reveal(true, $revealedItems)
}
} label: {
Label(
@@ -1698,7 +1704,7 @@ struct ChatView: View {
private func shrinkButton() -> Button<some View> {
Button {
withConditionalAnimation {
revealedChatItem = nil
merged.reveal(false, $revealedItems)
}
} label: {
Label (
+26 -13
View File
@@ -13,13 +13,17 @@ import SimpleXChat
/// 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 revealedItems: Set<Int64>
@Binding var unreadCount: Int
@Binding var scrollState: ReverseListScrollModel.State
/// Closure, that returns user interface for a given item
let content: (ChatItem) -> Content
/// Index, merged item
let content: (Int, MergedItem) -> Content
let loadPage: () -> Void
// pagination, visibleItemIndexesNonReversed
let loadItems: (ChatPagination, @escaping () -> ClosedRange<Int>) -> Void
func makeUIViewController(context: Context) -> Controller {
Controller(representer: self)
@@ -46,13 +50,19 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
public class Controller: UITableViewController {
private enum Section { case main }
var representer: ReverseList
private var dataSource: UITableViewDiffableDataSource<Section, ChatItem>!
var itemCount: Int = 0
private var dataSource: UITableViewDiffableDataSource<Section, MergedItem>!
var itemCount: Int {
get {
mergedItems.items.count
}
}
private var mergedItems: MergedItems
private let updateFloatingButtons = PassthroughSubject<Void, Never>()
private var bag = Set<AnyCancellable>()
init(representer: ReverseList) {
self.representer = representer
self.mergedItems = MergedItems.create(representer.items, representer.unreadCount, Set(), ItemsModel.shared.chatState)
super.init(style: .plain)
// 1. Style
@@ -75,20 +85,22 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
}
// 3. Configure data source
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
self.dataSource = UITableViewDiffableDataSource<Section, MergedItem>(
tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in
if indexPath.item > self.itemCount - 8 {
self.representer.loadPage()
logger.debug("LALAL ITEM \(indexPath.item)")
let pagination = ChatPagination.last(count: 0)
self.representer.loadItems(pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.mergedItems)) })
}
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
if #available(iOS 16.0, *) {
cell.contentConfiguration = UIHostingConfiguration { self.representer.content(item) }
cell.contentConfiguration = UIHostingConfiguration { self.representer.content(indexPath.item, item) }
.margins(.all, 0)
.minSize(height: 1) // Passing zero will result in system default of 44 points being used
} else {
if let cell = cell as? HostingCell<Content> {
cell.set(content: self.representer.content(item), parent: self)
cell.set(content: self.representer.content(indexPath.item, item), parent: self)
} else {
fatalError("Unexpected Cell Type for: \(item)")
}
@@ -185,22 +197,23 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
}
func update(items: [ChatItem]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, ChatItem>()
let wasCount = itemCount
self.mergedItems = MergedItems.create(items, representer.unreadCount, representer.revealedItems, ItemsModel.shared.chatState)
var snapshot = NSDiffableDataSourceSnapshot<Section, MergedItem>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
snapshot.appendItems(mergedItems.items)
dataSource.defaultRowAnimation = .none
dataSource.apply(
snapshot,
animatingDifferences: itemCount != 0 && abs(items.count - itemCount) == 1
animatingDifferences: wasCount != 0 && abs(mergedItems.items.count - wasCount) == 1
)
// Sets content offset on initial load
if itemCount == 0 {
if wasCount == 0 {
tableView.setContentOffset(
CGPoint(x: 0, y: -InvertedTableView.inset),
animated: false
)
}
itemCount = items.count
updateFloatingButtons.send()
}
+1 -2
View File
@@ -213,8 +213,7 @@ 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 jNavInfo = jApiChat["navInfo"] as? NSDictionary,
let (chat, navInfo) = try? parseChatData(jChat, jNavInfo) {
let (chat, navInfo) = try? parseChatData(jChat, jApiChat["navInfo"] as? NSDictionary) {
return .apiChat(user: user, chat: chat, navInfo: navInfo)
}
} else if type == "chatCmdError" {
+6 -1
View File
@@ -563,7 +563,7 @@ public enum ChatResponse: Decodable, Error {
case chatStopped
case chatSuspended
case apiChats(user: UserRef, chats: [ChatData])
case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo)
case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo?)
case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo)
case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?)
case serverOperatorConditions(conditions: ServerOperatorConditions)
@@ -1977,6 +1977,11 @@ public struct ChatSettings: Codable, Hashable {
public struct NavigationInfo: Decodable {
public var afterUnread: Int = 0
public var afterTotal: Int = 0
public init(afterUnread: Int = 0, afterTotal: Int = 0) {
self.afterUnread = afterUnread
self.afterTotal = afterTotal
}
}
public enum MsgFilter: String, Codable, Hashable {