Compare commits
No commits in common. "5f03feba66915239e4ee93eb37b43fff224ccff3" and "93c865f5ca2d9942080d7f617bb705dbb7811cdc" have entirely different histories.
5f03feba66
...
93c865f5ca
@ -3,12 +3,9 @@ import SwiftUI
|
|||||||
struct PrivateChatView: View {
|
struct PrivateChatView: View {
|
||||||
let chat: PrivateChatListItem
|
let chat: PrivateChatListItem
|
||||||
let currentUserId: String?
|
let currentUserId: String?
|
||||||
private let bottomAnchorId = "PrivateChatBottomAnchor"
|
|
||||||
|
|
||||||
@StateObject private var viewModel: PrivateChatViewModel
|
@StateObject private var viewModel: PrivateChatViewModel
|
||||||
@State private var hasPositionedToBottom: Bool = false
|
@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 draftText: String = ""
|
||||||
@State private var inputTab: ComposerTab = .chat
|
@State private var inputTab: ComposerTab = .chat
|
||||||
@State private var isVideoPreferred: Bool = false
|
@State private var isVideoPreferred: Bool = false
|
||||||
@ -22,34 +19,18 @@ struct PrivateChatView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ScrollViewReader { proxy in
|
||||||
ScrollViewReader { proxy in
|
content
|
||||||
content
|
.onChange(of: viewModel.messages.count) { _ in
|
||||||
.onChange(of: viewModel.messages.count) { _ in
|
guard !viewModel.isLoadingMore,
|
||||||
guard !viewModel.isLoadingMore else { return }
|
let lastId = viewModel.messages.last?.id else { return }
|
||||||
let targetId = viewModel.messages.last?.id ?? bottomAnchorId
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
proxy.scrollTo(lastId, anchor: .bottom)
|
||||||
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)
|
.navigationTitle(title)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@ -104,21 +85,9 @@ struct PrivateChatView: View {
|
|||||||
!viewModel.messages.isEmpty {
|
!viewModel.messages.isEmpty {
|
||||||
errorBanner(message: message)
|
errorBanner(message: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
Color.clear
|
|
||||||
.frame(height: 1)
|
|
||||||
.id(bottomAnchorId)
|
|
||||||
.onAppear { isBottomAnchorVisible = true }
|
|
||||||
.onDisappear { isBottomAnchorVisible = false }
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
}
|
}
|
||||||
.simultaneousGesture(
|
|
||||||
DragGesture().onChanged { value in
|
|
||||||
guard value.translation.height > 0 else { return }
|
|
||||||
isComposerFocused = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.refreshable {
|
.refreshable {
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
@ -244,16 +213,16 @@ struct PrivateChatView: View {
|
|||||||
Button(action: { }) { // переключатель на стикеры
|
Button(action: { }) { // переключатель на стикеры
|
||||||
Image(systemName: "paperclip")
|
Image(systemName: "paperclip")
|
||||||
.font(.system(size: 18, weight: .semibold))
|
.font(.system(size: 18, weight: .semibold))
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.buttonStyle(ComposerIconButtonStyle())
|
|
||||||
|
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
TextField(inputTab.placeholder, text: $draftText, axis: .vertical)
|
TextField(inputTab.placeholder, text: $draftText, axis: .vertical)
|
||||||
.lineLimit(1...4)
|
.lineLimit(1...4)
|
||||||
.focused($isComposerFocused)
|
.focused($isComposerFocused)
|
||||||
.submitLabel(.send)
|
.submitLabel(.send)
|
||||||
.disabled(currentUserId == nil)
|
.disabled(viewModel.isSending || currentUserId == nil)
|
||||||
.onSubmit { sendCurrentMessage() }
|
.onSubmit { sendCurrentMessage() }
|
||||||
.padding(.top, 10)
|
.padding(.top, 10)
|
||||||
.padding(.leading, 12)
|
.padding(.leading, 12)
|
||||||
@ -280,9 +249,10 @@ struct PrivateChatView: View {
|
|||||||
Button(action: { isVideoPreferred.toggle() }) {
|
Button(action: { isVideoPreferred.toggle() }) {
|
||||||
Image(systemName: isVideoPreferred ? "video.fill" : "mic.fill")
|
Image(systemName: isVideoPreferred ? "video.fill" : "mic.fill")
|
||||||
.font(.system(size: 18, weight: .semibold))
|
.font(.system(size: 18, weight: .semibold))
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.buttonStyle(ComposerIconButtonStyle())
|
.buttonStyle(.plain)
|
||||||
} else {
|
} else {
|
||||||
sendButton
|
sendButton
|
||||||
}
|
}
|
||||||
@ -294,21 +264,8 @@ struct PrivateChatView: View {
|
|||||||
.background(.ultraThinMaterial)
|
.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 {
|
private var isSendDisabled: Bool {
|
||||||
draftText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || currentUserId == nil
|
draftText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSending || currentUserId == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isSendAvailable: Bool {
|
private var isSendAvailable: Bool {
|
||||||
@ -360,35 +317,14 @@ struct PrivateChatView: View {
|
|||||||
let text = draftText.trimmingCharacters(in: .whitespacesAndNewlines)
|
let text = draftText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !text.isEmpty else { return }
|
guard !text.isEmpty else { return }
|
||||||
|
|
||||||
draftText = ""
|
|
||||||
scrollToBottomTrigger = .init()
|
|
||||||
viewModel.sendMessage(text: text) { success in
|
viewModel.sendMessage(text: text) { success in
|
||||||
if success {
|
if success {
|
||||||
|
draftText = ""
|
||||||
hasPositionedToBottom = true
|
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 {
|
private enum ComposerTab: String {
|
||||||
case chat
|
case chat
|
||||||
case stickers
|
case stickers
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user