Skip to content

Commit

Permalink
Add image sending
Browse files Browse the repository at this point in the history
  • Loading branch information
abdulajet committed Apr 30, 2024
1 parent e4ae34f commit 3d8e699
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 55 deletions.
17 changes: 13 additions & 4 deletions client-ios/Vapp/Auth/LogInView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import SwiftUI

struct LogInView: View {
@StateObject var viewModel = LogInViewModel()
@StateObject var clientManager = ClientManager.shared

var body: some View {
NavigationStack {
Expand Down Expand Up @@ -43,7 +42,7 @@ struct LogInView: View {
.alert(isPresented: $viewModel.errorContainer.hasError) {
Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text))
}
.navigationDestination(isPresented: $clientManager.isAuthed) {
.navigationDestination(isPresented: $viewModel.showHomeView) {
HomeView()
}
.navigationDestination(isPresented: $viewModel.showSignUp) {
Expand All @@ -59,20 +58,30 @@ final class LogInViewModel: ObservableObject {

@Published var isLoading = false
@Published var showSignUp = false
@Published var showHomeView = false
@Published var errorContainer = (hasError: false, text: "")

private let clientManager = ClientManager.shared

@MainActor
func logIn() async {
do {
try await ClientManager.shared.auth(username: username, password: password, path: Auth.loginPath)
try await clientManager.auth(username: username, password: password, path: Auth.loginPath)
username = ""
password = ""
if clientManager.isAuthed {
showHomeView = true
}
} catch {
errorContainer = (true, error.localizedDescription)
}
}

@MainActor
func attemptStoredLogIn() async {
await ClientManager.shared.attemptStoredLogIn()
await clientManager.attemptStoredLogIn()
if clientManager.isAuthed {
showHomeView = true
}
}
}
11 changes: 8 additions & 3 deletions client-ios/Vapp/Auth/SignUpView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import SwiftUI

struct SignUpView: View {
@StateObject var viewModel = SignUpViewModel()
@StateObject var clientManager = ClientManager.shared

var body: some View {
NavigationStack {
Expand All @@ -35,7 +34,7 @@ struct SignUpView: View {
.alert(isPresented: $viewModel.errorContainer.hasError) {
Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text))
}
.navigationDestination(isPresented: $clientManager.isAuthed) {
.navigationDestination(isPresented: $viewModel.showHomeView) {
HomeView()
}
}
Expand All @@ -48,15 +47,21 @@ final class SignUpViewModel: ObservableObject {
@Published var password = ""

@Published var isLoading = false
@Published var showHomeView = false
@Published var errorContainer = (hasError: false, text: "")

private let clientManager = ClientManager.shared

@MainActor
func signUp() async {
do {
try await ClientManager.shared.auth(username: username, password: password, displayName: displayName, path: Auth.signupPath)
try await clientManager.auth(username: username, password: password, displayName: displayName, path: Auth.signupPath)
username = ""
password = ""
displayName = ""
if clientManager.isAuthed {
showHomeView = true
}
} catch {
errorContainer = (true, error.localizedDescription)
}
Expand Down
1 change: 0 additions & 1 deletion client-ios/Vapp/CallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ struct CallView: View {
@StateObject var viewModel: CallViewModel

var body: some View {

NavigationStack {
VStack {
Text(viewModel.callStatus)
Expand Down
119 changes: 91 additions & 28 deletions client-ios/Vapp/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

import Combine
import SwiftUI
import PhotosUI
import VonageClientSDK

struct ChatView: View {
@StateObject var viewModel: ChatViewModel
@State private var message: String = ""
@State private var listTopId: Int?
@State private var selectedPhoto: PhotosPickerItem?

var body: some View {
NavigationStack {
Expand All @@ -29,10 +31,23 @@ struct ChatView: View {
let displayText = viewModel.generateDisplayText(event)
Text(displayText.body)
.frame(maxWidth: .infinity, alignment: .center)
case.messageText:
case .messageText:
let displayText = viewModel.generateDisplayText(event)
Text(displayText.body)
.frame(maxWidth: .infinity, alignment: displayText.isUser ? .trailing : .leading)
case .messageImage:
let urlString = viewModel.generateDisplayText(event)
AsyncImage(url: URL(string: urlString.body)) { phase in
if let image = phase.image {
image
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
} else {
ProgressView()
.frame(width: 200, height: 200)
}
}.frame(maxWidth: .infinity, alignment: urlString.isUser ? .trailing : .leading)
default:
EmptyView()
}
Expand All @@ -55,24 +70,40 @@ struct ChatView: View {

HStack {
TextField("Message", text: $message)
Button("Send") {
Task {
await viewModel.sendMessage(message)
self.message = ""

DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.easeInOut(duration: 1)) {
proxy.scrollTo(viewModel.events.first!.id, anchor: .bottom)
if message.isEmpty {
Button {
viewModel.showPhotoPicker = true
} label: {
Image(systemName: "photo.fill")
}.buttonStyle(.bordered)
} else {
Button {
Task {
await viewModel.sendMessage(message)
self.message = ""

DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.easeInOut(duration: 1)) {
proxy.scrollTo(viewModel.events.first!.id, anchor: .bottom)
}
}
}
}
}.buttonStyle(.bordered)
} label: {
Image(systemName: "paperplane")
}.buttonStyle(.bordered)
}
}.padding(8)
}
}
}
}
}
.photosPicker(isPresented: $viewModel.showPhotoPicker, selection: $selectedPhoto)
.task(id: selectedPhoto) {
if let selectedPhoto {
await viewModel.sendImage(selectedPhoto)
}
}
.alert(isPresented: $viewModel.errorContainer.hasError) {
Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text))
}
Expand Down Expand Up @@ -100,14 +131,14 @@ struct ChatView: View {
}

final class ChatViewModel: ObservableObject {

private var memberId: String?
private var cursor: String? = nil
private let clientManager = ClientManager.shared
private var subscriptions = Set<AnyCancellable>()

@Published var events: [VGPersistentConversationEvent] = []
@Published var showInviteUser = false
@Published var showPhotoPicker = false
@Published var isLoading = false

@Published var errorContainer = (hasError: false, text: "")
Expand Down Expand Up @@ -143,22 +174,8 @@ final class ChatViewModel: ObservableObject {
}
}

private func getEvents() async -> [VGPersistentConversationEvent] {
do {
let params = VGGetConversationEventsParameters(order: .desc, pageSize: cursorSize, cursor: cursor)
let eventsPage = try await clientManager.client.getConversationEvents(conversationId, parameters: params)
cursor = eventsPage.nextCursor
return eventsPage.events
} catch {
await MainActor.run {
errorContainer = (true, error.localizedDescription)
}
}

return []
}

func loadEarlierEvents() async {
guard cursor != nil else { return }
let earlierEvents = await getEvents()

await MainActor.run {
Expand All @@ -178,6 +195,28 @@ final class ChatViewModel: ObservableObject {
}
}

func sendImage(_ selectedItem: PhotosPickerItem) async {
do {
guard let authToken = clientManager.token else {
errorContainer = (true, "Auth Token Missing")
return
}

guard let imageData = try await selectedItem.loadTransferable(type: Data.self),
let mimeType = selectedItem.supportedContentTypes.first?.preferredMIMEType else {
errorContainer = (true, "Error Loading Image")
return
}

let uploadResponse: ImageUpload.Response = try await RemoteLoader.multipart(path: ImageUpload.chatPath, mimeType: mimeType, authToken: authToken, data: imageData)

_ = try await clientManager.client.sendMessageImageEvent(conversationId, imageUrl: URL(string: uploadResponse.imageURL)!)

} catch {
errorContainer = (true, error.localizedDescription)
}
}

func generateDisplayText(_ event: VGPersistentConversationEvent) -> (body: String, isUser: Bool) {
var from = "System"

Expand All @@ -204,17 +243,41 @@ final class ChatViewModel: ObservableObject {
}

return ("\(from) \(messageTextEvent.body.text)", isUser)
case .messageImage:
let messageImageEvent = event as! VGMessageImageEvent
var isUser = false
if let userInfo = messageImageEvent.from as? VGEmbeddedInfo {
isUser = userInfo.memberId == memberId
from = isUser ? "" : "\(userInfo.user.name): "
}

return (messageImageEvent.body.imageUrl, isUser)
default:
return ("", false)
}
}

// MARK: - Private

private func getEvents() async -> [VGPersistentConversationEvent] {
do {
let params = VGGetConversationEventsParameters(order: .desc, pageSize: cursorSize, cursor: cursor)
let eventsPage = try await clientManager.client.getConversationEvents(conversationId, parameters: params)
cursor = eventsPage.nextCursor
return eventsPage.events
} catch {
await MainActor.run {
errorContainer = (true, error.localizedDescription)
}
}

return []
}

private func getMemberId() async {
do {
let member = try await clientManager.client.getConversationMember(conversationId, memberId: "me")

if member.state == .joined {
memberId = member.id
return
Expand Down
26 changes: 10 additions & 16 deletions client-ios/Vapp/Helpers/ClientManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class ClientManager: NSObject, ObservableObject {
var users: [Users.User] = []

// Chat Publisher
private var handledEventKinds: Set<VGEventKind> = [.memberInvited, .memberJoined, .memberLeft, .messageText]
private var handledEventKinds: Set<VGEventKind> = [.memberInvited, .memberJoined, .memberLeft, .messageText, .messageImage]
private let messageSubject = PassthroughSubject<VGConversationEvent, Never>()
public var onEvent: AnyPublisher<VGConversationEvent, Never> {
messageSubject
Expand Down Expand Up @@ -55,17 +55,15 @@ final class ClientManager: NSObject, ObservableObject {

// MARK: - Public

public func auth(username: String, password: String, displayName: String? = nil, path: String, shouldStoreCredentials: Bool = true) async throws {
public func auth(username: String, password: String, displayName: String? = nil, path: String) async throws {
let body = Auth.Body(name: username, password: password, displayName: displayName)

let authResponse: Auth.Response = try await RemoteLoader.post(path: path, body: body)
self.token = authResponse.token
self.user = authResponse.user
try await client?.createSession(authResponse.token)

if shouldStoreCredentials {
storeCredentials(username: username, password: password)
}
storeCredentials(username: username, password: password)

await MainActor.run {
isAuthed = true
Expand All @@ -75,17 +73,13 @@ final class ClientManager: NSObject, ObservableObject {

public func attemptStoredLogIn() async {
if let credentials = getCredentials() {
try? await auth(username: credentials.0, password: credentials.1, path: Auth.loginPath, shouldStoreCredentials: false)
try? await auth(username: credentials.0, password: credentials.1, path: Auth.loginPath)
}
}

public func logout() async throws {
if let username = user?.name {
try await client.deleteSession()
deleteCredentials(username: username)
} else {
// TODO: throw error
}
try await client.deleteSession()
deleteCredentials()
}

// MARK: - Private
Expand Down Expand Up @@ -145,6 +139,7 @@ extension ClientManager: VGClientDelegate {

extension ClientManager {
private func storeCredentials(username: String, password: String) {
deleteCredentials()
if let passwordData = password.data(using: .utf8) {
let keychainItem = [
kSecClass: kSecClassInternetPassword,
Expand Down Expand Up @@ -183,13 +178,12 @@ extension ClientManager {
}
}

private func deleteCredentials(username: String) {
private func deleteCredentials() {
let query = [
kSecClass: kSecClassInternetPassword,
kSecAttrServer: Constants.keychainServer,
kSecAttrAccount: username
kSecClass: kSecClassInternetPassword
] as CFDictionary

SecItemDelete(query)
print("Keychain deleting attempted")
}
}
6 changes: 3 additions & 3 deletions client-ios/Vapp/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ import PhotosUI
struct SettingsView: View {
@ObservedObject var homeViewModel: HomeViewModel
@ObservedObject var viewModel = SettingsViewModel()
@Environment(\.dismiss) var dismiss
@Environment(\.dismiss) private var dismiss

@State var selectedPhoto: PhotosPickerItem?
@State private var selectedPhoto: PhotosPickerItem?

var body: some View {
NavigationStack {
VStack {
if viewModel.isLoading {
ProgressView()
} else {
PhotosPicker(selection: $selectedPhoto) {
PhotosPicker(selection: $selectedPhoto, matching: .images) {
ZStack {
if let url = viewModel.imageURL {
AsyncImage(url: url) { phase in
Expand Down
Loading

0 comments on commit 3d8e699

Please sign in to comment.