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
119 changes: 8 additions & 111 deletions GdeiAssistant-iOS.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions GdeiAssistant-iOS/Features/Express/Models/ExpressModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,32 @@ struct ExpressDraft: Codable {
let content: String
let targetGender: ExpressGender
}

extension ExpressPost {
/// A copy with only the given counters changed. Both the mock repository and the view
/// model apply the same optimistic updates, so adding a field must not require editing
/// six full memberwise constructions.
func with(
likeCount: Int? = nil,
commentCount: Int? = nil,
guessCount: Int? = nil,
correctGuessCount: Int? = nil,
isLiked: Bool? = nil
) -> ExpressPost {
ExpressPost(
id: id,
nickname: nickname,
targetName: targetName,
contentPreview: contentPreview,
publishTime: publishTime,
likeCount: likeCount ?? self.likeCount,
commentCount: commentCount ?? self.commentCount,
guessCount: guessCount ?? self.guessCount,
correctGuessCount: correctGuessCount ?? self.correctGuessCount,
isLiked: isLiked ?? self.isLiked,
canGuess: canGuess,
selfGender: selfGender,
targetGender: targetGender
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,63 +102,22 @@ final class MockExpressRepository: ExpressRepository {
comments[postID, default: []].insert(item, at: 0)
if let index = posts.firstIndex(where: { $0.id == postID }) {
let post = posts[index]
posts[index] = ExpressPost(
id: post.id,
nickname: post.nickname,
targetName: post.targetName,
contentPreview: post.contentPreview,
publishTime: post.publishTime,
likeCount: post.likeCount,
commentCount: post.commentCount + 1,
guessCount: post.guessCount,
correctGuessCount: post.correctGuessCount,
isLiked: post.isLiked,
canGuess: post.canGuess,
selfGender: post.selfGender,
targetGender: post.targetGender
)
posts[index] = post.with(commentCount: post.commentCount + 1)
}
}

func like(postID: String) async throws {
guard let index = posts.firstIndex(where: { $0.id == postID }) else { return }
let post = posts[index]
posts[index] = ExpressPost(
id: post.id,
nickname: post.nickname,
targetName: post.targetName,
contentPreview: post.contentPreview,
publishTime: post.publishTime,
likeCount: post.likeCount + (post.isLiked ? 0 : 1),
commentCount: post.commentCount,
guessCount: post.guessCount,
correctGuessCount: post.correctGuessCount,
isLiked: true,
canGuess: post.canGuess,
selfGender: post.selfGender,
targetGender: post.targetGender
)
posts[index] = post.with(likeCount: post.likeCount + (post.isLiked ? 0 : 1), isLiked: true)
}

func guess(postID: String, name: String) async throws -> Bool {
let matched = name.trimmingCharacters(in: .whitespacesAndNewlines) == localizedString("mock.express.detail.realName")
if let index = posts.firstIndex(where: { $0.id == postID }) {
let post = posts[index]
posts[index] = ExpressPost(
id: post.id,
nickname: post.nickname,
targetName: post.targetName,
contentPreview: post.contentPreview,
publishTime: post.publishTime,
likeCount: post.likeCount,
commentCount: post.commentCount,
guessCount: post.guessCount + 1,
correctGuessCount: post.correctGuessCount + (matched ? 1 : 0),
isLiked: post.isLiked,
canGuess: post.canGuess,
selfGender: post.selfGender,
targetGender: post.targetGender
)
posts[index] = post.with(guessCount: post.guessCount + 1,
correctGuessCount: post.correctGuessCount + (matched ? 1 : 0))
}
return matched
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,64 +46,23 @@ final class ExpressViewModel: ObservableObject {
func submitComment(postID: String, content: String) async throws {
try await repository.submitComment(postID: postID, content: content)
updatePost(postID: postID) { post in
ExpressPost(
id: post.id,
nickname: post.nickname,
targetName: post.targetName,
contentPreview: post.contentPreview,
publishTime: post.publishTime,
likeCount: post.likeCount,
commentCount: post.commentCount + 1,
guessCount: post.guessCount,
correctGuessCount: post.correctGuessCount,
isLiked: post.isLiked,
canGuess: post.canGuess,
selfGender: post.selfGender,
targetGender: post.targetGender
)
post.with(commentCount: post.commentCount + 1)
}
}

func like(postID: String) async throws {
try await repository.like(postID: postID)
if let index = posts.firstIndex(where: { $0.id == postID }) {
let post = posts[index]
posts[index] = ExpressPost(
id: post.id,
nickname: post.nickname,
targetName: post.targetName,
contentPreview: post.contentPreview,
publishTime: post.publishTime,
likeCount: post.likeCount + (post.isLiked ? 0 : 1),
commentCount: post.commentCount,
guessCount: post.guessCount,
correctGuessCount: post.correctGuessCount,
isLiked: true,
canGuess: post.canGuess,
selfGender: post.selfGender,
targetGender: post.targetGender
)
posts[index] = post.with(likeCount: post.likeCount + (post.isLiked ? 0 : 1), isLiked: true)
}
}

func guess(postID: String, name: String) async throws -> Bool {
let matched = try await repository.guess(postID: postID, name: name)
updatePost(postID: postID) { post in
ExpressPost(
id: post.id,
nickname: post.nickname,
targetName: post.targetName,
contentPreview: post.contentPreview,
publishTime: post.publishTime,
likeCount: post.likeCount,
commentCount: post.commentCount,
guessCount: post.guessCount + 1,
correctGuessCount: post.correctGuessCount + (matched ? 1 : 0),
isLiked: post.isLiked,
canGuess: post.canGuess,
selfGender: post.selfGender,
targetGender: post.targetGender
)
post.with(guessCount: post.guessCount + 1,
correctGuessCount: post.correctGuessCount + (matched ? 1 : 0))
}
return matched
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,68 @@ struct MarketplaceUpdateDraft: Codable {
let qq: String
let phone: String?
}

extension MarketplaceItem {
/// The single place that enumerates every stored field when copying an item.
/// `nil` means "keep the current value"; optional fields can only be replaced with a
/// non-nil value here. Reconstructing the struct by hand silently dropped `typeID`,
/// so an edited item kept showing its old category.
func with(
title: String? = nil,
price: Double? = nil,
summary: String? = nil,
sellerName: String? = nil,
sellerAvatarURL: String? = nil,
authorId: String? = nil,
postedAt: String? = nil,
location: String? = nil,
state: MarketplaceItemState? = nil,
tags: [String]? = nil,
previewImageURL: String? = nil,
typeID: Int? = nil
) -> MarketplaceItem {
MarketplaceItem(
id: id,
title: title ?? self.title,
price: price ?? self.price,
summary: summary ?? self.summary,
sellerName: sellerName ?? self.sellerName,
sellerAvatarURL: sellerAvatarURL ?? self.sellerAvatarURL,
authorId: authorId ?? self.authorId,
postedAt: postedAt ?? self.postedAt,
location: location ?? self.location,
state: state ?? self.state,
tags: tags ?? self.tags,
previewImageURL: previewImageURL ?? self.previewImageURL,
typeID: typeID ?? self.typeID
)
}
}

extension MarketplaceDetail {
/// The single place that enumerates every stored field when copying a detail.
/// `nil` means "keep the current value". Reconstructing by hand silently reset
/// `ownedByCurrentUser`, which controls whether the owner sees the edit actions.
func with(
item: MarketplaceItem? = nil,
condition: String? = nil,
description: String? = nil,
contactHint: String? = nil,
imageURLs: [String]? = nil,
ownedByCurrentUser: Bool? = nil
) -> MarketplaceDetail {
MarketplaceDetail(
item: item ?? self.item,
condition: condition ?? self.condition,
description: description ?? self.description,
contactHint: contactHint ?? self.contactHint,
sellerUsername: sellerUsername,
sellerNickname: sellerNickname,
sellerCollege: sellerCollege,
sellerMajor: sellerMajor,
sellerGrade: sellerGrade,
imageURLs: imageURLs ?? self.imageURLs,
ownedByCurrentUser: ownedByCurrentUser ?? self.ownedByCurrentUser
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,67 +107,32 @@ final class MockMarketplaceRepository: MarketplaceRepository {
throw NetworkError.server(code: 404, message: localizedString("marketplace.itemNotFound"))
}
let contactHintParts = contactHintParts(qq: draft.qq, phone: draft.phone)
let updatedItem = MarketplaceItem(
id: detail.item.id,
let category = MarketplaceRemoteMapper.displayName(forType: draft.typeID)
let updatedItem = detail.item.with(
title: draft.title,
price: draft.price,
summary: RemoteMapperSupport.truncated(draft.description, limit: 28),
sellerName: detail.item.sellerName,
sellerAvatarURL: detail.item.sellerAvatarURL,
authorId: detail.item.authorId,
postedAt: "刚刚修改",
location: draft.location,
state: detail.item.state,
tags: [MarketplaceRemoteMapper.displayName(forType: draft.typeID)],
previewImageURL: detail.item.previewImageURL
tags: [category],
typeID: draft.typeID
)
detailsByID[itemID] = MarketplaceDetail(
detailsByID[itemID] = detail.with(
item: updatedItem,
condition: MarketplaceRemoteMapper.displayName(forType: draft.typeID),
condition: category,
description: draft.description,
contactHint: contactHintParts.joined(separator: " / "),
sellerUsername: detail.sellerUsername,
sellerNickname: detail.sellerNickname,
sellerCollege: detail.sellerCollege,
sellerMajor: detail.sellerMajor,
sellerGrade: detail.sellerGrade,
imageURLs: detail.imageURLs
contactHint: contactHintParts.joined(separator: " / ")
)
syncItem(updatedItem)
}

func updateItemState(itemID: String, state: MarketplaceItemState) async throws {
guard var detail = detailsByID[itemID] else {
guard let detail = detailsByID[itemID] else {
throw NetworkError.server(code: 404, message: localizedString("marketplace.itemNotFound"))
}

let updatedItem = MarketplaceItem(
id: detail.item.id,
title: detail.item.title,
price: detail.item.price,
summary: detail.item.summary,
sellerName: detail.item.sellerName,
sellerAvatarURL: detail.item.sellerAvatarURL,
authorId: detail.item.authorId,
postedAt: detail.item.postedAt,
location: detail.item.location,
state: state,
tags: detail.item.tags,
previewImageURL: detail.item.previewImageURL
)
detail = MarketplaceDetail(
item: updatedItem,
condition: detail.condition,
description: detail.description,
contactHint: detail.contactHint,
sellerUsername: detail.sellerUsername,
sellerNickname: detail.sellerNickname,
sellerCollege: detail.sellerCollege,
sellerMajor: detail.sellerMajor,
sellerGrade: detail.sellerGrade,
imageURLs: detail.imageURLs
)
detailsByID[itemID] = detail
let updatedItem = detail.item.with(state: state)
detailsByID[itemID] = detail.with(item: updatedItem)
syncItem(updatedItem)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,32 +38,9 @@ final class RemoteMarketplaceRepository: MarketplaceRepository {
return detail
}

return MarketplaceDetail(
item: MarketplaceItem(
id: detail.item.id,
title: detail.item.title,
price: detail.item.price,
summary: detail.item.summary,
sellerName: detail.item.sellerName,
sellerAvatarURL: detail.item.sellerAvatarURL,
authorId: detail.item.authorId,
postedAt: detail.item.postedAt,
location: detail.item.location,
state: detail.item.state,
tags: detail.item.tags,
previewImageURL: detail.item.previewImageURL ?? previewURL,
typeID: detail.item.typeID
),
condition: detail.condition,
description: detail.description,
contactHint: detail.contactHint,
sellerUsername: detail.sellerUsername,
sellerNickname: detail.sellerNickname,
sellerCollege: detail.sellerCollege,
sellerMajor: detail.sellerMajor,
sellerGrade: detail.sellerGrade,
imageURLs: [previewURL],
ownedByCurrentUser: detail.ownedByCurrentUser
return detail.with(
item: detail.item.with(previewImageURL: detail.item.previewImageURL ?? previewURL),
imageURLs: [previewURL]
)
}

Expand Down
Loading
Loading