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