Compare commits

..

6 Commits

Author SHA1 Message Date
f69f0ae59e msg fix 2025-12-11 21:43:43 +03:00
3b1b2f3282 patch msg 2025-12-11 21:15:45 +03:00
4a045ac140 fix chat 2025-12-11 21:02:12 +03:00
0839009734 fix scroll button 2025-12-11 20:48:12 +03:00
f4e4a61192 fix msg 2025-12-11 20:42:26 +03:00
95bf287085 fix chat 2025-12-11 20:13:42 +03:00
3 changed files with 76 additions and 22 deletions

View File

@ -280,6 +280,9 @@
"Блокировка контакта \"%1$@\" появится позже." : {
"comment" : "Contacts block placeholder message"
},
"Больше сообщений нет" : {
"comment" : "Chat history top reached"
},
"Бот" : {
"comment" : "Тип сессии — бот"
},

View File

@ -7,13 +7,13 @@ final class PrivateChatViewModel: ObservableObject {
@Published private(set) var isLoadingMore: Bool = false
@Published var errorMessage: String?
@Published private(set) var isSending: Bool = false
@Published private(set) var hasMore: Bool = true
private let chatService: ChatService
private let chatId: String
private let currentUserId: String?
private let pageSize: Int
private let maxMessageLength: Int = 4096
private var hasMore: Bool = true
private var didLoadInitially: Bool = false
private var messageObserver: NSObjectProtocol?
@ -127,11 +127,21 @@ final class PrivateChatViewModel: ObservableObject {
func loadMoreIfNeeded(for message: MessageItem) {
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
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 }
switch result {

View File

@ -24,6 +24,10 @@ struct PrivateChatView: View {
@EnvironmentObject private var messageCenter: IncomingMessageCenter
@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?) {
self.chat = chat
self.currentUserId = currentUserId
@ -36,14 +40,15 @@ struct PrivateChatView: View {
ZStack(alignment: .bottomTrailing) {
content
.onChange(of: viewModel.messages.count) { _ in
guard !viewModel.isLoadingMore else { return }
scrollToBottom(proxy: proxy)
if isBottomAnchorVisible {
scrollToBottom(proxy: proxy)
}
}
.onChange(of: scrollToBottomTrigger) { _ in
scrollToBottom(proxy: proxy)
}
if !isBottomAnchorVisible {
if !isBottomAnchorVisible && !viewModel.isInitialLoading {
scrollToBottomButton(proxy: proxy)
.padding(.trailing, 12)
.padding(.bottom, 4)
@ -93,10 +98,24 @@ struct PrivateChatView: View {
}
.onAppear {
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
if isLoading {
hasPositionedToBottom = false
} else if !viewModel.messages.isEmpty {
scrollToBottomTrigger = .init()
}
}
.safeAreaInset(edge: .bottom) {
@ -106,6 +125,12 @@ struct PrivateChatView: View {
if messageCenter.activeChatId == chat.chatId {
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 {
ScrollView {
LazyVStack(alignment: .leading, spacing: 12) {
if viewModel.isLoadingMore {
loadingMoreView
} else if viewModel.messages.isEmpty {
emptyState
Color.clear
.frame(height: 1)
.id(bottomAnchorId)
.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)
.id(message.id)
.scaleEffect(x: 1, y: -1, anchor: .center)
.onAppear {
guard hasPositionedToBottom else { return }
viewModel.loadMoreIfNeeded(for: message)
}
}
if let message = viewModel.errorMessage,
!message.isEmpty,
!viewModel.messages.isEmpty {
errorBanner(message: message)
if viewModel.isLoadingMore {
loadingMoreView
.scaleEffect(x: 1, y: -1, anchor: .center)
} 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)
}
.scaleEffect(x: 1, y: -1, anchor: .center)
.simultaneousGesture(
DragGesture().onChanged { value in
guard value.translation.height > 0 else { return }
@ -179,6 +212,14 @@ struct PrivateChatView: View {
.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 {
VStack(spacing: 12) {
Text(message)
@ -441,7 +482,7 @@ struct PrivateChatView: View {
DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.2)) {
proxy.scrollTo(targetId, anchor: .bottom)
proxy.scrollTo(targetId, anchor: .top)
}
}
}