mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
ios: enhancements to floating buttons (#5644)
* ios: enhancements to floating buttons * nearBottom * timeout * changes --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
parent
dc980ae88f
commit
676583d3c3
5 changed files with 95 additions and 36 deletions
|
@ -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)
|
||||
|
|
|
@ -9,13 +9,14 @@
|
|||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
func loadLastItems(_ loadingMoreItems: Binding<Bool>, _ chat: Chat) {
|
||||
func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Binding<Bool>, _ 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<Bool>, _ chat: Chat) {
|
|||
await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState)
|
||||
await MainActor.run {
|
||||
loadingMoreItems.wrappedValue = false
|
||||
loadingBottomItems.wrappedValue = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Void, Never>? = 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<MergedItem>
|
||||
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<MergedItem>.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 {
|
||||
Group {
|
||||
if model.unreadBelow > 0 {
|
||||
unreadCountText(model.unreadBelow)
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture {
|
||||
scrollView.scrollToBottomAnimated()
|
||||
}
|
||||
}
|
||||
} else if !model.isNearBottom {
|
||||
if loadingMoreItems {
|
||||
circleButton { ProgressView() }
|
||||
} else {
|
||||
circleButton {
|
||||
Image(systemName: "chevron.down").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture { scrollView.scrollToBottomAnimated() }
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if loadingBottomItems {
|
||||
requestedTopScroll = false
|
||||
requestedBottomScroll = true
|
||||
} else {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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: View>(_ 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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -74,7 +74,6 @@
|
|||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue