diff --git a/dudu-tweet/dudu-tweet.xcodeproj/project.pbxproj b/dudu-tweet/dudu-tweet.xcodeproj/project.pbxproj index 942bffe..8a93a8b 100644 --- a/dudu-tweet/dudu-tweet.xcodeproj/project.pbxproj +++ b/dudu-tweet/dudu-tweet.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 24A07CB82A02173400F4ECA8 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */; }; 24A07CBA2A02186500F4ECA8 /* Tweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB92A02186500F4ECA8 /* Tweet.swift */; }; 24A07CBC2A022B2000F4ECA8 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CBB2A022B2000F4ECA8 /* ProfileViewModel.swift */; }; + 24A07CC22A02302700F4ECA8 /* TweetRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CC12A02302700F4ECA8 /* TweetRowViewModel.swift */; }; 24A59AB42A002EB8009C9E3E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AB32A002EB8009C9E3E /* MainTabView.swift */; }; 24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AB92A0030CB009C9E3E /* ExploreView.swift */; }; 24A59ABC2A0030EC009C9E3E /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59ABB2A0030EC009C9E3E /* NotificationsView.swift */; }; @@ -68,6 +69,7 @@ 24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = ""; }; 24A07CB92A02186500F4ECA8 /* Tweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tweet.swift; sourceTree = ""; }; 24A07CBB2A022B2000F4ECA8 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; + 24A07CC12A02302700F4ECA8 /* TweetRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweetRowViewModel.swift; sourceTree = ""; }; 24A59AB32A002EB8009C9E3E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; 24A59AB92A0030CB009C9E3E /* ExploreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreView.swift; sourceTree = ""; }; 24A59ABB2A0030EC009C9E3E /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; @@ -238,11 +240,28 @@ 2492CC262A0025A50086C525 /* Tweets */ = { isa = PBXGroup; children = ( - 2492CC272A0025DD0086C525 /* TweetRowView.swift */, + 24A07CBE2A022FCB00F4ECA8 /* ViewModels */, + 24A07CBD2A022FC600F4ECA8 /* Views */, ); path = Tweets; sourceTree = ""; }; + 24A07CBD2A022FC600F4ECA8 /* Views */ = { + isa = PBXGroup; + children = ( + 2492CC272A0025DD0086C525 /* TweetRowView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 24A07CBE2A022FCB00F4ECA8 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 24A07CC12A02302700F4ECA8 /* TweetRowViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; 24A59AB22A002E79009C9E3E /* MainTab */ = { isa = PBXGroup; children = ( @@ -522,6 +541,7 @@ 24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */, 24A07CB62A020FF900F4ECA8 /* UploadTweetViewModel.swift in Sources */, 24A07CB42A020ECB00F4ECA8 /* TweetService.swift in Sources */, + 24A07CC22A02302700F4ECA8 /* TweetRowViewModel.swift in Sources */, 24A59AD22A00BE14009C9E3E /* SideMenuViewModel.swift in Sources */, 24A59ADF2A00DCC2009C9E3E /* TextArea.swift in Sources */, 24A07CB22A01869F00F4ECA8 /* SearchBar.swift in Sources */, diff --git a/dudu-tweet/dudu-tweet.xcodeproj/project.xcworkspace/xcuserdata/ching.xcuserdatad/UserInterfaceState.xcuserstate b/dudu-tweet/dudu-tweet.xcodeproj/project.xcworkspace/xcuserdata/ching.xcuserdatad/UserInterfaceState.xcuserstate index d69d3a0..1fe90d3 100644 Binary files a/dudu-tweet/dudu-tweet.xcodeproj/project.xcworkspace/xcuserdata/ching.xcuserdatad/UserInterfaceState.xcuserstate and b/dudu-tweet/dudu-tweet.xcodeproj/project.xcworkspace/xcuserdata/ching.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/dudu-tweet/dudu-tweet/Core/Components/Tweets/ViewModels/TweetRowViewModel.swift b/dudu-tweet/dudu-tweet/Core/Components/Tweets/ViewModels/TweetRowViewModel.swift new file mode 100644 index 0000000..d939d2a --- /dev/null +++ b/dudu-tweet/dudu-tweet/Core/Components/Tweets/ViewModels/TweetRowViewModel.swift @@ -0,0 +1,38 @@ +// +// TweetRowViewModel.swift +// dudu-tweet +// +// Created by ching on 2023/5/3. +// + +import Foundation + +class TweetVeiwModel: ObservableObject { + private let service = TweetService() + @Published var tweet: Tweet + + init(tweet: Tweet) { + self.tweet = tweet + self.checkUserLikedTweet() + } + + func likeTweet() { + service.likeTweet(tweet) { _ in + self.tweet.didLike = true + } + } + + func unlikeTweet() { + service.unlikeTweet(tweet) { _ in + self.tweet.didLike = false + } + } + + func checkUserLikedTweet() { + service.checkIfUserLikedTweet(tweet) { didLiked in + if didLiked { + self.tweet.didLike = true + } + } + } +} diff --git a/dudu-tweet/dudu-tweet/Core/Components/Tweets/TweetRowView.swift b/dudu-tweet/dudu-tweet/Core/Components/Tweets/Views/TweetRowView.swift similarity index 83% rename from dudu-tweet/dudu-tweet/Core/Components/Tweets/TweetRowView.swift rename to dudu-tweet/dudu-tweet/Core/Components/Tweets/Views/TweetRowView.swift index b7c5851..2befad5 100644 --- a/dudu-tweet/dudu-tweet/Core/Components/Tweets/TweetRowView.swift +++ b/dudu-tweet/dudu-tweet/Core/Components/Tweets/Views/TweetRowView.swift @@ -9,15 +9,15 @@ import SwiftUI import Kingfisher struct TweetRowView: View { - let tweet: Tweet + @ObservedObject var viewModel: TweetVeiwModel init(tweet: Tweet) { - self.tweet = tweet + self.viewModel = TweetVeiwModel(tweet: tweet) } var body: some View { VStack(alignment: .leading) { // profile image + user info + tweet - if let user = tweet.user { + if let user = viewModel.tweet.user { HStack(alignment: .top, spacing: 12) { // profile image KFImage(URL(string: user.profileImageUrl)) @@ -41,7 +41,7 @@ struct TweetRowView: View { } // tweet caption - Text(tweet.caption) + Text(viewModel.tweet.caption) .font(.subheadline) .multilineTextAlignment(.leading) } @@ -64,10 +64,11 @@ struct TweetRowView: View { } Spacer() Button { - // action here + viewModel.tweet.didLike ?? false ? viewModel.unlikeTweet() : viewModel.likeTweet() } label: { - Image(systemName: "heart") + Image(systemName: viewModel.tweet.didLike ?? false ? "heart.fill" : "heart") .font(.subheadline) + .foregroundColor(viewModel.tweet.didLike ?? false ? .red : .gray) } Spacer() Button { diff --git a/dudu-tweet/dudu-tweet/Core/Profile/ViewModels/ProfileViewModel.swift b/dudu-tweet/dudu-tweet/Core/Profile/ViewModels/ProfileViewModel.swift index 36fc673..ad3307e 100644 --- a/dudu-tweet/dudu-tweet/Core/Profile/ViewModels/ProfileViewModel.swift +++ b/dudu-tweet/dudu-tweet/Core/Profile/ViewModels/ProfileViewModel.swift @@ -10,12 +10,30 @@ import Foundation class ProfileViewModel: ObservableObject { @Published var tweets = [Tweet]() + @Published var likedTweets = [Tweet]() private let service = TweetService() + private let userService = UserService() let user: User init(user: User) { self.user = user self.fetchUserTweet() + self.fetchLikedTweets() + } + + var actionBarTitle: String { + user.isCurrentUser ? "Edit Profile" : "Follow" + } + + func tweets(forFilter filter: TweetFilterViewModel) -> [Tweet] { + switch filter { + case .tweets: + return tweets + case .replies: + return tweets + case .likes: + return likedTweets + } } func fetchUserTweet() { @@ -27,4 +45,19 @@ class ProfileViewModel: ObservableObject { } } } + + func fetchLikedTweets() { + guard let uid = user.id else { return } + service.fetchLikedTweets(forUid: uid) { tweets in + self.likedTweets = tweets + + for i in 0 ..< tweets.count { + let uid = tweets[i].uid + self.userService.fetchUser(withUid: uid) { user in + self.likedTweets[i].user = user + } + } + + } + } } diff --git a/dudu-tweet/dudu-tweet/Core/Profile/Views/ProfileView.swift b/dudu-tweet/dudu-tweet/Core/Profile/Views/ProfileView.swift index 3b4b728..36d5f02 100644 --- a/dudu-tweet/dudu-tweet/Core/Profile/Views/ProfileView.swift +++ b/dudu-tweet/dudu-tweet/Core/Profile/Views/ProfileView.swift @@ -87,7 +87,7 @@ extension ProfileView { Button { // action here } label: { - Text("Edit Profile") + Text(viewModel.actionBarTitle) .font(.subheadline).bold() .frame(width: 120, height: 32) .foregroundColor(.black) @@ -165,7 +165,7 @@ extension ProfileView { var tweetsView: some View { ScrollView { LazyVStack { - ForEach(viewModel.tweets) { tweet in + ForEach(viewModel.tweets(forFilter: self.selectedFilter)) { tweet in TweetRowView(tweet: tweet) } } diff --git a/dudu-tweet/dudu-tweet/Model/Tweet.swift b/dudu-tweet/dudu-tweet/Model/Tweet.swift index 34e19d8..789ac4c 100644 --- a/dudu-tweet/dudu-tweet/Model/Tweet.swift +++ b/dudu-tweet/dudu-tweet/Model/Tweet.swift @@ -16,4 +16,5 @@ struct Tweet: Identifiable, Decodable { var likes: Int var user: User? + var didLike: Bool? = false } diff --git a/dudu-tweet/dudu-tweet/Model/User.swift b/dudu-tweet/dudu-tweet/Model/User.swift index c5837ff..8e1577d 100644 --- a/dudu-tweet/dudu-tweet/Model/User.swift +++ b/dudu-tweet/dudu-tweet/Model/User.swift @@ -6,6 +6,7 @@ // import FirebaseFirestoreSwift +import Firebase struct User: Identifiable, Decodable { @DocumentID var id: String? @@ -13,4 +14,6 @@ struct User: Identifiable, Decodable { let fullname: String let profileImageUrl: String let email: String + + var isCurrentUser: Bool { return Auth.auth().currentUser?.uid == self.id} } diff --git a/dudu-tweet/dudu-tweet/Service/TweetService.swift b/dudu-tweet/dudu-tweet/Service/TweetService.swift index 94ecd87..4c93993 100644 --- a/dudu-tweet/dudu-tweet/Service/TweetService.swift +++ b/dudu-tweet/dudu-tweet/Service/TweetService.swift @@ -50,5 +50,64 @@ struct TweetService { completion(tweets.sorted(by: { $0.timestamp.dateValue() > $1.timestamp.dateValue()})) } } + + func likeTweet(_ tweet: Tweet, completion: @escaping(Bool) -> Void) { + guard let uid = Auth.auth().currentUser?.uid else { return } + guard let tweetId = tweet.id else { return } + + let userLikeRef = Firestore.firestore().collection("users").document(uid).collection("user-likes") + Firestore.firestore().collection("tweets").document(tweetId) + .updateData(["likes": tweet.likes + 1]) { _ in + userLikeRef.document(tweetId).setData([:]) { _ in + completion(true) + } + } + } + + func unlikeTweet(_ tweet: Tweet, completion: @escaping(Bool) -> Void) { + guard let uid = Auth.auth().currentUser?.uid else { return } + guard let tweetId = tweet.id else { return } + guard tweet.likes > 0 else { return } + + let userLikeRef = Firestore.firestore().collection("users").document(uid).collection("user-likes") + Firestore.firestore().collection("tweets").document(tweetId) + .updateData(["likes": tweet.likes - 1]) { _ in + userLikeRef.document(tweetId).delete() { _ in + completion(true) + } + } + } + + func checkIfUserLikedTweet(_ tweet: Tweet, completion: @escaping(Bool) -> Void) { + guard let uid = Auth.auth().currentUser?.uid else { return } + guard let tweetId = tweet.id else { return } + print("DEBUG: checking likes for \(tweetId)") + Firestore.firestore().collection("users").document(uid).collection("user-likes") + .document(tweetId) + .getDocument { snapshot, _ in + guard let snapshot = snapshot else { return } + completion(snapshot.exists) + } + } + + func fetchLikedTweets(forUid uid: String, completion: @escaping([Tweet]) -> Void) { + var tweets = [Tweet]() + Firestore.firestore().collection("users").document(uid).collection("user-likes") + .getDocuments { snapshot, _ in + guard let documents = snapshot?.documents else { return } + documents.forEach { doc in + let tweetID = doc.documentID + Firestore.firestore().collection("tweets") + .document(tweetID) + .getDocument { snapshot, _ in + guard let tweet = try? snapshot?.data(as: Tweet.self) else { return } + tweets.append(tweet) + completion(tweets) + } + } + } + + } + }