Compare commits
6 Commits
2396a707ec
...
f69f0ae59e
| Author | SHA1 | Date | |
|---|---|---|---|
| f69f0ae59e | |||
| 3b1b2f3282 | |||
| 4a045ac140 | |||
| 0839009734 | |||
| f4e4a61192 | |||
| 95bf287085 |
@ -280,6 +280,9 @@
|
|||||||
"Блокировка контакта \"%1$@\" появится позже." : {
|
"Блокировка контакта \"%1$@\" появится позже." : {
|
||||||
"comment" : "Contacts block placeholder message"
|
"comment" : "Contacts block placeholder message"
|
||||||
},
|
},
|
||||||
|
"Больше сообщений нет" : {
|
||||||
|
"comment" : "Chat history top reached"
|
||||||
|
},
|
||||||
"Бот" : {
|
"Бот" : {
|
||||||
"comment" : "Тип сессии — бот"
|
"comment" : "Тип сессии — бот"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,13 +7,13 @@ final class PrivateChatViewModel: ObservableObject {
|
|||||||
@Published private(set) var isLoadingMore: Bool = false
|
@Published private(set) var isLoadingMore: Bool = false
|
||||||
@Published var errorMessage: String?
|
@Published var errorMessage: String?
|
||||||
@Published private(set) var isSending: Bool = false
|
@Published private(set) var isSending: Bool = false
|
||||||
|
@Published private(set) var hasMore: Bool = true
|
||||||
|
|
||||||
private let chatService: ChatService
|
private let chatService: ChatService
|
||||||
private let chatId: String
|
private let chatId: String
|
||||||
private let currentUserId: String?
|
private let currentUserId: String?
|
||||||
private let pageSize: Int
|
private let pageSize: Int
|
||||||
private let maxMessageLength: Int = 4096
|
private let maxMessageLength: Int = 4096
|
||||||
private var hasMore: Bool = true
|
|
||||||
private var didLoadInitially: Bool = false
|
private var didLoadInitially: Bool = false
|
||||||
private var messageObserver: NSObjectProtocol?
|
private var messageObserver: NSObjectProtocol?
|
||||||
|
|
||||||
@ -127,11 +127,21 @@ final class PrivateChatViewModel: ObservableObject {
|
|||||||
|
|
||||||
func loadMoreIfNeeded(for message: MessageItem) {
|
func loadMoreIfNeeded(for message: MessageItem) {
|
||||||
guard didLoadInitially, !isInitialLoading, hasMore, !isLoadingMore else { return }
|
guard didLoadInitially, !isInitialLoading, hasMore, !isLoadingMore else { return }
|
||||||
guard let first = messages.first, first.id == message.id else { return }
|
|
||||||
|
guard let messageIndex = messages.firstIndex(where: { $0.id == message.id }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let threshold = 10
|
||||||
|
guard messageIndex < threshold else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let oldestMessage = messages.first else { return }
|
||||||
|
|
||||||
isLoadingMore = true
|
isLoadingMore = true
|
||||||
|
|
||||||
chatService.fetchPrivateChatHistory(chatId: chatId, beforeMessageId: message.id, limit: pageSize) { [weak self] result in
|
chatService.fetchPrivateChatHistory(chatId: chatId, beforeMessageId: oldestMessage.id, limit: pageSize) { [weak self] result in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
|
|||||||
@ -24,6 +24,10 @@ struct PrivateChatView: View {
|
|||||||
@EnvironmentObject private var messageCenter: IncomingMessageCenter
|
@EnvironmentObject private var messageCenter: IncomingMessageCenter
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@State private var previousStandardAppearance: UINavigationBarAppearance?
|
||||||
|
@State private var previousScrollEdgeAppearance: UINavigationBarAppearance?
|
||||||
|
@State private var previousCompactAppearance: UINavigationBarAppearance?
|
||||||
|
|
||||||
init(chat: PrivateChatListItem, currentUserId: String?) {
|
init(chat: PrivateChatListItem, currentUserId: String?) {
|
||||||
self.chat = chat
|
self.chat = chat
|
||||||
self.currentUserId = currentUserId
|
self.currentUserId = currentUserId
|
||||||
@ -36,14 +40,15 @@ struct PrivateChatView: View {
|
|||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
content
|
content
|
||||||
.onChange(of: viewModel.messages.count) { _ in
|
.onChange(of: viewModel.messages.count) { _ in
|
||||||
guard !viewModel.isLoadingMore else { return }
|
if isBottomAnchorVisible {
|
||||||
scrollToBottom(proxy: proxy)
|
scrollToBottom(proxy: proxy)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.onChange(of: scrollToBottomTrigger) { _ in
|
.onChange(of: scrollToBottomTrigger) { _ in
|
||||||
scrollToBottom(proxy: proxy)
|
scrollToBottom(proxy: proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isBottomAnchorVisible {
|
if !isBottomAnchorVisible && !viewModel.isInitialLoading {
|
||||||
scrollToBottomButton(proxy: proxy)
|
scrollToBottomButton(proxy: proxy)
|
||||||
.padding(.trailing, 12)
|
.padding(.trailing, 12)
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
@ -93,10 +98,24 @@ struct PrivateChatView: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
messageCenter.activeChatId = chat.chatId
|
messageCenter.activeChatId = chat.chatId
|
||||||
|
|
||||||
|
previousStandardAppearance = UINavigationBar.appearance().standardAppearance
|
||||||
|
previousScrollEdgeAppearance = UINavigationBar.appearance().scrollEdgeAppearance
|
||||||
|
previousCompactAppearance = UINavigationBar.appearance().compactAppearance
|
||||||
|
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithDefaultBackground()
|
||||||
|
// appearance.shadowColor = .clear
|
||||||
|
|
||||||
|
UINavigationBar.appearance().standardAppearance = appearance
|
||||||
|
UINavigationBar.appearance().scrollEdgeAppearance = appearance
|
||||||
|
UINavigationBar.appearance().compactAppearance = appearance
|
||||||
}
|
}
|
||||||
.onChange(of: viewModel.isInitialLoading) { isLoading in
|
.onChange(of: viewModel.isInitialLoading) { isLoading in
|
||||||
if isLoading {
|
if isLoading {
|
||||||
hasPositionedToBottom = false
|
hasPositionedToBottom = false
|
||||||
|
} else if !viewModel.messages.isEmpty {
|
||||||
|
scrollToBottomTrigger = .init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.safeAreaInset(edge: .bottom) {
|
.safeAreaInset(edge: .bottom) {
|
||||||
@ -106,6 +125,12 @@ struct PrivateChatView: View {
|
|||||||
if messageCenter.activeChatId == chat.chatId {
|
if messageCenter.activeChatId == chat.chatId {
|
||||||
messageCenter.activeChatId = nil
|
messageCenter.activeChatId = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let standard = previousStandardAppearance {
|
||||||
|
UINavigationBar.appearance().standardAppearance = standard
|
||||||
|
}
|
||||||
|
UINavigationBar.appearance().scrollEdgeAppearance = previousScrollEdgeAppearance
|
||||||
|
UINavigationBar.appearance().compactAppearance = previousCompactAppearance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,35 +149,43 @@ struct PrivateChatView: View {
|
|||||||
private var messagesList: some View {
|
private var messagesList: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(alignment: .leading, spacing: 12) {
|
LazyVStack(alignment: .leading, spacing: 12) {
|
||||||
if viewModel.isLoadingMore {
|
Color.clear
|
||||||
loadingMoreView
|
.frame(height: 1)
|
||||||
} else if viewModel.messages.isEmpty {
|
.id(bottomAnchorId)
|
||||||
emptyState
|
.onAppear { isBottomAnchorVisible = true }
|
||||||
|
.onDisappear { isBottomAnchorVisible = false }
|
||||||
|
|
||||||
|
if let message = viewModel.errorMessage,
|
||||||
|
!message.isEmpty,
|
||||||
|
!viewModel.messages.isEmpty {
|
||||||
|
errorBanner(message: message)
|
||||||
|
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(viewModel.messages) { message in
|
ForEach(viewModel.messages.reversed()) { message in
|
||||||
messageRow(for: message)
|
messageRow(for: message)
|
||||||
.id(message.id)
|
.id(message.id)
|
||||||
|
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
guard hasPositionedToBottom else { return }
|
guard hasPositionedToBottom else { return }
|
||||||
viewModel.loadMoreIfNeeded(for: message)
|
viewModel.loadMoreIfNeeded(for: message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let message = viewModel.errorMessage,
|
if viewModel.isLoadingMore {
|
||||||
!message.isEmpty,
|
loadingMoreView
|
||||||
!viewModel.messages.isEmpty {
|
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||||
errorBanner(message: message)
|
} else if !viewModel.hasMore && !viewModel.messages.isEmpty {
|
||||||
|
noMoreMessagesView
|
||||||
|
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||||
|
} else if viewModel.messages.isEmpty {
|
||||||
|
emptyState
|
||||||
|
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||||
}
|
}
|
||||||
|
|
||||||
Color.clear
|
|
||||||
.frame(height: 1)
|
|
||||||
.id(bottomAnchorId)
|
|
||||||
.onAppear { isBottomAnchorVisible = true }
|
|
||||||
.onDisappear { isBottomAnchorVisible = false }
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
}
|
}
|
||||||
|
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||||
.simultaneousGesture(
|
.simultaneousGesture(
|
||||||
DragGesture().onChanged { value in
|
DragGesture().onChanged { value in
|
||||||
guard value.translation.height > 0 else { return }
|
guard value.translation.height > 0 else { return }
|
||||||
@ -179,6 +212,14 @@ struct PrivateChatView: View {
|
|||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var noMoreMessagesView: some View {
|
||||||
|
Text(NSLocalizedString("Больше сообщений нет", comment: "Chat history top reached"))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
private func errorView(message: String) -> some View {
|
private func errorView(message: String) -> some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Text(message)
|
Text(message)
|
||||||
@ -441,7 +482,7 @@ struct PrivateChatView: View {
|
|||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
proxy.scrollTo(targetId, anchor: .bottom)
|
proxy.scrollTo(targetId, anchor: .top)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user