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

View File

@ -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 {

View File

@ -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)
} }
} }
} }