Skip to content

Commit

Permalink
Communication: Add Profile Info View (#177)
Browse files Browse the repository at this point in the history
  • Loading branch information
anian03 authored Oct 5, 2024
1 parent ebf3027 commit c2515e0
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ls1intum/artemis-ios-core-modules",
"state" : {
"revision" : "e10992600af821dab51ef29b5f28db7ae457c78c",
"version" : "14.4.0"
"revision" : "c5ad034195a126f616ed7e5e6b71a57472a120d4",
"version" : "14.5.0"
}
},
{
Expand Down Expand Up @@ -77,8 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift",
"state" : {
"revision" : "7cbd73f46a7dfaeca079e18df7324c6de6d1834a",
"version" : "5.2.3"
"revision" : "21d1dc412cfecbe6e34f1f4c4eb88d3f912654a6",
"version" : "5.2.4"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion ArtemisKit/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/Kelvas09/EmojiPicker.git", from: "1.0.0"),
.package(url: "https://github.com/ls1intum/apollon-ios-module", .upToNextMajor(from: "1.0.2")),
.package(url: "https://github.com/ls1intum/artemis-ios-core-modules", .upToNextMajor(from: "14.4.0")),
.package(url: "https://github.com/ls1intum/artemis-ios-core-modules", .upToNextMajor(from: "14.5.0")),
.package(url: "https://github.com/mac-cain13/R.swift.git", from: "7.0.0")
],
targets: [
Expand Down
3 changes: 3 additions & 0 deletions ArtemisKit/Sources/ArtemisKit/RootViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ private extension RootViewModel {
userSession.setTokenExpired(expired: false)
case .done:
isLoggedIn = userSession.isLoggedIn
if isLoggedIn, let user = user.value {
userSession.user = user
}
didSetupNotifications = userSession.getCurrentNotificationDeviceConfiguration() != nil
}
isLoading = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
"pinMessage" = "Pin Message";
"unpinMessage" = "Unpin Message";

// MARK: ProfileView
"profile" = "Profile";
"actions" = "Actions";
"name" = "Name";

// MARK: ConversationView
"noMessages" = "No Messages";
"noMessagesDescription" = "Write the first message to kickstart this conversation.";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// ProfileViewModel.swift
// ArtemisKit
//
// Created by Anian Schleyer on 23.09.24.
//

import Common
import Foundation
import Navigation
import SharedModels
import SharedServices
import UserStore

@Observable
class ProfileViewModel {
var error: UserFacingError?
var isLoading = false
var showProfileSheet = false

let user: ConversationUser
let role: UserRole?
private var login: String?
let course: Course

init(course: Course, user: ConversationUser, role: UserRole?) {
self.course = course
self.user = user
self.role = role
}

// We can only create a conversation if we have the user's login
// Don't allow sending messages to oneself
var canSendMessage: Bool {
(user.login != nil || login != nil)
&& UserSessionFactory.shared.user?.id != user.id
}

func loadUserLogin() async {
guard let name = user.name else { return }
isLoading = true
let result = await CourseServiceFactory.shared.getCourseMembers(courseId: course.id, searchLoginOrName: name)
switch result {
case .done(let matches):
login = matches.first(where: { $0.name == user.name })?.login
isLoading = false
default:
// No user found – cannot send a message
break
}
}

@MainActor
func openConversation(navigationController: NavigationController, completion: @escaping () -> Void) {
guard let login = user.login ?? login else { return }
isLoading = true
Task {
let messageCellModel = MessageCellModel(course: course, conversationPath: nil, isHeaderVisible: false, roundBottomCorners: false, retryButtonAction: {})
if let conversation = await messageCellModel.getOneToOneChatOrCreate(login: login) {
navigationController.goToCourseConversation(courseId: course.id, conversation: conversation)
completion()
}
isLoading = false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ private extension MessageCell {
if viewModel.isHeaderVisible {
HStack(alignment: .top, spacing: .m) {
if let authorUser {
ProfilePictureView(user: authorUser)
ProfilePictureView(user: authorUser, role: authorRole, course: viewModel.course)
}
VStack(alignment: .leading, spacing: .xs) {
HStack(alignment: .firstTextBaseline, spacing: .m) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,55 @@
//

import DesignLibrary
import Navigation
import SharedModels
import SwiftUI

struct ProfilePictureView: View {
let user: ConversationUser
@State private var viewModel: ProfileViewModel

init(user: ConversationUser, role: UserRole?, course: Course) {
self._viewModel = State(initialValue: ProfileViewModel(course: course, user: user, role: role))
}

var body: some View {
Group {
if let url = user.imagePath {
Button {
viewModel.showProfileSheet = true
} label: {
if let url = viewModel.user.imagePath {
ArtemisAsyncImage(imageURL: url) {
defaultProfilePicture
DefaultProfilePictureView(viewModel: viewModel)
}
} else {
defaultProfilePicture
DefaultProfilePictureView(viewModel: viewModel)
}
}
.frame(width: 44, height: 44)
.clipShape(.rect(cornerRadius: .m))
.sheet(isPresented: $viewModel.showProfileSheet) {
ProfileInfoSheet(viewModel: viewModel)
}
}
}

private struct DefaultProfilePictureView: View {
let viewModel: ProfileViewModel
var font: Font = .headline.bold()

@ViewBuilder var defaultProfilePicture: some View {
var body: some View {
ZStack {
Rectangle()
.fill(backgroundColor)
Text(initials)
.font(.headline.bold())
.font(font)
.fontDesign(.rounded)
.foregroundStyle(.white)
}
.accessibilityHidden(true)
}

private var initials: String {
let nameComponents = user.name?.split(separator: " ")
let nameComponents = viewModel.user.name?.split(separator: " ")
let initialFirstName = nameComponents?.first?.prefix(1) ?? ""
let initialLastName = nameComponents?.last?.prefix(1) ?? ""
let initials = initialFirstName + initialLastName
Expand All @@ -51,7 +66,102 @@ struct ProfilePictureView: View {
}

private var backgroundColor: Color {
let hash = abs(String(user.id).hashValue) % 255
let hash = abs(String(viewModel.user.id).hashValue) % 255
return Color(hue: Double(hash) / 255, saturation: 0.5, brightness: 0.5)
}
}

struct ProfileInfoSheet: View {
@EnvironmentObject var navController: NavigationController
@Environment(\.dismiss) var dismiss
let viewModel: ProfileViewModel

var body: some View {
@Bindable var viewModel = viewModel
NavigationStack {
List {
Group {
if let name = viewModel.user.name {
HStack(alignment: .center, spacing: .l) {
Group {
if let profileUrl = viewModel.user.imagePath {
ArtemisAsyncImage(imageURL: profileUrl) {}
} else {
DefaultProfilePictureView(viewModel: viewModel, font: .largeTitle)
}
}
.frame(width: 100, height: 100)
.clipShape(.rect(cornerRadius: 8))

VStack(alignment: .leading, spacing: .m) {
if let role = viewModel.role {
Chip(text: role.displayName,
backgroundColor: role.badgeColor,
horizontalPadding: .m,
verticalPadding: .s)
.font(.body)
// For visual alignment
.padding(.top, .m)
}

Text(name)
.font(.title)
.multilineTextAlignment(.leading)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
}

if viewModel.canSendMessage {
Section(R.string.localizable.actions()) {
Button(R.string.localizable.sendMessage(), systemImage: "bubble.fill") {
viewModel.openConversation(navigationController: navController) {
dismiss()
}
}
}
}
}
.listRowBackground(
Spacer()
.background(.primary.opacity(0.15))
.background(.thinMaterial)
)
}
.navigationTitle(R.string.localizable.profile())
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(R.string.localizable.done()) {
dismiss()
}
}
}
.scrollContentBackground(.hidden)
.background(backgroundImage)
}
.task {
await viewModel.loadUserLogin()
}
.loadingIndicator(isLoading: $viewModel.isLoading)
.alert(isPresented: Binding(get: {
viewModel.error != nil
}, set: { newValue in
if !newValue {
viewModel.error = nil
}
}), error: viewModel.error, actions: {})
}

@ViewBuilder private var backgroundImage: some View {
if let profileUrl = viewModel.user.imagePath {
ArtemisAsyncImage(imageURL: profileUrl) {}
.scaledToFill()
.ignoresSafeArea()
.blur(radius: .l, opaque: true)
.opacity(0.15)
}
}
}

0 comments on commit c2515e0

Please sign in to comment.