diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 56fe727e5f..0be8436320 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -78,7 +78,7 @@ class ItemsModel: ObservableObject { loadChatTask?.cancel() navigationTimeoutTask = Task { do { - try await Task.sleep(nanoseconds: 100_000000) + try await Task.sleep(nanoseconds: 250_000000) await MainActor.run { ChatModel.shared.chatId = chatId willNavigate() @@ -949,7 +949,7 @@ final class ChatModel: ObservableObject { memberIds.insert(m.groupMemberId) } } else { - logger.error("getPrevHiddenMember: index >= count of reversed items: \(i) vs \(items.count)") + logger.error("getPrevHiddenMember: index >= count of reversed items: \(i) vs \(items.count), range: \(String(describing: range))") } } return (prevMember, memberIds.count) diff --git a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift index 6e5e25e5d8..094c6e13e1 100644 --- a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift +++ b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift @@ -9,13 +9,14 @@ import SwiftUI import SimpleXChat -func loadLastItems(_ loadingMoreItems: Binding, _ chat: Chat) { +func loadLastItems(_ loadingMoreItems: Binding, loadingBottomItems: Binding, _ chat: Chat) { if ItemsModel.shared.chatState.totalAfter == 0 { return } loadingMoreItems.wrappedValue = true + loadingBottomItems.wrappedValue = true Task { - try? await Task.sleep(nanoseconds: 1500_000000) + try? await Task.sleep(nanoseconds: 500_000000) if ChatModel.shared.chatId != chat.chatInfo.id { await MainActor.run { loadingMoreItems.wrappedValue = false @@ -25,6 +26,7 @@ func loadLastItems(_ loadingMoreItems: Binding, _ chat: Chat) { await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState) await MainActor.run { loadingMoreItems.wrappedValue = false + loadingBottomItems.wrappedValue = false } } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index a8d19733b5..f3bfa294b4 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -33,6 +33,10 @@ struct ChatView: View { @State private var customUserProfile: Profile? @State private var connectionCode: String? @State private var loadingMoreItems = false + @State private var loadingTopItems = false + @State private var requestedTopScroll = false + @State private var loadingBottomItems = false + @State private var requestedBottomScroll = false @State private var searchMode = false @State private var searchText: String = "" @FocusState private var searchFocussed @@ -49,6 +53,7 @@ struct ChatView: View { @State private var allowToDeleteSelectedMessagesForAll: Bool = false @State private var allowLoadMoreItems: Bool = false @State private var ignoreLoadingRequests: Int64? = nil + @State private var animatedScrollingInProgress: Bool = false @State private var updateMergedItemsTask: Task? = nil @State private var floatingButtonModel: FloatingButtonModel = FloatingButtonModel() @@ -88,7 +93,7 @@ struct ChatView: View { if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty { GroupMentionsView(groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible) } - FloatingButtons(theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, listState: scrollView.listState, model: floatingButtonModel) + FloatingButtons(theme: theme, scrollView: scrollView, chat: chat, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel) } connectingText() if selectedChatItems == nil { @@ -424,7 +429,9 @@ struct ChatView: View { index = mergedItems.boxedValue.indexInParentItems[itemId] } if let index { + await MainActor.run { animatedScrollingInProgress = true } await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index)) + await MainActor.run { animatedScrollingInProgress = false } } } catch { logger.error("Error scrolling to item: \(error)") @@ -551,7 +558,7 @@ struct ChatView: View { scrollView.updateItems(mergedItems.boxedValue.items) } .onChange(of: chat.id) { _ in - loadLastItems($loadingMoreItems, chat) + loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) allowLoadMoreItems = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { allowLoadMoreItems = true @@ -571,10 +578,10 @@ struct ChatView: View { } else if let index = scrollView.listState.items.lastIndex(where: { $0.hasUnread() }) { // scroll to the top unread item scrollView.scrollToItem(index) - loadLastItems($loadingMoreItems, chat) + loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) } else { scrollView.scrollToBottom() - loadLastItems($loadingMoreItems, chat) + loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) } } } @@ -620,7 +627,7 @@ struct ChatView: View { if let unreadIndex { scrollView.scrollToItem(unreadIndex) } - loadLastItems($loadingMoreItems, chat) + loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { allowLoadMoreItems = true } @@ -659,7 +666,7 @@ struct ChatView: View { } // set floating button indication mode - let nearBottom = listState.firstVisibleItemIndex < 4 + let nearBottom = listState.firstVisibleItemIndex < 1 if nearBottom != self.isNearBottom { DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in self?.isNearBottom = nearBottom @@ -702,7 +709,11 @@ struct ChatView: View { let theme: AppTheme let scrollView: EndlessScrollView let chat: Chat - @Binding var loadingMoreItems: Bool + @Binding var loadingTopItems: Bool + @Binding var requestedTopScroll: Bool + @Binding var loadingBottomItems: Bool + @Binding var requestedBottomScroll: Bool + @Binding var animatedScrollingInProgress: Bool let listState: EndlessScrollView.ListState @ObservedObject var model: FloatingButtonModel @@ -717,8 +728,8 @@ struct ChatView: View { .padding(.vertical, 4) } VStack { - if model.unreadAbove > 0 { - if loadingMoreItems { + if model.unreadAbove > 0 && !animatedScrollingInProgress { + if loadingTopItems && requestedTopScroll { circleButton { ProgressView() } } else { circleButton { @@ -727,11 +738,11 @@ struct ChatView: View { .foregroundColor(theme.colors.primary) } .onTapGesture { - if let index = listState.items.lastIndex(where: { $0.hasUnread() }) { - // scroll to the top unread item - Task { await scrollView.scrollToItemAnimated(index) } + if loadingTopItems { + requestedTopScroll = true + requestedBottomScroll = false } else { - logger.debug("No more unread items, total: \(listState.items.count)") + scrollToTopUnread() } } .contextMenu { @@ -746,37 +757,71 @@ struct ChatView: View { } } Spacer() - if model.unreadBelow > 0 { - if loadingMoreItems { + if listState.firstVisibleItemIndex != 0 && !animatedScrollingInProgress { + if loadingBottomItems && requestedBottomScroll { circleButton { ProgressView() } } else { circleButton { - unreadCountText(model.unreadBelow) - .font(.callout) - .foregroundColor(theme.colors.primary) + Group { + if model.unreadBelow > 0 { + unreadCountText(model.unreadBelow) + .font(.callout) + .foregroundColor(theme.colors.primary) + } else { + Image(systemName: "chevron.down").foregroundColor(theme.colors.primary) + } + } } .onTapGesture { - scrollView.scrollToBottomAnimated() + if loadingBottomItems { + requestedTopScroll = false + requestedBottomScroll = true + } else { + scrollToBottom() + } } } - } else if !model.isNearBottom { - if loadingMoreItems { - circleButton { ProgressView() } - } else { - circleButton { - Image(systemName: "chevron.down").foregroundColor(theme.colors.primary) - } - .onTapGesture { scrollView.scrollToBottomAnimated() } - } } } - .disabled(loadingMoreItems) .padding() .frame(maxWidth: .infinity, alignment: .trailing) } + .onChange(of: loadingTopItems) { loading in + if !loading && requestedTopScroll { + requestedTopScroll = false + scrollToTopUnread() + } + } + .onChange(of: loadingBottomItems) { loading in + if !loading && requestedBottomScroll { + requestedBottomScroll = false + scrollToBottom() + } + } .onDisappear(perform: model.resetDate) } + private func scrollToTopUnread() { + if let index = listState.items.lastIndex(where: { $0.hasUnread() }) { + animatedScrollingInProgress = true + // scroll to the top unread item + Task { + await scrollView.scrollToItemAnimated(index) + await MainActor.run { animatedScrollingInProgress = false } + } + } else { + logger.debug("No more unread items, total: \(listState.items.count)") + } + } + + private func scrollToBottom() { + animatedScrollingInProgress = true + Task { + await scrollView.scrollToItemAnimated(0, top: false) + await MainActor.run { animatedScrollingInProgress = false } + } + } + private func circleButton(_ content: @escaping () -> Content) -> some View { ZStack { Circle() @@ -1010,10 +1055,20 @@ struct ChatView: View { if loadingMoreItems { return false } await MainActor.run { loadingMoreItems = true + if case .before = pagination { + loadingTopItems = true + } else if case .after = pagination { + loadingBottomItems = true + } } let triedToLoad = await loadChatItemsUnchecked(chat, pagination) await MainActor.run { loadingMoreItems = false + if case .before = pagination { + loadingTopItems = false + } else if case .after = pagination { + loadingBottomItems = false + } } return triedToLoad } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index 676d9c43d2..5995bd30e1 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -136,8 +136,11 @@ private class CustomUITextField: UITextView, UITextViewDelegate { } override var intrinsicContentSize: CGSize { - if height.wrappedValue != newHeight { - DispatchQueue.main.asyncAfter(deadline: .now(), execute: { self.height.wrappedValue = self.newHeight }) + if height.wrappedValue != newHeight || + // when both heights equal to minHeight, we must update $height, even if it's the same, because only this way + // the swift ui wrapper will redisplay this view with updated height + newHeight == NativeTextEditor.minHeight { + DispatchQueue.main.async { self.height.wrappedValue = self.newHeight } } return CGSizeMake(0, newHeight) } diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme index a2639eb263..1a1fa8b800 100644 --- a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme @@ -74,7 +74,6 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" - askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2">