initial commit

This commit is contained in:
Filip
2026-03-11 16:06:00 +01:00
parent b3c69053f6
commit 5fd80e6dd6
127 changed files with 39684 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
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()
}
}
}