Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion Modules/Sources/APIClient/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ public struct APIClient: Sendable {

// User
public var getUser: @Sendable (_ userId: Int, _ policy: CachePolicy) async throws -> AsyncThrowingStream<User, any Error>
public var editUserProfile: @Sendable (_ request: UserProfileEditRequest) async throws -> Bool
public var getReputationVotes: @Sendable (_ data: ReputationVotesRequest) async throws -> ReputationVotes
public var changeReputation: @Sendable (_ data: ReputationChangeRequest) async throws -> ReputationChangeResponseType
public var updateUserAvatar: @Sendable (_ userId: Int, _ image: Data) async throws -> UserAvatarResponseType
public var updateUserDevice: @Sendable (_ userId: Int, _ action: UserDeviceAction, _ fullTag: String, _ isPrimary: Bool) async throws -> Bool

// Bookmarks
public var getBookmarksList: @Sendable () async throws -> [Bookmark]
Expand Down Expand Up @@ -203,7 +206,22 @@ extension APIClient: DependencyKey {
policy: policy
)
},

editUserProfile: { request in
let command = MemberCommand.profile(
data: MemberProfileRequest(
memberId: request.userId,
city: request.city,
gender: request.transferGender,
status: request.status,
about: request.about,
signature: request.signature,
birthday: request.birthdayDate
)
)
let response = try await api.send(command)
let status = Int(response.getResponseStatus())!
return status == 0
},
getReputationVotes: { request in
let command = MemberCommand.reputationVotes(data: MemberReputationVotesRequest(
memberId: request.userId,
Expand All @@ -226,8 +244,30 @@ extension APIClient: DependencyKey {
let status = Int(response.getResponseStatus())!
return ReputationChangeResponseType(rawValue: status)
},
updateUserAvatar: { userId, image in
let command = MemberCommand.avatar(memberId: userId, avatar: image)
let response = try await api.send(command)
return try await parser.parseAvatarUrl(response: response)
},
updateUserDevice: { userId, action, fullTag, isPrimary in
let action: MemberDeviceRequest.Action = switch action {
case .add: .add
case .modify: .modify
case .remove: .remove
}
let command = MemberCommand.device(data: MemberDeviceRequest(
memberId: userId,
action: action,
fullTag: fullTag,
primary: isPrimary
))
let response = try await api.send(command)
let status = Int(response.getResponseStatus())!
return status == 0
},

// MARK: - Bookmarks

getBookmarksList: {
let response = try await api.send(MemberCommand.Bookmarks.list)
return try await parser.parseBookmarksList(response)
Expand Down Expand Up @@ -519,12 +559,21 @@ extension APIClient: DependencyKey {
getUser: { _, _ in
AsyncThrowingStream { $0.yield(.mock) }
},
editUserProfile: { _ in
return true
},
getReputationVotes: { _ in
return .mock
},
changeReputation: { _ in
return .success
},
updateUserAvatar: { _, _ in
return .success(URL(string: "https://github.com/SubvertDev/ForPDA/raw/main/Images/logo.png")!)
},
updateUserDevice: { _, _, _, _ in
return true
},
getBookmarksList: {
return [.mockArticle, .mockForum, .mockUser]
},
Expand Down
48 changes: 48 additions & 0 deletions Modules/Sources/APIClient/Requests/UserProfileEditRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// UserProfileEditRequest.swift
// ForPDA
//
// Created by Xialtal on 2.09.25.
//

import Foundation
import Models
import PDAPI

public struct UserProfileEditRequest: Sendable {
public let userId: Int
public let city: String
public let about: String
public let gender: User.Gender
public let status: String
public let signature: String
public let birthdayDate: Date?

public init(
userId: Int,
city: String,
about: String,
gender: User.Gender,
status: String,
signature: String,
birthdayDate: Date?
) {
self.userId = userId
self.city = city
self.about = about
self.gender = gender
self.status = status
self.signature = signature
self.birthdayDate = birthdayDate
}
}

extension UserProfileEditRequest {
var transferGender: MemberProfileRequest.Gender {
switch gender {
case .male: return .male
case .female: return .female
case .unknown: return .unknown
}
}
}
1 change: 1 addition & 0 deletions Modules/Sources/AnalyticsClient/Events/ProfileEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation

public enum ProfileEvent: Event {
case qmsTapped
case editTapped
case settingsTapped
case logoutTapped
case historyTapped
Expand Down
55 changes: 34 additions & 21 deletions Modules/Sources/Models/Profile/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@
//

import Foundation
import SwiftUI

public struct User: Sendable, Hashable, Codable {
public let id: Int
public let nickname: String
public let imageUrl: URL
public let group: Group
public let status: String?
public let signature: String?
public let aboutMe: String?
public var imageUrl: URL
public var group: Group
public var status: String?
public var signature: String?
public var aboutMe: String?
public let registrationDate: Date
public let lastSeenDate: Date
public let birthdate: String?
public let gender: Gender?
public var gender: Gender?
public let userTime: Int?
public let city: String?
public let devDBdevices: [Device]
public var city: String?
public var devDBdevices: [Device]
public let karma: Double
public let posts: Int
public let comments: Int
Expand All @@ -33,6 +34,16 @@ public struct User: Sendable, Hashable, Codable {
public let email: String?
public let achievements: [Achievement]

public var birthdayDate: Date? {
if let birthdate {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd.MM.yyyy"
return dateFormatter.date(from: birthdate)
} else {
return nil
}
}

public var userTimeFormatted: String? {
if let userTime {
let currentDate = Date()
Expand Down Expand Up @@ -195,29 +206,20 @@ public extension User {

// MARK: Gender

enum Gender: Int, Codable, Hashable, Sendable {
enum Gender: Int, CaseIterable, Codable, Hashable, Sendable, Identifiable {
case unknown = 0
case male
case female

public var title: String {
switch self {
case .unknown:
"Неизвестно"
case .male:
"Мужчина"
case .female:
"Женщина"
}
}
public var id: Self { self }
}

// MARK: Device

struct Device: Codable, Hashable, Sendable, Identifiable {
public let id: String
public let name: String
public let main: Bool
public var main: Bool

public init(id: String, name: String, main: Bool) {
self.id = id
Expand Down Expand Up @@ -274,7 +276,18 @@ public extension User {
gender: .male,
userTime: 10800,
city: "Moscow",
devDBdevices: [],
devDBdevices: [
.init(
id: "ip16pro",
name: "iPhone 16 Pro",
main: true
),
.init(
id: "ip13",
name: "iPhone 13",
main: false
)
],
karma: 1500,
posts: 23,
comments: 173,
Expand Down
13 changes: 13 additions & 0 deletions Modules/Sources/Models/Profile/UserAvatarResponseType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// UserAvatarResponseType.swift
// ForPDA
//
// Created by Xialtal on 29.08.25.
//

import Foundation

public enum UserAvatarResponseType: Sendable {
case error
case success(URL?)
}
12 changes: 12 additions & 0 deletions Modules/Sources/Models/Profile/UserDeviceAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// UserDeviceAction.swift
// ForPDA
//
// Created by Xialtal on 29.08.25.
//

public enum UserDeviceAction: Sendable {
case add
case modify
case remove
}
20 changes: 20 additions & 0 deletions Modules/Sources/ParsingClient/Parsers/ProfileParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ public struct ProfileParser {
}
}

public static func parseAvatarUrl(from string: String) throws -> UserAvatarResponseType {
if let data = string.data(using: .utf8) {
do {
guard let array = try JSONSerialization.jsonObject(with: data, options: []) as? [Any] else { throw ParsingError.failedToCastDataToAny }
let status = array[1] as! Int

if status == 0 {
let stringUrl = if array.count > 2 { array[2] as! String } else { "" }
return .success(URL(string: stringUrl))
} else {
return .error
}
} catch {
throw ParsingError.failedToSerializeData(error)
}
} else {
throw ParsingError.failedToCreateDataFromString
}
}

private static func parseUserDevDBDevices(_ array: [[Any]]) -> [User.Device] {
return array.map { device in
return User.Device(
Expand Down
4 changes: 4 additions & 0 deletions Modules/Sources/ParsingClient/ParsingClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public struct ParsingClient: Sendable {
// User
public var parseUser: @Sendable (_ response: String) async throws -> User
public var parseReputationVotes: @Sendable ( _ response: String) async throws -> ReputationVotes
public var parseAvatarUrl: @Sendable (_ response: String) async throws -> UserAvatarResponseType

// Bookmarks
public var parseBookmarksList: @Sendable (_ response: String) async throws -> [Bookmark]
Expand Down Expand Up @@ -80,6 +81,9 @@ extension ParsingClient: DependencyKey {
parseReputationVotes: { response in
return try ReputationParser.parse(from: response)
},
parseAvatarUrl: { response in
return try ProfileParser.parseAvatarUrl(from: response)
},
parseBookmarksList: { response in
return try BookmarksParser.parse(from: response)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ extension ProfileFeature {
case .view(.qmsButtonTapped):
analyticsClient.log(ProfileEvent.qmsTapped)

case .view(.editButtonTapped):
analyticsClient.log(ProfileEvent.editTapped)

case .view(.settingsButtonTapped):
analyticsClient.log(ProfileEvent.settingsTapped)

Expand Down
Loading
Loading