Compare commits

..

No commits in common. "d34651e3064bcfbb04981041b09e8a80d510d4c6" and "9d237961b694196669ab3006b2f706d6fc5e16dc" have entirely different histories.

14 changed files with 58 additions and 271 deletions

View File

@ -19,8 +19,6 @@
24A07CB62A020FF900F4ECA8 /* UploadTweetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB52A020FF900F4ECA8 /* UploadTweetViewModel.swift */; }; 24A07CB62A020FF900F4ECA8 /* UploadTweetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB52A020FF900F4ECA8 /* UploadTweetViewModel.swift */; };
24A07CB82A02173400F4ECA8 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */; }; 24A07CB82A02173400F4ECA8 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */; };
24A07CBA2A02186500F4ECA8 /* Tweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB92A02186500F4ECA8 /* Tweet.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 */; }; 24A59AB42A002EB8009C9E3E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AB32A002EB8009C9E3E /* MainTabView.swift */; };
24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AB92A0030CB009C9E3E /* ExploreView.swift */; }; 24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AB92A0030CB009C9E3E /* ExploreView.swift */; };
24A59ABC2A0030EC009C9E3E /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59ABB2A0030EC009C9E3E /* NotificationsView.swift */; }; 24A59ABC2A0030EC009C9E3E /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59ABB2A0030EC009C9E3E /* NotificationsView.swift */; };
@ -68,8 +66,6 @@
24A07CB52A020FF900F4ECA8 /* UploadTweetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTweetViewModel.swift; sourceTree = "<group>"; }; 24A07CB52A020FF900F4ECA8 /* UploadTweetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTweetViewModel.swift; sourceTree = "<group>"; };
24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = "<group>"; }; 24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = "<group>"; };
24A07CB92A02186500F4ECA8 /* Tweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tweet.swift; sourceTree = "<group>"; }; 24A07CB92A02186500F4ECA8 /* Tweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tweet.swift; sourceTree = "<group>"; };
24A07CBB2A022B2000F4ECA8 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = "<group>"; };
24A07CC12A02302700F4ECA8 /* TweetRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweetRowViewModel.swift; sourceTree = "<group>"; };
24A59AB32A002EB8009C9E3E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; }; 24A59AB32A002EB8009C9E3E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
24A59AB92A0030CB009C9E3E /* ExploreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreView.swift; sourceTree = "<group>"; }; 24A59AB92A0030CB009C9E3E /* ExploreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreView.swift; sourceTree = "<group>"; };
24A59ABB2A0030EC009C9E3E /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; }; 24A59ABB2A0030EC009C9E3E /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
@ -238,28 +234,11 @@
sourceTree = "<group>"; sourceTree = "<group>";
}; };
2492CC262A0025A50086C525 /* Tweets */ = { 2492CC262A0025A50086C525 /* Tweets */ = {
isa = PBXGroup;
children = (
24A07CBE2A022FCB00F4ECA8 /* ViewModels */,
24A07CBD2A022FC600F4ECA8 /* Views */,
);
path = Tweets;
sourceTree = "<group>";
};
24A07CBD2A022FC600F4ECA8 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2492CC272A0025DD0086C525 /* TweetRowView.swift */, 2492CC272A0025DD0086C525 /* TweetRowView.swift */,
); );
path = Views; path = Tweets;
sourceTree = "<group>";
};
24A07CBE2A022FCB00F4ECA8 /* ViewModels */ = {
isa = PBXGroup;
children = (
24A07CC12A02302700F4ECA8 /* TweetRowViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
24A59AB22A002E79009C9E3E /* MainTab */ = { 24A59AB22A002E79009C9E3E /* MainTab */ = {
@ -316,7 +295,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
24A59AC32A003A52009C9E3E /* TwewtFilterViewModel.swift */, 24A59AC32A003A52009C9E3E /* TwewtFilterViewModel.swift */,
24A07CBB2A022B2000F4ECA8 /* ProfileViewModel.swift */,
); );
path = ViewModels; path = ViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
@ -541,7 +519,6 @@
24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */, 24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */,
24A07CB62A020FF900F4ECA8 /* UploadTweetViewModel.swift in Sources */, 24A07CB62A020FF900F4ECA8 /* UploadTweetViewModel.swift in Sources */,
24A07CB42A020ECB00F4ECA8 /* TweetService.swift in Sources */, 24A07CB42A020ECB00F4ECA8 /* TweetService.swift in Sources */,
24A07CC22A02302700F4ECA8 /* TweetRowViewModel.swift in Sources */,
24A59AD22A00BE14009C9E3E /* SideMenuViewModel.swift in Sources */, 24A59AD22A00BE14009C9E3E /* SideMenuViewModel.swift in Sources */,
24A59ADF2A00DCC2009C9E3E /* TextArea.swift in Sources */, 24A59ADF2A00DCC2009C9E3E /* TextArea.swift in Sources */,
24A07CB22A01869F00F4ECA8 /* SearchBar.swift in Sources */, 24A07CB22A01869F00F4ECA8 /* SearchBar.swift in Sources */,
@ -557,7 +534,6 @@
24A59AB42A002EB8009C9E3E /* MainTabView.swift in Sources */, 24A59AB42A002EB8009C9E3E /* MainTabView.swift in Sources */,
24A59ABC2A0030EC009C9E3E /* NotificationsView.swift in Sources */, 24A59ABC2A0030EC009C9E3E /* NotificationsView.swift in Sources */,
24A59AD42A00C07D009C9E3E /* UserStatsView.swift in Sources */, 24A59AD42A00C07D009C9E3E /* UserStatsView.swift in Sources */,
24A07CBC2A022B2000F4ECA8 /* ProfileViewModel.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -37,6 +37,7 @@ extension ContentView {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
MainTabView() MainTabView()
.toolbar(showMenu ? .hidden : .visible) .toolbar(showMenu ? .hidden : .visible)
if showMenu { if showMenu {
ZStack { ZStack {
Color(.black) Color(.black)
@ -54,6 +55,8 @@ extension ContentView {
.offset(x: showMenu ? 0: -300, y: 0) .offset(x: showMenu ? 0: -300, y: 0)
.background(showMenu ? Color.white: Color.clear) .background(showMenu ? Color.white: Color.clear)
} }
.navigationTitle("Home")
.navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
if let user = viewModel.currentUser { if let user = viewModel.currentUser {

View File

@ -19,6 +19,7 @@ class AuthViewModel: ObservableObject {
init() { init() {
self.userSession = Auth.auth().currentUser self.userSession = Auth.auth().currentUser
self.fetchUser() self.fetchUser()
print("DEBUG: user session is \(self.userSession?.uid)")
} }
func login(withEmail email: String, password: String) { func login(withEmail email: String, password: String) {

View File

@ -9,15 +9,15 @@ import SwiftUI
import Kingfisher import Kingfisher
struct TweetRowView: View { struct TweetRowView: View {
@ObservedObject var viewModel: TweetVeiwModel let tweet: Tweet
init(tweet: Tweet) { init(tweet: Tweet) {
self.viewModel = TweetVeiwModel(tweet: tweet) self.tweet = tweet
} }
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
// profile image + user info + tweet // profile image + user info + tweet
if let user = viewModel.tweet.user { if let user = tweet.user {
HStack(alignment: .top, spacing: 12) { HStack(alignment: .top, spacing: 12) {
// profile image // profile image
KFImage(URL(string: user.profileImageUrl)) KFImage(URL(string: user.profileImageUrl))
@ -41,7 +41,7 @@ struct TweetRowView: View {
} }
// tweet caption // tweet caption
Text(viewModel.tweet.caption) Text(tweet.caption)
.font(.subheadline) .font(.subheadline)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
} }
@ -64,11 +64,10 @@ struct TweetRowView: View {
} }
Spacer() Spacer()
Button { Button {
viewModel.tweet.didLike ?? false ? viewModel.unlikeTweet() : viewModel.likeTweet() // action here
} label: { } label: {
Image(systemName: viewModel.tweet.didLike ?? false ? "heart.fill" : "heart") Image(systemName: "heart")
.font(.subheadline) .font(.subheadline)
.foregroundColor(viewModel.tweet.didLike ?? false ? .red : .gray)
} }
Spacer() Spacer()
Button { Button {

View File

@ -1,38 +0,0 @@
//
// 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
}
}
}
}

View File

@ -22,10 +22,13 @@ struct ExploreView: View {
} label: { } label: {
UserRowView(user: user) UserRowView(user: user)
} }
} }
} }
} }
} }
.navigationTitle("Explore")
.navigationBarTitleDisplayMode(.inline)
} }
} }

View File

@ -37,6 +37,7 @@ struct FeedView: View {
.fullScreenCover(isPresented: $showNewTweetView) { .fullScreenCover(isPresented: $showNewTweetView) {
NewTweetView() NewTweetView()
} }
} }
} }
} }

View File

@ -11,7 +11,6 @@ struct MainTabView: View {
@State private var selectedIndex: Int = 0 @State private var selectedIndex: Int = 0
var body: some View { var body: some View {
NavigationView {
TabView(selection: $selectedIndex) { TabView(selection: $selectedIndex) {
FeedView() FeedView()
.onTapGesture { .onTapGesture {
@ -43,24 +42,6 @@ struct MainTabView: View {
}.tag(3) }.tag(3)
} }
} }
.navigationTitle(title(for: selectedIndex))
.navigationBarTitleDisplayMode(.inline)
}
func title(for tab: Int) -> String {
switch tab {
case 0:
return "Home"
case 1:
return "Explore"
case 2:
return "Notification"
case 3:
return "Message"
default:
return ""
}
}
} }
struct MainTabView_Previews: PreviewProvider { struct MainTabView_Previews: PreviewProvider {

View File

@ -1,63 +0,0 @@
//
// ProfileViewModel.swift
// dudu-tweet
//
// Created by ching on 2023/5/3.
//
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() {
guard let uid = user.id else { return }
service.fetchTweets(forUid: uid) { tweets in
self.tweets = tweets
for i in 0 ..< tweets.count {
self.tweets[i].user = self.user
}
}
}
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
}
}
}
}
}

View File

@ -12,12 +12,12 @@ struct ProfileView: View {
@State private var selectedFilter: TweetFilterViewModel = .tweets @State private var selectedFilter: TweetFilterViewModel = .tweets
// @Environment(\.presentationMode) var mode // explore // @Environment(\.presentationMode) var mode // explore
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@ObservedObject var viewModel: ProfileViewModel private let user: User
@Namespace var animation @Namespace var animation
init(user: User) { init(user: User) {
self.viewModel = ProfileViewModel(user: user) self.user = user
} }
var body: some View { var body: some View {
@ -65,7 +65,7 @@ extension ProfileView {
.offset(x: 16, y: -4) .offset(x: 16, y: -4)
} }
KFImage(URL(string: viewModel.user.profileImageUrl)) KFImage(URL(string: user.profileImageUrl))
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.clipShape(Circle()) .clipShape(Circle())
@ -87,7 +87,7 @@ extension ProfileView {
Button { Button {
// action here // action here
} label: { } label: {
Text(viewModel.actionBarTitle) Text("Edit Profile")
.font(.subheadline).bold() .font(.subheadline).bold()
.frame(width: 120, height: 32) .frame(width: 120, height: 32)
.foregroundColor(.black) .foregroundColor(.black)
@ -100,12 +100,12 @@ extension ProfileView {
var userInfoDetails: some View { var userInfoDetails: some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack { HStack {
Text(viewModel.user.fullname) Text(user.fullname)
.font(.title2).bold() .font(.title2).bold()
Image(systemName: "checkmark.seal.fill") Image(systemName: "checkmark.seal.fill")
.foregroundColor(Color(.systemBlue)) .foregroundColor(Color(.systemBlue))
} }
Text("@\(viewModel.user.username)") Text("@\(user.username)")
.font(.subheadline) .font(.subheadline)
.foregroundColor(.gray) .foregroundColor(.gray)
Text("一个普通人") Text("一个普通人")
@ -165,8 +165,8 @@ extension ProfileView {
var tweetsView: some View { var tweetsView: some View {
ScrollView { ScrollView {
LazyVStack { LazyVStack {
ForEach(viewModel.tweets(forFilter: self.selectedFilter)) { tweet in ForEach(0...9, id: \.self) { _ in
TweetRowView(tweet: tweet) // TweetRowView()
} }
} }
} }

View File

@ -16,5 +16,4 @@ struct Tweet: Identifiable, Decodable {
var likes: Int var likes: Int
var user: User? var user: User?
var didLike: Bool? = false
} }

View File

@ -6,7 +6,6 @@
// //
import FirebaseFirestoreSwift import FirebaseFirestoreSwift
import Firebase
struct User: Identifiable, Decodable { struct User: Identifiable, Decodable {
@DocumentID var id: String? @DocumentID var id: String?
@ -14,6 +13,4 @@ struct User: Identifiable, Decodable {
let fullname: String let fullname: String
let profileImageUrl: String let profileImageUrl: String
let email: String let email: String
var isCurrentUser: Bool { return Auth.auth().currentUser?.uid == self.id}
} }

View File

@ -30,84 +30,12 @@ struct TweetService {
func fetchTweets(completion: @escaping([Tweet]) -> Void) { func fetchTweets(completion: @escaping([Tweet]) -> Void) {
Firestore.firestore().collection("tweets") Firestore.firestore().collection("tweets").getDocuments { snapshot, _ in
.order(by: "timestamp", descending: true)
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return } guard let documents = snapshot?.documents else { return }
let tweets = documents.compactMap({ try? $0.data(as: Tweet.self) }) let tweets = documents.compactMap({ try? $0.data(as: Tweet.self) })
completion(tweets) completion(tweets)
} }
} }
func fetchTweets(forUid uid: String, completion: @escaping([Tweet]) -> Void) {
Firestore.firestore().collection("tweets")
.whereField("uid", isEqualTo: uid)
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
let tweets = documents.compactMap({ try? $0.data(as: Tweet.self) })
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)
}
}
}
}
} }