165 lines
6.6 KiB
Swift
165 lines
6.6 KiB
Swift
import SwiftUI
|
|
|
|
struct ChatView: View {
|
|
let conversation: Conversation
|
|
var appState: AppState
|
|
@State private var viewModel = ChatViewModel()
|
|
@State private var inputText = ""
|
|
@State private var replyTo: Message?
|
|
@State private var showGroupInfo = false
|
|
@State private var showSearch = false
|
|
@State private var showDeleteConfirm = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Search bar
|
|
if showSearch {
|
|
SearchOverlayView(
|
|
query: $viewModel.searchQuery,
|
|
matchCount: viewModel.searchResults.count,
|
|
currentIndex: viewModel.currentSearchIndex,
|
|
onSearch: { viewModel.search(query: $0) },
|
|
onNext: { viewModel.nextSearchResult() },
|
|
onPrev: { viewModel.prevSearchResult() },
|
|
onClose: { showSearch = false; viewModel.search(query: "") }
|
|
)
|
|
}
|
|
|
|
// Messages
|
|
ScrollViewReader { proxy in
|
|
ScrollView {
|
|
LazyVStack(spacing: 8) {
|
|
if viewModel.messages.count >= 50 {
|
|
Button("Load older messages") {
|
|
Task {
|
|
await viewModel.loadOlderMessages(convId: conversation.id, chatClient: appState.chatClient)
|
|
}
|
|
}
|
|
.font(.caption)
|
|
.padding()
|
|
}
|
|
|
|
ForEach(viewModel.messages) { message in
|
|
MessageBubbleView(
|
|
message: message,
|
|
isMine: message.isMine(currentUserId: appState.currentUser?.id ?? ""),
|
|
isHighlighted: viewModel.searchResults.contains(message.id),
|
|
isCurrentSearchResult: viewModel.searchResults.indices.contains(viewModel.currentSearchIndex) &&
|
|
viewModel.searchResults[viewModel.currentSearchIndex] == message.id,
|
|
onReply: { replyTo = message },
|
|
onDelete: {
|
|
Task {
|
|
await viewModel.deleteMessage(messageId: message.id, convId: conversation.id, chatClient: appState.chatClient)
|
|
}
|
|
}
|
|
)
|
|
.id(message.id)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
}
|
|
.onChange(of: viewModel.messages.count) {
|
|
if let lastId = viewModel.messages.last?.id {
|
|
withAnimation {
|
|
proxy.scrollTo(lastId, anchor: .bottom)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reply preview
|
|
if let reply = replyTo {
|
|
HStack {
|
|
Rectangle()
|
|
.fill(.blue)
|
|
.frame(width: 3)
|
|
VStack(alignment: .leading) {
|
|
Text(reply.senderUsername)
|
|
.font(.caption.bold())
|
|
Text(reply.text ?? "")
|
|
.font(.caption)
|
|
.lineLimit(1)
|
|
}
|
|
Spacer()
|
|
Button(action: { replyTo = nil }) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 6)
|
|
.background(.ultraThinMaterial)
|
|
}
|
|
|
|
// Input
|
|
MessageInputView(
|
|
text: $inputText,
|
|
isSending: viewModel.isSending,
|
|
onSend: {
|
|
Task {
|
|
let text = inputText
|
|
inputText = ""
|
|
let reply = replyTo?.id
|
|
replyTo = nil
|
|
await viewModel.sendMessage(
|
|
convId: conversation.id,
|
|
text: text,
|
|
members: conversation.members,
|
|
chatClient: appState.chatClient,
|
|
replyTo: reply
|
|
)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
.navigationTitle(conversation.displayName(currentUserId: appState.currentUser?.id ?? ""))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
HStack(spacing: 16) {
|
|
Button(action: { showSearch.toggle() }) {
|
|
Image(systemName: "magnifyingglass")
|
|
}
|
|
|
|
if conversation.isGroup {
|
|
Button(action: { showGroupInfo = true }) {
|
|
Image(systemName: "info.circle")
|
|
}
|
|
}
|
|
|
|
// Delete button
|
|
if !conversation.isGroup || conversation.createdBy == appState.currentUser?.id {
|
|
Button(action: { showDeleteConfirm = true }) {
|
|
Image(systemName: "trash")
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.alert("Delete Conversation?", isPresented: $showDeleteConfirm) {
|
|
Button("Cancel", role: .cancel) {}
|
|
Button("Delete", role: .destructive) {
|
|
Task {
|
|
await appState.chatClient.deleteConversation(convId: conversation.id)
|
|
}
|
|
}
|
|
} message: {
|
|
Text(conversation.isGroup
|
|
? "This will remove all members and delete the conversation."
|
|
: "This will remove you from the conversation.")
|
|
}
|
|
.sheet(isPresented: $showGroupInfo) {
|
|
GroupInfoView(conversation: conversation, appState: appState)
|
|
}
|
|
.task {
|
|
await viewModel.loadMessages(convId: conversation.id, chatClient: appState.chatClient)
|
|
viewModel.startNotificationListener(convId: conversation.id, chatClient: appState.chatClient)
|
|
}
|
|
.onDisappear {
|
|
viewModel.stop()
|
|
}
|
|
}
|
|
}
|