ios_client
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ConversationListView: View {
|
||||
var appState: AppState
|
||||
@Bindable var viewModel: ConversationListVM
|
||||
@State private var showNewConversation = false
|
||||
@State private var showProfile = false
|
||||
@State private var selectedConversation: Conversation?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
// Invitations section
|
||||
if !viewModel.invitations.isEmpty {
|
||||
Section {
|
||||
ForEach(viewModel.invitations) { invitation in
|
||||
InvitationBanner(
|
||||
invitation: invitation,
|
||||
onAccept: {
|
||||
Task {
|
||||
let (success, _) = await appState.chatClient.acceptInvitation(convId: invitation.conversationId)
|
||||
if success {
|
||||
await viewModel.refresh(chatClient: appState.chatClient)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDecline: {
|
||||
Task {
|
||||
_ = await appState.chatClient.declineInvitation(convId: invitation.conversationId)
|
||||
await viewModel.refresh(chatClient: appState.chatClient)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} header: {
|
||||
Text("Invitations")
|
||||
}
|
||||
}
|
||||
|
||||
// Conversations section
|
||||
Section {
|
||||
ForEach(viewModel.conversations) { conversation in
|
||||
NavigationLink(value: conversation) {
|
||||
ConversationRowView(
|
||||
conversation: conversation,
|
||||
currentUserId: appState.currentUser?.id ?? "",
|
||||
isOnline: conversation.dmPartnerId(currentUserId: appState.currentUser?.id ?? "")
|
||||
.map { viewModel.onlineUsers.contains($0) } ?? false,
|
||||
unreadCount: viewModel.unreadCounts[conversation.id] ?? 0,
|
||||
avatarData: viewModel.avatarCache[conversation.id]
|
||||
)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(conversation.isFavorite ? "Remove from Favorites" : "Add to Favorites") {
|
||||
viewModel.toggleFavorite(convId: conversation.id, email: appState.email)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Chats")
|
||||
.navigationDestination(for: Conversation.self) { conversation in
|
||||
ChatView(
|
||||
conversation: conversation,
|
||||
appState: appState,
|
||||
conversationListVM: viewModel
|
||||
)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
ConnectionIndicator(status: appState.connectionStatus)
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
HStack {
|
||||
Button(action: { showProfile = true }) {
|
||||
Image(systemName: "person.circle")
|
||||
}
|
||||
Button(action: { showNewConversation = true }) {
|
||||
Image(systemName: "square.and.pencil")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh(chatClient: appState.chatClient)
|
||||
}
|
||||
.sheet(isPresented: $showNewConversation) {
|
||||
NewConversationSheet(appState: appState) { convId in
|
||||
showNewConversation = false
|
||||
await viewModel.refresh(chatClient: appState.chatClient)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showProfile) {
|
||||
ProfileView(appState: appState, isOwnProfile: true)
|
||||
}
|
||||
.task {
|
||||
await viewModel.load(chatClient: appState.chatClient, email: appState.email)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ConversationRowView: View {
|
||||
let conversation: Conversation
|
||||
let currentUserId: String
|
||||
let isOnline: Bool
|
||||
let unreadCount: Int
|
||||
var avatarData: Data?
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
// Avatar
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
CircularAvatarView(
|
||||
name: conversation.displayName(currentUserId: currentUserId),
|
||||
imageData: avatarData,
|
||||
size: 44,
|
||||
isGroup: conversation.isGroup
|
||||
)
|
||||
|
||||
if isOnline && !conversation.isGroup {
|
||||
OnlineDotOverlay(size: 12)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack {
|
||||
if conversation.isFavorite {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.yellow)
|
||||
}
|
||||
|
||||
Text(conversation.displayName(currentUserId: currentUserId))
|
||||
.font(unreadCount > 0 ? .body.bold() : .body)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
if conversation.isGroup {
|
||||
Text("\(conversation.members.count) members")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if unreadCount > 0 {
|
||||
Text("\(unreadCount)")
|
||||
.font(.caption2.bold())
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.blue)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import SwiftUI
|
||||
|
||||
struct NewConversationSheet: View {
|
||||
var appState: AppState
|
||||
var onCreated: (String) async -> Void
|
||||
|
||||
@State private var email = ""
|
||||
@State private var groupName = ""
|
||||
@State private var isGroup = false
|
||||
@State private var memberEmails: [String] = [""]
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
Toggle("Create Group", isOn: $isGroup)
|
||||
|
||||
if isGroup {
|
||||
TextField("Group Name", text: $groupName)
|
||||
}
|
||||
}
|
||||
|
||||
Section(isGroup ? "Members" : "Recipient") {
|
||||
if isGroup {
|
||||
ForEach(memberEmails.indices, id: \.self) { index in
|
||||
TextField("Email", text: $memberEmails[index])
|
||||
.textContentType(.emailAddress)
|
||||
.keyboardType(.emailAddress)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
Button("Add Member") {
|
||||
memberEmails.append("")
|
||||
}
|
||||
} else {
|
||||
TextField("Email", text: $email)
|
||||
.textContentType(.emailAddress)
|
||||
.keyboardType(.emailAddress)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
}
|
||||
|
||||
if let error = errorMessage {
|
||||
Section {
|
||||
Text(error)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("New Conversation")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") { dismiss() }
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Create") {
|
||||
Task { await create() }
|
||||
}
|
||||
.disabled(isLoading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func create() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
let emails: [String]
|
||||
if isGroup {
|
||||
emails = memberEmails.map { $0.trimmed }.filter { !$0.isEmpty }
|
||||
guard !emails.isEmpty else {
|
||||
errorMessage = "Add at least one member"
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
guard !email.trimmed.isEmpty else {
|
||||
errorMessage = "Enter an email address"
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
emails = [email.trimmed]
|
||||
}
|
||||
|
||||
let name = isGroup && !groupName.trimmed.isEmpty ? groupName.trimmed : nil
|
||||
let (convId, message) = await appState.chatClient.createConversation(emails: emails, name: name)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if let convId = convId {
|
||||
await onCreated(convId)
|
||||
} else {
|
||||
errorMessage = message
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user