Compare commits

...

6 Commits

Author SHA1 Message Date
5f03feba66 fix pos 2025-10-22 06:14:18 +03:00
b267e5a999 add button scroll down 2025-10-22 06:11:19 +03:00
b9ea0807e5 scroll down 2025-10-22 06:03:10 +03:00
055c57c208 disable keyboard 2025-10-22 05:59:34 +03:00
bbed505033 patch send msg 2025-10-22 05:49:19 +03:00
ee4f783fe7 fix keyboard 2025-10-22 05:41:51 +03:00

View File

@ -3,9 +3,12 @@ import SwiftUI
struct PrivateChatView: View {
let chat: PrivateChatListItem
let currentUserId: String?
private let bottomAnchorId = "PrivateChatBottomAnchor"
@StateObject private var viewModel: PrivateChatViewModel
@State private var hasPositionedToBottom: Bool = false
@State private var scrollToBottomTrigger: UUID = .init()
@State private var isBottomAnchorVisible: Bool = true
@State private var draftText: String = ""
@State private var inputTab: ComposerTab = .chat
@State private var isVideoPreferred: Bool = false
@ -19,18 +22,34 @@ struct PrivateChatView: View {
}
var body: some View {
ScrollViewReader { proxy in
content
.onChange(of: viewModel.messages.count) { _ in
guard !viewModel.isLoadingMore,
let lastId = viewModel.messages.last?.id else { return }
DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.2)) {
proxy.scrollTo(lastId, anchor: .bottom)
ZStack(alignment: .bottomTrailing) {
ScrollViewReader { proxy in
content
.onChange(of: viewModel.messages.count) { _ in
guard !viewModel.isLoadingMore else { return }
let targetId = viewModel.messages.last?.id ?? bottomAnchorId
DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.2)) {
proxy.scrollTo(targetId, anchor: .bottom)
}
hasPositionedToBottom = true
}
hasPositionedToBottom = true
}
}
.onChange(of: scrollToBottomTrigger) { _ in
let targetId = viewModel.messages.last?.id ?? bottomAnchorId
DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.2)) {
proxy.scrollTo(targetId, anchor: .bottom)
}
}
}
}
if !isBottomAnchorVisible {
scrollToBottomButton
.padding(.trailing, 20)
.padding(.bottom, 30)
}
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
@ -85,9 +104,21 @@ struct PrivateChatView: View {
!viewModel.messages.isEmpty {
errorBanner(message: message)
}
Color.clear
.frame(height: 1)
.id(bottomAnchorId)
.onAppear { isBottomAnchorVisible = true }
.onDisappear { isBottomAnchorVisible = false }
}
.padding(.vertical, 12)
}
.simultaneousGesture(
DragGesture().onChanged { value in
guard value.translation.height > 0 else { return }
isComposerFocused = false
}
)
.refreshable {
viewModel.refresh()
}
@ -213,16 +244,16 @@ struct PrivateChatView: View {
Button(action: { }) { // переключатель на стикеры
Image(systemName: "paperclip")
.font(.system(size: 18, weight: .semibold))
.frame(width: 40, height: 40)
.foregroundColor(.secondary)
}
.buttonStyle(ComposerIconButtonStyle())
ZStack(alignment: .bottomTrailing) {
TextField(inputTab.placeholder, text: $draftText, axis: .vertical)
.lineLimit(1...4)
.focused($isComposerFocused)
.submitLabel(.send)
.disabled(viewModel.isSending || currentUserId == nil)
.disabled(currentUserId == nil)
.onSubmit { sendCurrentMessage() }
.padding(.top, 10)
.padding(.leading, 12)
@ -249,10 +280,9 @@ struct PrivateChatView: View {
Button(action: { isVideoPreferred.toggle() }) {
Image(systemName: isVideoPreferred ? "video.fill" : "mic.fill")
.font(.system(size: 18, weight: .semibold))
.frame(width: 40, height: 40)
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
.buttonStyle(ComposerIconButtonStyle())
} else {
sendButton
}
@ -264,8 +294,21 @@ struct PrivateChatView: View {
.background(.ultraThinMaterial)
}
private var scrollToBottomButton: some View {
Button(action: scrollToBottom) {
Image(systemName: "arrow.down")
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
.frame(width: 44, height: 44)
.background(Color.accentColor)
.clipShape(Circle())
}
.buttonStyle(.plain)
.shadow(color: Color.black.opacity(0.2), radius: 4, x: 0, y: 2)
}
private var isSendDisabled: Bool {
draftText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSending || currentUserId == nil
draftText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || currentUserId == nil
}
private var isSendAvailable: Bool {
@ -317,14 +360,35 @@ struct PrivateChatView: View {
let text = draftText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return }
draftText = ""
scrollToBottomTrigger = .init()
viewModel.sendMessage(text: text) { success in
if success {
draftText = ""
hasPositionedToBottom = true
}
}
}
private func scrollToBottom() {
scrollToBottomTrigger = .init()
hasPositionedToBottom = true
isBottomAnchorVisible = true
}
private struct ComposerIconButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(width: 36, height: 36)
.background(Color(.secondarySystemBackground))
.clipShape(Circle())
.overlay(
Circle().stroke(Color.secondary.opacity(0.15))
)
.scaleEffect(configuration.isPressed ? 0.94 : 1)
.opacity(configuration.isPressed ? 0.75 : 1)
}
}
private enum ComposerTab: String {
case chat
case stickers