Skip to content

Commit

Permalink
Merge branch 'main'
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-nirali-s committed Dec 5, 2024
2 parents c9ba99f + a11e6e0 commit 5a42f12
Show file tree
Hide file tree
Showing 27 changed files with 122 additions and 69 deletions.
5 changes: 3 additions & 2 deletions Data/Data/Model/Groups.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ public struct Groups: Codable, Identifiable {
public var hasExpenses: Bool
public var isActive: Bool

public init(name: String, createdBy: String, updatedBy: String, imageUrl: String? = nil, members: [String], balances: [GroupMemberBalance],
createdAt: Timestamp, updatedAt: Timestamp, hasExpenses: Bool = false, isActive: Bool = true) {
public init(name: String, createdBy: String, updatedBy: String, imageUrl: String? = nil,
members: [String], balances: [GroupMemberBalance], createdAt: Timestamp = Timestamp(),
updatedAt: Timestamp = Timestamp(), hasExpenses: Bool = false, isActive: Bool = true) {
self.name = name
self.createdBy = createdBy
self.updatedBy = updatedBy
Expand Down
43 changes: 31 additions & 12 deletions Data/Data/Repository/GroupRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,12 @@ public class GroupRepository: ObservableObject {
}

public func deleteGroup(group: Groups) async throws {
var group = group
guard let userId = preference.user?.id else { return }

// Make group inactive
group.isActive = false
var group = group
group.isActive = false // Make group inactive
group.updatedBy = userId
group.updatedAt = Timestamp()

try await updateGroup(group: group, type: .groupDeleted)
}
Expand Down Expand Up @@ -193,17 +195,19 @@ public class GroupRepository: ObservableObject {
try await store.fetchGroupsBy(userId: userId, limit: limit, lastDocument: lastDocument)
}

public func fetchMemberBy(userId: String) async throws -> AppUser? {
public func fetchMemberBy(memberId: String) async throws -> AppUser? {
// Use a synchronous read to check if the member already exists in groupMembers
let existingMember = groupMembersQueue.sync { groupMembers.first(where: { $0.id == userId }) }

if let existingMember {
return existingMember // Return the available member from groupMembers
if let existingMember = groupMembersQueue.sync(execute: {
groupMembers.first(where: { $0.id == memberId })
}) {
await updateCurrentUserImageUrl(for: [memberId])
return existingMember // Return the cached member
}

let member = try await userRepository.fetchUserBy(userID: userId)
// Fetch the member from the repository if not found locally
let member = try await userRepository.fetchUserBy(userID: memberId)

// Append to groupMembers safely with a barrier, ensuring thread safety
// Append the newly fetched member to groupMembers in a thread-safe manner
if let member {
try await withCheckedThrowingContinuation { continuation in
groupMembersQueue.async(flags: .barrier) {
Expand All @@ -218,23 +222,26 @@ public class GroupRepository: ObservableObject {
public func fetchMembersBy(memberIds: [String]) async throws -> [AppUser] {
var members: [AppUser] = []

// Filter out memberIds that already exist in groupMembers to minimise API calls
// Filter out memberIds that already exist in groupMembers to minimize API calls
let missingMemberIds = memberIds.filter { memberId in
let cachedMember = self.groupMembersQueue.sync { self.groupMembers.first { $0.id == memberId } }
return cachedMember == nil
}

if missingMemberIds.isEmpty {
await updateCurrentUserImageUrl(for: memberIds)
return groupMembersQueue.sync { self.groupMembers.filter { memberIds.contains($0.id) } }
}

// Fetch missing members concurrently using a TaskGroup
try await withThrowingTaskGroup(of: AppUser?.self) { groupTask in
for memberId in missingMemberIds {
groupTask.addTask {
try await self.fetchMemberBy(userId: memberId)
try await self.fetchMemberBy(memberId: memberId)
}
}

// Collect results from the task group & add to the groupMembers array
for try await member in groupTask {
if let member {
members.append(member)
Expand All @@ -249,4 +256,16 @@ public class GroupRepository: ObservableObject {

return members
}

// Updates the current user's image url in groupMembers if applicable
private func updateCurrentUserImageUrl(for memberIds: [String]) async {
guard let currentUser = preference.user, memberIds.contains(currentUser.id) else { return }

groupMembersQueue.async(flags: .barrier) {
if let index = self.groupMembers.firstIndex(where: { $0.id == currentUser.id }),
self.groupMembers[index].imageUrl != currentUser.imageUrl {
self.groupMembers[index].imageUrl = currentUser.imageUrl
}
}
}
}
2 changes: 1 addition & 1 deletion Data/Data/Store/ExpenseStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class ExpenseStore: ObservableObject {

func updateExpense(groupId: String, expense: Expense) async throws {
if let expenseId = expense.id {
try expenseReference(groupId: groupId).document(expenseId).setData(from: expense, merge: true)
try expenseReference(groupId: groupId).document(expenseId).setData(from: expense, merge: false)
} else {
LogE("ExpenseStore: \(#function) Expense not found.")
throw ServiceError.dataNotFound
Expand Down
2 changes: 1 addition & 1 deletion Data/Data/Store/GroupStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class GroupStore: ObservableObject {

func updateGroup(group: Groups) async throws {
if let groupId = group.id {
try groupReference.document(groupId).setData(from: group, merge: true)
try groupReference.document(groupId).setData(from: group, merge: false)
} else {
LogE("GroupStore: \(#function) Group not found.")
throw ServiceError.dataNotFound
Expand Down
2 changes: 1 addition & 1 deletion Data/Data/Store/TransactionStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class TransactionStore: ObservableObject {

func updateTransaction(groupId: String, transaction: Transactions) async throws {
if let transactionId = transaction.id {
try transactionReference(groupId: groupId).document(transactionId).setData(from: transaction, merge: true)
try transactionReference(groupId: groupId).document(transactionId).setData(from: transaction, merge: false)
} else {
LogE("TransactionStore: \(#function) Payment not found.")
throw ServiceError.dataNotFound
Expand Down
2 changes: 1 addition & 1 deletion Data/Data/Store/UserStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class UserStore: ObservableObject {
}

func updateUser(user: AppUser) async throws -> AppUser? {
try usersCollection.document(user.id).setData(from: user, merge: true)
try usersCollection.document(user.id).setData(from: user, merge: false)
return user
}

Expand Down
13 changes: 10 additions & 3 deletions Splito/Localization/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@
},
"%@" : {

},
"%@ %@ " : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ %2$@ "
}
}
}
},
"%@ %@ %@" : {
"localizations" : {
Expand All @@ -61,9 +71,6 @@
},
"%@ and %@" : {
"extractionState" : "manual"
},
"%@ owes " : {

},
"%@ owes you " : {

Expand Down
7 changes: 4 additions & 3 deletions Splito/UI/Home/Expense/AddExpenseViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ class AddExpenseViewModel: BaseViewModel, ObservableObject {

private func fetchUserData(for userId: String) async -> AppUser? {
do {
let user = try await groupRepository.fetchMemberBy(userId: userId)
let user = try await groupRepository.fetchMemberBy(memberId: userId)
LogD("AddExpenseViewModel: \(#function) Member fetched successfully.")
return user
} catch {
Expand Down Expand Up @@ -389,7 +389,7 @@ extension AddExpenseViewModel {
if !group.hasExpenses {
selectedGroup?.hasExpenses = true
}
await updateGroupMemberBalance(expense: expense, updateType: .Add)
await updateGroupMemberBalance(expense: newExpense, updateType: .Add)

showLoader = false
LogD("AddExpenseViewModel: \(#function) Expense added successfully.")
Expand Down Expand Up @@ -458,7 +458,8 @@ extension AddExpenseViewModel {
private func hasExpenseChanged(_ expense: Expense, oldExpense: Expense) -> Bool {
return oldExpense.amount != expense.amount || oldExpense.paidBy != expense.paidBy ||
oldExpense.splitTo != expense.splitTo || oldExpense.splitType != expense.splitType ||
oldExpense.splitData != expense.splitData || oldExpense.isActive != expense.isActive
oldExpense.splitData != expense.splitData || oldExpense.isActive != expense.isActive ||
oldExpense.date != expense.date
}

private func updateGroupMemberBalance(expense: Expense, updateType: ExpenseUpdateType) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ struct ChooseMultiplePayerView: View {
.scrollIndicators(.hidden)
.scrollBounceBehavior(.basedOnSize)

BottomInfoCardView(title: "\(String(format: "%.2f", viewModel.totalAmount)) of \(viewModel.expenseAmount.formattedCurrency)",
BottomInfoCardView(title: "\(viewModel.totalAmount.formattedCurrency) of \(viewModel.expenseAmount.formattedCurrency)",
value: "\((viewModel.expenseAmount - viewModel.totalAmount).formattedCurrencyWithSign) left")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private struct SplitOptionsBottomView: View {
memberCount: viewModel.selectedMembers.count, isAllSelected: viewModel.isAllSelected,
isForEqualSplit: true, onAllBtnTap: viewModel.handleAllBtnAction)
case .fixedAmount:
BottomInfoCardView(title: "\(String(format: "%.2f", viewModel.totalFixedAmount)) of \(viewModel.expenseAmount.formattedCurrency)",
BottomInfoCardView(title: "\(viewModel.totalFixedAmount.formattedCurrency) of \(viewModel.expenseAmount.formattedCurrency)",
value: "\((viewModel.expenseAmount - viewModel.totalFixedAmount).formattedCurrencyWithSign) left")
case .percentage:
BottomInfoCardView(title: "\(String(format: "%.0f", viewModel.totalPercentage))% of 100%",
Expand Down
3 changes: 1 addition & 2 deletions Splito/UI/Home/Groups/Add Member/InviteMemberViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ class InviteMemberViewModel: BaseViewModel, ObservableObject {
// MARK: - Data Loading
private func fetchGroup() async {
do {
let group = try await groupRepository.fetchGroupBy(id: groupId)
self.group = group
self.group = try await groupRepository.fetchGroupBy(id: groupId)
viewState = .initial
LogD("InviteMemberViewModel: \(#function) Group fetched successfully.")
} catch {
Expand Down
5 changes: 3 additions & 2 deletions Splito/UI/Home/Groups/Create Group/CreateGroupViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class CreateGroupViewModel: BaseViewModel, ObservableObject {
super.init()
}

// MARK: - User Actions
private func checkCameraPermission(authorized: @escaping (() -> Void)) {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .notDetermined:
Expand Down Expand Up @@ -100,8 +101,8 @@ class CreateGroupViewModel: BaseViewModel, ObservableObject {
guard let userId = preference.user?.id else { return false }

let memberBalance = GroupMemberBalance(id: userId, balance: 0, totalSummary: [])
let group = Groups(name: groupName.trimming(spaces: .leadingAndTrailing), createdBy: userId, updatedBy: userId, imageUrl: nil,
members: [userId], balances: [memberBalance], createdAt: Timestamp(), updatedAt: Timestamp())
let group = Groups(name: groupName.trimming(spaces: .leadingAndTrailing), createdBy: userId,
updatedBy: userId, members: [userId], balances: [memberBalance])

do {
showLoader = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import SwiftUI
import BaseStyle
import Data

struct GroupBalancesView: View {

Expand Down Expand Up @@ -71,6 +72,8 @@ struct GroupBalancesView: View {
@MainActor
private struct GroupBalanceItemView: View {

@Inject private var preference: SplitoPreference

let memberBalance: MembersCombinedBalance
let viewModel: GroupBalancesViewModel

Expand All @@ -88,7 +91,7 @@ private struct GroupBalanceItemView: View {

let hasDue = memberBalance.totalOwedAmount < 0
let name = viewModel.getMemberName(id: memberBalance.id, needFullName: true)
let owesOrGetsBack = hasDue ? "owes" : "gets back"
let owesOrGetsBack = hasDue ? (memberBalance.id == preference.user?.id ? "owe" : "owes") : (memberBalance.id == preference.user?.id ? "get back" : "gets back")

if memberBalance.totalOwedAmount == 0 {
Group {
Expand Down Expand Up @@ -142,6 +145,8 @@ private struct GroupBalanceItemView: View {
private struct GroupBalanceItemMemberView: View {
let SUB_IMAGE_HEIGHT: CGFloat = 24

@Inject private var preference: SplitoPreference

let id: String
let balances: [String: Double]
let viewModel: GroupBalancesViewModel
Expand All @@ -156,13 +161,14 @@ private struct GroupBalanceItemMemberView: View {
let imageUrl = viewModel.getMemberImage(id: memberId)
let owesMemberName = viewModel.getMemberName(id: hasDue ? memberId : id)
let owedMemberName = viewModel.getMemberName(id: hasDue ? id : memberId)
let owesText = ((hasDue ? id : memberId) == preference.user?.id) ? "owe" : "owes"

VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .center, spacing: 16) {
MemberProfileImageView(imageUrl: imageUrl, height: SUB_IMAGE_HEIGHT, scaleEffect: 0.6)

Group {
Text("\(owedMemberName) owes ")
Text("\(owedMemberName.capitalized) \(owesText.localized) ")

+ Text(amount.formattedCurrency)
.foregroundColor(hasDue ? errorColor : successColor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ class GroupBalancesViewModel: BaseViewModel, ObservableObject {
}

func getMemberName(id: String, needFullName: Bool = false) -> String {
guard let member = getMemberDataBy(id: id) else { return "" }
return needFullName ? member.fullName : member.nameWithLastInitial
guard let userId = preference.user?.id, let member = getMemberDataBy(id: id) else { return "" }
return needFullName ? (id == userId ? "You" : member.fullName) : (id == userId ? "you" : member.nameWithLastInitial)
}

// MARK: - User Actions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,8 @@ class GroupSettleUpViewModel: BaseViewModel, ObservableObject {
// MARK: - Data Loading
func fetchGroupDetails() async {
do {
let group = try await groupRepository.fetchGroupBy(id: groupId)
guard let group else {
viewState = .initial
return
}
self.group = group
calculateMemberPayableAmount(group: group)
self.group = try await groupRepository.fetchGroupBy(id: groupId)
calculateMemberPayableAmount()
viewState = .initial
LogD("GroupSettleUpViewModel: \(#function) Group fetched successfully.")
} catch {
Expand All @@ -54,8 +49,8 @@ class GroupSettleUpViewModel: BaseViewModel, ObservableObject {
}
}

func calculateMemberPayableAmount(group: Groups) {
guard let userId = preference.user?.id else {
func calculateMemberPayableAmount() {
guard let group, let userId = preference.user?.id else {
viewState = .initial
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ class GroupPaymentViewModel: BaseViewModel, ObservableObject {
private func getPayerUserDetail() async {
do {
viewState = .loading
let user = try await userRepository.fetchUserBy(userID: payerId)
if let user { payer = user }
payer = try await userRepository.fetchUserBy(userID: payerId)
viewState = .initial
LogD("GroupPaymentViewModel: \(#function) Payer fetched successfully.")
} catch {
Expand All @@ -137,8 +136,7 @@ class GroupPaymentViewModel: BaseViewModel, ObservableObject {
private func getPayableUserDetail() async {
do {
viewState = .loading
let user = try await userRepository.fetchUserBy(userID: receiverId)
if let user { receiver = user }
receiver = try await userRepository.fetchUserBy(userID: receiverId)
viewState = .initial
LogD("GroupPaymentViewModel: \(#function) Payable fetched successfully.")
} catch {
Expand Down Expand Up @@ -294,6 +292,7 @@ class GroupPaymentViewModel: BaseViewModel, ObservableObject {
NotificationCenter.default.post(name: .updateTransaction, object: self.transaction)
}

guard let transaction = self.transaction else { return false }
guard hasTransactionChanged(transaction, oldTransaction: oldTransaction) else { return true }
await updateGroupMemberBalance(updateType: .Update(oldTransaction: oldTransaction))

Expand All @@ -316,7 +315,8 @@ class GroupPaymentViewModel: BaseViewModel, ObservableObject {

private func hasTransactionChanged(_ transaction: Transactions, oldTransaction: Transactions) -> Bool {
return oldTransaction.payerId != transaction.payerId || oldTransaction.receiverId != transaction.receiverId ||
oldTransaction.amount != transaction.amount || oldTransaction.isActive != transaction.isActive
oldTransaction.amount != transaction.amount || oldTransaction.isActive != transaction.isActive ||
oldTransaction.date != transaction.date
}

private func updateGroupMemberBalance(updateType: TransactionUpdateType) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ struct GroupWhoIsPayingView: View {

struct GroupPayingMemberView: View {

@Inject private var preference: SplitoPreference

let member: AppUser

let isSelected: Bool
Expand All @@ -61,6 +63,14 @@ struct GroupPayingMemberView: View {

let onMemberTap: (String) -> Void

private var memberName: String {
if let user = preference.user, user.id == member.id {
return "You"
} else {
return member.fullName
}
}

init(member: AppUser, isSelected: Bool = false, isLastMember: Bool, disableMemberTap: Bool = false, onMemberTap: @escaping (String) -> Void) {
self.member = member
self.isSelected = isSelected
Expand All @@ -73,7 +83,7 @@ struct GroupPayingMemberView: View {
HStack(alignment: .center, spacing: 16) {
MemberProfileImageView(imageUrl: member.imageUrl)

Text(member.fullName.localized)
Text(memberName.localized)
.font(.subTitle2())
.foregroundStyle(primaryText)
.lineLimit(1)
Expand Down
Loading

0 comments on commit 5a42f12

Please sign in to comment.