diff --git a/Secretly.xcodeproj/project.pbxproj b/Secretly.xcodeproj/project.pbxproj index 8dd4829..c6ffa1d 100644 --- a/Secretly.xcodeproj/project.pbxproj +++ b/Secretly.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 0409C25B26981ADA008301E0 /* UpdatePostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0409C25A26981ADA008301E0 /* UpdatePostService.swift */; }; + 0409C25D26981AE9008301E0 /* CreateLikeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0409C25C26981AE9008301E0 /* CreateLikeService.swift */; }; + 0409C25F26981AF9008301E0 /* DeleteLikeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0409C25E26981AF9008301E0 /* DeleteLikeService.swift */; }; + 0409C26126981B15008301E0 /* Like.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0409C26026981B15008301E0 /* Like.swift */; }; 302B5845267E658E007133E6 /* HttpResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B583D267E658E007133E6 /* HttpResponse.swift */; }; 302B5846267E658E007133E6 /* AmacaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B583E267E658E007133E6 /* AmacaConfig.swift */; }; 302B5847267E658E007133E6 /* StatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B583F267E658E007133E6 /* StatusCode.swift */; }; @@ -76,6 +80,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0409C25A26981ADA008301E0 /* UpdatePostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePostService.swift; sourceTree = ""; }; + 0409C25C26981AE9008301E0 /* CreateLikeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateLikeService.swift; sourceTree = ""; }; + 0409C25E26981AF9008301E0 /* DeleteLikeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteLikeService.swift; sourceTree = ""; }; + 0409C26026981B15008301E0 /* Like.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Like.swift; sourceTree = ""; }; 302B583D267E658E007133E6 /* HttpResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpResponse.swift; sourceTree = ""; }; 302B583E267E658E007133E6 /* AmacaConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmacaConfig.swift; sourceTree = ""; }; 302B583F267E658E007133E6 /* StatusCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusCode.swift; sourceTree = ""; }; @@ -222,6 +230,9 @@ 30C77CAF266AD69700A888DC /* CurrentUserService.swift */, 304E06C726742BDA00A99128 /* CreatePostService.swift */, 304E06C926742CC500A99128 /* FeedService.swift */, + 0409C25A26981ADA008301E0 /* UpdatePostService.swift */, + 0409C25C26981AE9008301E0 /* CreateLikeService.swift */, + 0409C25E26981AF9008301E0 /* DeleteLikeService.swift */, ); path = Services; sourceTree = ""; @@ -248,6 +259,7 @@ 307A30572661AD540020DF8B /* User.swift */, 30C77CB3266AF47300A888DC /* Credentials.swift */, 30C77CB5266AF48300A888DC /* CurrentUser.swift */, + 0409C26026981B15008301E0 /* Like.swift */, ); path = Models; sourceTree = ""; @@ -446,6 +458,7 @@ 302B5846267E658E007133E6 /* AmacaConfig.swift in Sources */, 302BB61C267D7CC800FD74F5 /* PreviewPostVIew.swift in Sources */, 3033795D267537B40066D94A /* FeedCollectionViewController+UICollectionViewDelegateFlowLayout .swift in Sources */, + 0409C26126981B15008301E0 /* Like.swift in Sources */, 30BC8BA82662CEBA00F7E6A5 /* Checksum.swift in Sources */, 30FD0E722659645A006E309A /* Faker.swift in Sources */, 30B9B939268CA989007B1942 /* Nudity.mlmodel in Sources */, @@ -454,10 +467,12 @@ 302BB626267E447900FD74F5 /* PostInputViewController+UITextFieldDelegate.swift in Sources */, 30BC8BA02662B8A700F7E6A5 /* StorageType.swift in Sources */, 307A305B2661B7A20020DF8B /* FeedCollectionViewController.swift in Sources */, + 0409C25B26981ADA008301E0 /* UpdatePostService.swift in Sources */, 304E06CC2674442800A99128 /* UIImage+encodeBase64.swift in Sources */, 30BC8BA62662C02300F7E6A5 /* CacheImage.swift in Sources */, 302B5847267E658E007133E6 /* StatusCode.swift in Sources */, 302B584B267E658E007133E6 /* RequestBuilder.swift in Sources */, + 0409C25F26981AF9008301E0 /* DeleteLikeService.swift in Sources */, 307A306526629B990020DF8B /* AuthorView.swift in Sources */, 30C77CB4266AF47300A888DC /* Credentials.swift in Sources */, 30BC8BA22662BB0000F7E6A5 /* DataContainer.swift in Sources */, @@ -467,6 +482,7 @@ 302BB624267E3A8700FD74F5 /* PostInputViewController+UIColorPickerViewControllerDelegate.swift in Sources */, 304E06CF267468DA00A99128 /* UIColor+Pastel.swift in Sources */, 304E06C42674133D00A99128 /* String+isBlank.swift in Sources */, + 0409C25D26981AE9008301E0 /* CreateLikeService.swift in Sources */, 30BC8BA42662BDEF00F7E6A5 /* ImageStore.swift in Sources */, 3033795B267537490066D94A /* FeedCollectionViewController+UICollectionViewDataSourcePrefetching.swift in Sources */, 30B9B93B268CA9E6007B1942 /* UIImage+PixelBuffer.swift in Sources */, @@ -635,13 +651,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = D3XL2U7DQC; + DEVELOPMENT_TEAM = KE5G7XBPRL; INFOPLIST_FILE = Secretly/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = mx.unam.ioslab.Secretly; + PRODUCT_BUNDLE_IDENTIFIER = Toun.Secretly; PRODUCT_NAME = Secretly; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; diff --git a/Secretly/Info.plist b/Secretly/Info.plist index 154ea82..51ca4f3 100644 --- a/Secretly/Info.plist +++ b/Secretly/Info.plist @@ -2,12 +2,6 @@ - NSPhotoLibraryUsageDescription - To add some context to your posts - NSCameraUsageDescription - To add some context to your posts - NSLocationWhenInUseUsageDescription - To georeference posts within a region CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -26,6 +20,12 @@ 1 LSRequiresIPhoneOS + NSCameraUsageDescription + To add some context to your posts + NSLocationWhenInUseUsageDescription + To georeference posts within a region + NSPhotoLibraryUsageDescription + To add some context to your posts UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Secretly/Models/Like.swift b/Secretly/Models/Like.swift new file mode 100644 index 0000000..4e45611 --- /dev/null +++ b/Secretly/Models/Like.swift @@ -0,0 +1,21 @@ +// +// Like.swift +// Secretly +// +// Created by Antonio Lara Navarrete on 09/07/21. +// Copyright © 2021 3zcurdia. All rights reserved. +// + +import Foundation +struct Like:Restable { + let id: Int? + let createdAt: Date? + let updatedAt: Date? + init(id:Int?,createdAt:Date?,updatedAt:Date?) { + self.id=id + self.createdAt=createdAt + self.updatedAt=updatedAt + } + + +} diff --git a/Secretly/Models/Post.swift b/Secretly/Models/Post.swift index eba5ff0..eafb7b1 100644 --- a/Secretly/Models/Post.swift +++ b/Secretly/Models/Post.swift @@ -21,8 +21,11 @@ struct Post: Restable { let longitude: Double? let createdAt: Date? let updatedAt: Date? + let likesCount:Int? + let liked: Bool? + - init(content: String, backgroundColor: String, latitude: Double? = nil, longitude: Double? = nil, image: UIImage? = nil) { + init(content: String, backgroundColor: String, latitude: Double? = nil, longitude: Double? = nil, image: UIImage? = nil,likesCount:Int?,liked:Bool?) { self.content = content self.backgroundColor = backgroundColor self.id = nil @@ -34,6 +37,8 @@ struct Post: Restable { self.commentsCount = nil self.createdAt = nil self.updatedAt = nil + self.likesCount = nil + self.liked = nil } func encode(to encoder: Encoder) throws { diff --git a/Secretly/Network/HttpClient.swift b/Secretly/Network/HttpClient.swift index 0554709..40983b9 100644 --- a/Secretly/Network/HttpClient.swift +++ b/Secretly/Network/HttpClient.swift @@ -31,10 +31,10 @@ struct HttpClient { } private func request(method: String, path: String, body: Data?, complete: @escaping ResultResponse) { - guard let req = buildRequest(method: method, path: path, body: body) else { - complete(.failure(RequestError.invalidRequest)) - return - } + guard let req = RequestBuilder.build(baseUrl: self.baseUrl, method: method, path: path, body: body) else { + complete(.failure(RequestError.invalidRequest)) + return + } session.dataTask(with: req) { (data, response, error) in if let error = error { @@ -46,15 +46,4 @@ struct HttpClient { complete(result) }.resume() } - - private func buildRequest(method: String, path: String, body: Data?) -> URLRequest? { - var builder = RequestBuilder(baseUrl: self.baseUrl) - builder.method = method - builder.path = path - builder.body = body - if let token = AmacaConfig.shared.apiToken { - builder.headers = ["Authorization": "Bearer \(token)"] - } - return builder.request() - } } diff --git a/Secretly/Network/HttpResponse.swift b/Secretly/Network/HttpResponse.swift index eb0543a..7de7806 100644 --- a/Secretly/Network/HttpResponse.swift +++ b/Secretly/Network/HttpResponse.swift @@ -20,6 +20,13 @@ struct HttpResponse { } func result(for data: Data?) -> Result { + #if DEBUG + debugPrint(status) + print("\(self.httpUrlResponse.statusCode) \(httpUrlResponse.url!)") + if let unwrapedData = data, let currentData = String(data: unwrapedData, encoding: .utf8) { + print(currentData) + } + #endif return status.result().map { _ in data } } } diff --git a/Secretly/Network/RequestBuilder.swift b/Secretly/Network/RequestBuilder.swift index c29e13e..e0480e2 100644 --- a/Secretly/Network/RequestBuilder.swift +++ b/Secretly/Network/RequestBuilder.swift @@ -7,8 +7,20 @@ // import Foundation - -struct RequestBuilder { +struct RequestBuilder: CustomDebugStringConvertible { + static func build(baseUrl: String, method: String, path: String, body: Data?) -> URLRequest? { + var builder = RequestBuilder(baseUrl: baseUrl) + builder.method = method + builder.path = path + builder.body = body + if let token = AmacaConfig.shared.apiToken { + builder.headers = ["Authorization": "Bearer \(token)"] + } + #if DEBUG + debugPrint(builder) + #endif + return builder.request() + } enum ContentMode { case jsonApp @@ -33,7 +45,15 @@ struct RequestBuilder { public var body: Data? public var headers: [String: String]? public var contentMode: ContentMode = .jsonApp - + var debugDescription: String { + let currentUrl: String = url()?.debugDescription ?? "Invalid url" + let currentHeaders: String = headers?.debugDescription ?? "" + if let unwrapedbody = body, let currentBody = String(data: unwrapedbody, encoding: .utf8) { + return "\(method.uppercased()) \(currentUrl) -H \(currentHeaders) -d \(currentBody)" + } else { + return "\(method.uppercased()) \(currentUrl) -H \(currentHeaders)" + } + } init(baseUrl: String) { self.urlComponents = URLComponents(string: baseUrl)! } diff --git a/Secretly/Network/RestClient.swift b/Secretly/Network/RestClient.swift index 3048ccf..bf272df 100644 --- a/Secretly/Network/RestClient.swift +++ b/Secretly/Network/RestClient.swift @@ -61,14 +61,14 @@ struct RestClient { func update(model: T, complete: @escaping (Result) -> Void) throws { let data = try encoder.encode(model) - client.put(path: "\(path)/\(model.id)", body: data) { result in + client.put(path: path, body: data) { result in let newResult = result.flatMap { parse(data: $0) } complete(newResult) } } func delete(model: T, complete: @escaping (Result) -> Void) { - client.delete(path: "\(path)/\(model.id)") { result in + client.delete(path: path) { result in let newResult = result.flatMap { parse(data: $0) } complete(newResult) } diff --git a/Secretly/Services/CreateLikeService.swift b/Secretly/Services/CreateLikeService.swift new file mode 100644 index 0000000..0fa0bf6 --- /dev/null +++ b/Secretly/Services/CreateLikeService.swift @@ -0,0 +1,22 @@ +// +// CreateLikeService.swift +// Secretly +// +// Created by Antonio Lara Navarrete on 09/07/21. +// Copyright © 2021 3zcurdia. All rights reserved. +// + +import Foundation + +struct CreateLikeService { + private var endpoint: RestClient + init(id:Int) { + endpoint = RestClient(client: AmacaConfig.shared.httpClient, path: "/api/v1/posts/\(id)/likes") + } + + func create(_ model: Like, complete: @escaping (Result) -> Void ) { + try? endpoint.create(model: model) { result in + DispatchQueue.main.async { complete(result) } + } + } +} diff --git a/Secretly/Services/DeleteLikeService.swift b/Secretly/Services/DeleteLikeService.swift new file mode 100644 index 0000000..d5b1141 --- /dev/null +++ b/Secretly/Services/DeleteLikeService.swift @@ -0,0 +1,21 @@ +// +// DeleteLikeService.swift +// Secretly +// +// Created by Antonio Lara Navarrete on 09/07/21. +// Copyright © 2021 3zcurdia. All rights reserved. +// +import Foundation + +struct DeleteLikeService { + private var endpoint: RestClient + init(id:Int) { + endpoint = RestClient(client: AmacaConfig.shared.httpClient, path: "/api/v1/posts/\(id)/likes") + } + + func delete(_ model: Like, complete: @escaping (Result) -> Void ) { + endpoint.delete(model: model) { result in + DispatchQueue.main.async { complete(result) } + } + } +} diff --git a/Secretly/Services/UpdatePostService.swift b/Secretly/Services/UpdatePostService.swift new file mode 100644 index 0000000..8dc9171 --- /dev/null +++ b/Secretly/Services/UpdatePostService.swift @@ -0,0 +1,23 @@ +// +// UpdatePostService.swift +// Secretly +// +// Created by Antonio Lara Navarrete on 09/07/21. +// Copyright © 2021 3zcurdia. All rights reserved. +// + +import Foundation + +struct UpdatePostService { + private var endpoint: RestClient + + init(id: Int) { + endpoint = RestClient(client: AmacaConfig.shared.httpClient, path: "/api/v1/posts/\(id)") + } + + func update(_ model: Post, complete: @escaping (Result) -> Void ) { + try? endpoint.update(model: model) { result in + DispatchQueue.main.async { complete(result) } + } + } +} diff --git a/Secretly/ViewControllers/CreatePostViewController.swift b/Secretly/ViewControllers/CreatePostViewController.swift index 4091252..6dc8395 100644 --- a/Secretly/ViewControllers/CreatePostViewController.swift +++ b/Secretly/ViewControllers/CreatePostViewController.swift @@ -19,7 +19,7 @@ class CreatePostViewController: UIViewController { @IBAction func createPost(_ sender: Any?) { - let post = Post(content: contentField.text!, backgroundColor: colorField.text!) + let post = Post(content: contentField.text!, backgroundColor: colorField.text!,likesCount: 0,liked:false) let postsEndpoint = RestClient(client: AmacaConfig.shared.httpClient, path: "/api/v1/posts") do { diff --git a/Secretly/ViewControllers/PostInputViewController.swift b/Secretly/ViewControllers/PostInputViewController.swift index 676ce35..ce6ba4e 100644 --- a/Secretly/ViewControllers/PostInputViewController.swift +++ b/Secretly/ViewControllers/PostInputViewController.swift @@ -141,7 +141,9 @@ class PostInputViewController: UIViewController, UINavigationControllerDelegate content: postText, backgroundColor: previewPost.backgroundColor?.hexString ?? "#3366CC", latitude: currentLocation?.latitude, - longitude: currentLocation?.longitude + longitude: currentLocation?.longitude, + likesCount: 0, + liked:false ) if let uimage = previewPost.image { post.imageData = uimage.encodeBase64() diff --git a/Secretly/Views/PostCollectionViewCell.swift b/Secretly/Views/PostCollectionViewCell.swift index ee08d53..b1f37b0 100644 --- a/Secretly/Views/PostCollectionViewCell.swift +++ b/Secretly/Views/PostCollectionViewCell.swift @@ -10,27 +10,111 @@ import UIKit class PostCollectionViewCell: UICollectionViewCell { static let reuseIdentifier = "feedPostCell" + var imgP: UIImage? var post: Post? { didSet { updateView() } } + var likePostState:Bool? + var like :Like?{ + didSet{ + updateView() + } + } + + @IBOutlet weak var bigLikeState: UIImageView! @IBOutlet weak var authorView: AuthorView! @IBOutlet weak var contentLabel: UILabel! @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var likeState: UIImageView! @IBOutlet weak var commentCounter: UILabel! - + @IBOutlet weak var likeLabel: UILabel! + override func awakeFromNib() { super.awakeFromNib() + let tap=UITapGestureRecognizer(target: self, action: #selector(tapLike(_:))) + let doubleTap=UITapGestureRecognizer(target: self, action: #selector(tapLike(_:))) + doubleTap.numberOfTapsRequired=2 + imageView.isUserInteractionEnabled=true + imageView.addGestureRecognizer(doubleTap) + likeState.isUserInteractionEnabled=true + likeState.addGestureRecognizer(tap) + } + @objc func tapLike(_ sender: UITapGestureRecognizer) { + guard let post=post, let id=post.id, let postState=post.liked else{return} + let updateLike=Like(id: id, createdAt: Date(), updatedAt: Date()) + + if postState || self.likePostState ?? postState { + print("Tiene like") +// Eliminar like + let delLike=DeleteLikeService(id:id) + delLike.delete(updateLike) { resultDelLike in + switch resultDelLike { + case .success: + print("Like borrado") + self.likePostState=false + self.updatePost(id: id, sum: -1,state: self.likePostState!) + case .failure: + print("Like NO borrado") + self.bigLikeState.isHidden=true + self.likeState.tintColor = .red + + } + } + + }else if (postState || self.likePostState ?? postState) == false { + print("No tiene like") + let createLike=CreateLikeService(id: id) + createLike.create(updateLike) { resultDelLike in + switch resultDelLike { + case .success: + print("Like creado") + self.likePostState=true + self.updatePost(id: id, sum: 1,state: self.likePostState!) + self.bigLikeState.isHidden=false + self.likeState.tintColor = .white + case .failure: + print("Like NO creado") + } + } + } + updateView() + } + func updatePost(id:Int,sum:Int,state:Bool) { + self.likePostState=state + guard let post = post, let id=post.id, let countLikes=post.likesCount else{return} + let newLikes = countLikes + sum + if let imgP = post.image { + ImageLoader.load(imgP.mediumUrl) { img in self.imageView.image = img } + } + let updatePost=UpdatePostService(id: id) + + if let postImg = post.image { + ImageLoader.load(postImg.mediumUrl) { img in self.imgP = img } + } + + + let newPost=Post(content: post.content, backgroundColor: post.backgroundColor, latitude: post.latitude, longitude: post.longitude, image: imgP, likesCount: newLikes, liked: state) + updatePost.update(newPost) { resultUpdatePost in + switch resultUpdatePost { + case .success: + print("Post updated") + self.likeLabel.text=String(describing: newLikes) + case .failure: + print("Post not updated") + } + } } func updateView() { imageView.image = nil guard let post = post else { return } + self.likePostState=post.liked if let color = UIColor(hex: post.backgroundColor) { self.backgroundColor = color } + checkStatus() self.contentLabel.text = post.content self.commentCounter.text = String(describing: post.commentsCount ?? 0) if let postImg = post.image { @@ -38,4 +122,17 @@ class PostCollectionViewCell: UICollectionViewCell { } self.authorView.author = post.user } + + func checkStatus(){ + guard let post=post, let likePostState=post.liked, let likesCount = post.likesCount else{return} + if likePostState || self.likePostState!{ + likeState.tintColor = .red + bigLikeState.isHidden = true + }else if (likePostState || self.likePostState!)==false { + likeState.tintColor = .white + bigLikeState.isHidden = true + } + + likeLabel.text = String(describing: likesCount) + } } diff --git a/Secretly/Views/PostCollectionViewCell.xib b/Secretly/Views/PostCollectionViewCell.xib index 7b3a4e2..f1f9e67 100644 --- a/Secretly/Views/PostCollectionViewCell.xib +++ b/Secretly/Views/PostCollectionViewCell.xib @@ -62,6 +62,16 @@ + + + + + @@ -69,26 +79,36 @@ + + + + + + + + + + @@ -96,9 +116,13 @@ + + + +