diff --git a/Packages/Backend/Sources/Backend/Extensions/URL+StaticString.swift b/Packages/Backend/Sources/Backend/Extensions/URL+StaticString.swift new file mode 100644 index 0000000..1e257f3 --- /dev/null +++ b/Packages/Backend/Sources/Backend/Extensions/URL+StaticString.swift @@ -0,0 +1,19 @@ +// +// URL+StaticString.swift +// +// +// Created by Dan Korkelia on 01/01/2021. +// + +import Foundation + +extension URL { + /// Use this init for static URL strings to avoid using force unwrap or doing redundant error handling + /// - Parameter string: static url ie https://www.example.com/privacy/ + init(staticString: StaticString) { + guard let url = URL(string: "\(staticString)") else { + fatalError("URL is illegal: \(staticString)") + } + self = url + } +} diff --git a/Packages/Backend/Sources/Backend/Models/Award.swift b/Packages/Backend/Sources/Backend/Models/Award.swift new file mode 100644 index 0000000..1273d42 --- /dev/null +++ b/Packages/Backend/Sources/Backend/Models/Award.swift @@ -0,0 +1,24 @@ +// +// File.swift +// +// +// Created by Thomas Ricouard on 05/08/2020. +// + +import Foundation + +public struct Award: Decodable, Identifiable { + public let id: String + public let name: String + public let staticIconUrl: URL + public let description: String + public let count: Int + public let coinPrice: Int + + public static let `default` = Award(id: "award", + name: "Awesome", + staticIconUrl: URL(staticString: "https://i.redd.it/award_images/t5_22cerq/5smbysczm1w41_Hugz.png"), + description: "Awesome reward", + count: 5, + coinPrice: 200) +} diff --git a/Packages/Backend/Sources/Backend/Models/Comment.swift b/Packages/Backend/Sources/Backend/Models/Comment.swift index 616113c..99efb3f 100644 --- a/Packages/Backend/Sources/Backend/Models/Comment.swift +++ b/Packages/Backend/Sources/Backend/Models/Comment.swift @@ -17,12 +17,27 @@ public struct Comment: Decodable, Identifiable { public let id: String public let name: String public let body: String? + public let isSubmitter: Bool? public let author: String? public let lindId: String? public let created: Date? public let createdUtc: Date? public let replies: Replies? - public let score: Int? + public var score: Int? + public var likes: Bool? + public let allAwardings: [Award]? + public var saved: Bool? + + public let permalink: String? + public var permalinkURL: URL? { + guard let permalink = permalink else { return nil } + return URL(string: "https://reddit.com\(permalink)") + } + + public let authorFlairRichtext: [FlairRichText]? + public let authorFlairText: String? + public let authorFlairTextColor: String? + public let authorFlairBackgroundColor: String? public var repliesComments: [Comment]? { if let replies = replies { @@ -53,11 +68,19 @@ public enum Replies: Decodable { public let static_comment = Comment(id: UUID().uuidString, name: "t1_id", - body: "Comment text with a long line of text \n and another line.", + body: "Comment text with a long line of text\nThis is another line.", + isSubmitter: false, author: "TestUser", lindId: "", created: Date(), createdUtc: Date(), replies: .none(""), - score: 2500) + score: 2500, + allAwardings: [], + saved: false, + permalink: "", + authorFlairRichtext: nil, + authorFlairText: nil, + authorFlairTextColor: nil, + authorFlairBackgroundColor: nil) public let static_comments = [static_comment, static_comment, static_comment] diff --git a/Packages/Backend/Sources/Backend/Models/FlairRichText.swift b/Packages/Backend/Sources/Backend/Models/FlairRichText.swift new file mode 100644 index 0000000..7b5209e --- /dev/null +++ b/Packages/Backend/Sources/Backend/Models/FlairRichText.swift @@ -0,0 +1,23 @@ +// +// File.swift +// +// +// Created by Thomas Ricouard on 10/08/2020. +// + +import Foundation + +public struct FlairRichText: Decodable, Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(e) + hasher.combine(u) + hasher.combine(t) + } + + /// type + public let e: String + /// image URL + public let u: URL? + /// text + public let t: String? +} diff --git a/Packages/Backend/Sources/Backend/Models/Listing.swift b/Packages/Backend/Sources/Backend/Models/Listing.swift index ff8f781..ad98b5d 100644 --- a/Packages/Backend/Sources/Backend/Models/Listing.swift +++ b/Packages/Backend/Sources/Backend/Models/Listing.swift @@ -28,14 +28,24 @@ public struct ListingHolder: Decodable { case kind, data } - public init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws + where T == GenericListingContent + { let container = try decoder.container(keyedBy: CodingKeys.self) kind = try container.decode(String.self, forKey: .kind) - if T.self == GenericListingContent.self { - data = try GenericListingContent(from: decoder) as! T - } else { - data = try container.decode(T.self, forKey: .data) - } + data = try T(from: decoder) + } + + public init(from decoder: Decoder) throws + { + let container = try decoder.container(keyedBy: CodingKeys.self) + kind = try container.decode(String.self, forKey: .kind) + // This assert can be removed before merging the + // patch. It's just here to verify that + // the specialized init, above, is used for + // when T is a GenericListingContent. + assert(T.self != GenericListingContent.self) + data = try container.decode(T.self, forKey: .data) } } diff --git a/Packages/Backend/Sources/Backend/Models/Media.swift b/Packages/Backend/Sources/Backend/Models/Media.swift new file mode 100644 index 0000000..9d07fbf --- /dev/null +++ b/Packages/Backend/Sources/Backend/Models/Media.swift @@ -0,0 +1,52 @@ +// +// File.swift +// +// +// Created by Thomas Ricouard on 05/08/2020. +// + +import Foundation + +public struct SecureMedia: Decodable { + public let redditVideo: RedditVideo? + public let oembed: Oembed? + + public var video: Video? { + if let video = redditVideo { + return Video(url: video.fallbackUrl, width: video.width, height: video.height) + } else if oembed?.type == "video", + let oembed = oembed, + let url = oembed.url, + let width = oembed.width, + let height = oembed.height { + return Video(url: url, width: width, height: height) + } + return nil + } +} + +public struct RedditVideo: Decodable { + public let fallbackUrl: URL + public let height: Int + public let width: Int +} + +public struct Oembed: Decodable { + public let providerUrl: URL? + public let thumbnailUrl: String? + public var thumbnailUrlAsURL: URL? { + thumbnailUrl != nil ? URL(string: thumbnailUrl!) : nil + } + public let url: URL? + public let width: Int? + public let height: Int? + public let thumbnailWidth: Int? + public let thumbnailHeight: Int? + public let type: String? +} + +public struct Video { + public let url: URL + public let width: Int + public let height: Int +} diff --git a/Packages/Backend/Sources/Backend/Models/Subreddit.swift b/Packages/Backend/Sources/Backend/Models/Subreddit.swift index 5edc107..f5fffaa 100644 --- a/Packages/Backend/Sources/Backend/Models/Subreddit.swift +++ b/Packages/Backend/Sources/Backend/Models/Subreddit.swift @@ -2,6 +2,7 @@ import Foundation public struct Subreddit: Codable, Identifiable { public let id: String + public let name: String public let displayName: String public let title: String public let publicDescription: String? @@ -12,4 +13,26 @@ public struct Subreddit: Codable, Identifiable { public let bannerImg: String? public let subscribers: Int? public let accountsActive: Int? + public let createdUtc: Date + public let url: String + public var redditURL: URL { + URL(string: "https://reddit.com\(url)")! + } + public var userIsSubscriber: Bool? } + +public let static_subreddit_full = Subreddit(id: "games", + name: "t3_fjfj", + displayName: "games", + title: "games", + publicDescription: "a description", + primaryColor: "#545452", + keyColor: "#545452", + bannerBackgroundColor: "#545452", + iconImg: "https://a.thumbs.redditmedia.com/8hr1PTpJ9iWLNWP67vZN0w3IEP8uI3eAQ1kE4XLRg88.png", + bannerImg: "https://a.thumbs.redditmedia.com/8hr1PTpJ9iWLNWP67vZN0w3IEP8uI3eAQ1kE4XLRg88.png", + subscribers: 1000, + accountsActive: 500, + createdUtc: Date(), + url: "/r/games", + userIsSubscriber: false) diff --git a/Packages/Backend/Sources/Backend/Models/SubredditPost.swift b/Packages/Backend/Sources/Backend/Models/SubredditPost.swift index 883b451..2a28024 100644 --- a/Packages/Backend/Sources/Backend/Models/SubredditPost.swift +++ b/Packages/Backend/Sources/Backend/Models/SubredditPost.swift @@ -9,7 +9,9 @@ import Foundation public struct SubredditPost: Decodable, Identifiable, Hashable { public static func == (lhs: SubredditPost, rhs: SubredditPost) -> Bool { - lhs.id == rhs.id && lhs.likes == rhs.likes + lhs.id == rhs.id && + lhs.likes == rhs.likes && + lhs.saved == rhs.saved } public func hash(into hasher: inout Hasher) { @@ -39,9 +41,18 @@ public struct SubredditPost: Decodable, Identifiable, Hashable { public let secureMedia: SecureMedia? public let url: String? public let permalink: String? + public let linkFlairText: String? + public let linkFlairRichtext: [FlairRichText]? public let linkFlairBackgroundColor: String? public let linkFlairTextColor: String? + + public let authorFlairRichtext: [FlairRichText]? + public let authorFlairText: String? + public let authorFlairTextColor: String? + public let authorFlairBackgroundColor: String? + + public let allAwardings: [Award] public var visited: Bool public var saved: Bool public var redditURL: URL? { @@ -53,50 +64,6 @@ public struct SubredditPost: Decodable, Identifiable, Hashable { public var likes: Bool? } -public struct SecureMedia: Decodable { - public let redditVideo: RedditVideo? - public let oembed: Oembed? - - public var video: Video? { - if let video = redditVideo { - return Video(url: video.fallbackUrl, width: video.width, height: video.height) - } else if oembed?.type == "video", - let oembed = oembed, - let url = oembed.url, - let width = oembed.width, - let height = oembed.height { - return Video(url: url, width: width, height: height) - } - return nil - } -} - -public struct RedditVideo: Decodable { - public let fallbackUrl: URL - public let height: Int - public let width: Int -} - -public struct Oembed: Decodable { - public let providerUrl: URL? - public let thumbnailUrl: String? - public var thumbnailUrlAsURL: URL? { - thumbnailUrl != nil ? URL(string: thumbnailUrl!) : nil - } - public let url: URL? - public let width: Int? - public let height: Int? - public let thumbnailWidth: Int? - public let thumbnailHeight: Int? - public let type: String? -} - -public struct Video { - public let url: URL - public let width: Int - public let height: Int -} - public let static_listing = SubredditPost(id: "0", name: "t3_0", title: "A very long title to be able to debug the UI correctly as it should be displayed on mutliple lines.", @@ -114,8 +81,14 @@ public let static_listing = SubredditPost(id: "0", url: "https://test.com", permalink: nil, linkFlairText: nil, + linkFlairRichtext: nil, linkFlairBackgroundColor: nil, linkFlairTextColor: nil, + authorFlairRichtext: nil, + authorFlairText: nil, + authorFlairTextColor: nil, + authorFlairBackgroundColor: nil, + allAwardings: [], visited: false, saved: false, likes: nil) diff --git a/Packages/Backend/Sources/Backend/Models/SubredditSmall.swift b/Packages/Backend/Sources/Backend/Models/SubredditSmall.swift index c713bd3..ca3a1ca 100644 --- a/Packages/Backend/Sources/Backend/Models/SubredditSmall.swift +++ b/Packages/Backend/Sources/Backend/Models/SubredditSmall.swift @@ -9,6 +9,10 @@ public struct SubredditResponse: Decodable { } public struct SubredditSmall: Codable, Identifiable, Equatable, Hashable { + public static func == (lhs: SubredditSmall, rhs: SubredditSmall) -> Bool { + lhs.id == rhs.id + } + public var id: String { name } public let name: String public let subscriberCount: Int diff --git a/Packages/Backend/Sources/Backend/Models/Vote.swift b/Packages/Backend/Sources/Backend/Models/Vote.swift new file mode 100644 index 0000000..d92e24d --- /dev/null +++ b/Packages/Backend/Sources/Backend/Models/Vote.swift @@ -0,0 +1,12 @@ +// +// File.swift +// +// +// Created by Thomas Ricouard on 12/08/2020. +// + +import Foundation + +public enum Vote: Int { + case upvote = 1, downvote = -1, neutral = 0 +} diff --git a/Packages/Backend/Sources/Backend/Network/Endpoint.swift b/Packages/Backend/Sources/Backend/Network/Endpoint.swift index f37172f..668ca16 100644 --- a/Packages/Backend/Sources/Backend/Network/Endpoint.swift +++ b/Packages/Backend/Sources/Backend/Network/Endpoint.swift @@ -3,6 +3,7 @@ import Foundation public enum Endpoint { case subreddit(name: String, sort: String?) case subredditAbout(name: String) + case subscribe case searchSubreddit case comments(name: String, id: String) case accessToken @@ -26,6 +27,8 @@ public enum Endpoint { } case .searchSubreddit: return "api/search_subreddits" + case .subscribe: + return "api/subscribe" case let .comments(name, id): return "r/\(name)/comments/\(id)" case .accessToken: diff --git a/Packages/Backend/Sources/Backend/Network/Models/Comment+Networking.swift b/Packages/Backend/Sources/Backend/Network/Models/Comment+Networking.swift index d410d22..66b4a1c 100644 --- a/Packages/Backend/Sources/Backend/Network/Models/Comment+Networking.swift +++ b/Packages/Backend/Sources/Backend/Network/Models/Comment+Networking.swift @@ -9,10 +9,39 @@ import Foundation import Combine extension Comment { - static public func fetch(subreddit: String, id: String) -> AnyPublisher<[ListingResponse], Never> { - API.shared.request(endpoint: .comments(name: subreddit, id: id)) + public enum Sort: String, CaseIterable { + case best = "confidence" + case top, new, controversial, old, qa + } + + static public func fetch(subreddit: String, id: String, sort: Sort = .top) -> AnyPublisher<[ListingResponse], Never> { + let params: [String: String] = ["sort": sort.rawValue] + return API.shared.request(endpoint: .comments(name: subreddit, id: id), params: params) .subscribe(on: DispatchQueue.global()) .replaceError(with: []) .eraseToAnyPublisher() } + + public mutating func vote(vote: Vote) -> AnyPublisher { + switch vote { + case .upvote: + likes = true + case .downvote: + likes = false + case .neutral: + likes = nil + } + return API.shared.POST(endpoint: .vote, + params: ["id": name, "dir": "\(vote.rawValue)"]) + } + + public mutating func save() -> AnyPublisher { + saved = true + return API.shared.POST(endpoint: .save, params: ["id": name]) + } + + public mutating func unsave() -> AnyPublisher { + saved = false + return API.shared.POST(endpoint: .unsave, params: ["id": name]) + } } diff --git a/Packages/Backend/Sources/Backend/Network/Models/Subreddit+Networking.swift b/Packages/Backend/Sources/Backend/Network/Models/Subreddit+Networking.swift index 7c19a42..fb5623d 100644 --- a/Packages/Backend/Sources/Backend/Network/Models/Subreddit+Networking.swift +++ b/Packages/Backend/Sources/Backend/Network/Models/Subreddit+Networking.swift @@ -21,5 +21,15 @@ extension Subreddit { .replaceError(with: nil) .eraseToAnyPublisher() } + + public mutating func subscribe() -> AnyPublisher { + userIsSubscriber = true + return API.shared.POST(endpoint: .subscribe, params: ["action": "sub", "sr": name]) + } + + public mutating func unSubscribe() -> AnyPublisher { + userIsSubscriber = false + return API.shared.POST(endpoint: .subscribe, params: ["action": "unsub", "sr": name]) + } } diff --git a/Packages/Backend/Sources/Backend/Network/Models/SubredditPost+Networking.swift b/Packages/Backend/Sources/Backend/Network/Models/SubredditPost+Networking.swift index 5d2791c..04df462 100644 --- a/Packages/Backend/Sources/Backend/Network/Models/SubredditPost+Networking.swift +++ b/Packages/Backend/Sources/Backend/Network/Models/SubredditPost+Networking.swift @@ -8,11 +8,7 @@ import Foundation import Combine -extension SubredditPost { - public enum Vote: Int { - case upvote = 1, downvote = -1, neutral = 0 - } - +extension SubredditPost { static public func fetch(subreddit: String, sort: String, after: SubredditPost?) -> AnyPublisher, Never> { diff --git a/Packages/Backend/Sources/Backend/Network/OauthClient.swift b/Packages/Backend/Sources/Backend/Network/OauthClient.swift index 741d855..b1994ad 100644 --- a/Packages/Backend/Sources/Backend/Network/OauthClient.swift +++ b/Packages/Backend/Sources/Backend/Network/OauthClient.swift @@ -24,7 +24,8 @@ public class OauthClient: ObservableObject { private let baseURL = "https://www.reddit.com/api/v1/authorize" private let secrets: [String: AnyObject]? private let scopes = ["mysubreddits", "identity", "edit", "save", - "vote", "subscribe", "read", "submit", "history"] + "vote", "subscribe", "read", "submit", "history", + "privatemessages"] private let state = UUID().uuidString private let redirectURI = "redditos://auth" private let duration = "permanent" diff --git a/Packages/Backend/Sources/Backend/User/CurrentUserStore.swift b/Packages/Backend/Sources/Backend/User/CurrentUserStore.swift index 5c746fc..e0544ca 100644 --- a/Packages/Backend/Sources/Backend/User/CurrentUserStore.swift +++ b/Packages/Backend/Sources/Backend/User/CurrentUserStore.swift @@ -4,6 +4,8 @@ import Combine public class CurrentUserStore: ObservableObject, PersistentDataStore { + public static let shared = CurrentUserStore() + @Published public private(set) var user: User? { didSet { persistData(data: SaveData(user: user, diff --git a/Packages/Backend/Sources/Backend/User/LocalDataStore.swift b/Packages/Backend/Sources/Backend/User/LocalDataStore.swift index e38e758..ed7267b 100644 --- a/Packages/Backend/Sources/Backend/User/LocalDataStore.swift +++ b/Packages/Backend/Sources/Backend/User/LocalDataStore.swift @@ -15,7 +15,9 @@ public class LocalDataStore: ObservableObject, PersistentDataStore { } public init() { - self.favorites = restorePersistedData()?.favorites ?? [] + var favorites = restorePersistedData()?.favorites ?? [] + favorites.sort{ $0.name.lowercased() < $1.name.lowercased() } + self.favorites = favorites } // MARK: - Favorites management diff --git a/Packages/Backend/Tests/BackendTests/Models/AwardTests.swift b/Packages/Backend/Tests/BackendTests/Models/AwardTests.swift new file mode 100644 index 0000000..7256f4c --- /dev/null +++ b/Packages/Backend/Tests/BackendTests/Models/AwardTests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import Backend + +final class AwardTests: XCTestCase { + + func test_defaultValueAreCorrect() { + XCTAssertEqual(Award.default.id, "award") + XCTAssertEqual(Award.default.name, "Awesome") + XCTAssertEqual(Award.default.staticIconUrl, URL(staticString: "https://i.redd.it/award_images/t5_22cerq/5smbysczm1w41_Hugz.png")) + XCTAssertEqual(Award.default.description, "Awesome reward") + XCTAssertEqual(Award.default.count, 5) + XCTAssertEqual(Award.default.coinPrice, 200) + } + +} diff --git a/Packages/UI/.gitignore b/Packages/UI/.gitignore new file mode 100644 index 0000000..95c4320 --- /dev/null +++ b/Packages/UI/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/Packages/UI/Package.swift b/Packages/UI/Package.swift new file mode 100644 index 0000000..3fa773a --- /dev/null +++ b/Packages/UI/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "UI", + platforms: [ + .macOS("11"), .iOS("14"), .tvOS("14"), .watchOS("7") + ], + products: [ + .library( + name: "UI", + targets: ["UI"]), + ], + targets: [ + .target( + name: "UI", + dependencies: []), + .testTarget( + name: "UITests", + dependencies: ["UI"]), + ] +) diff --git a/Packages/UI/README.md b/Packages/UI/README.md new file mode 100644 index 0000000..096aa1d --- /dev/null +++ b/Packages/UI/README.md @@ -0,0 +1,3 @@ +# UI + +A description of this package. diff --git a/Packages/UI/Sources/UI/RecursiveView.swift b/Packages/UI/Sources/UI/RecursiveView.swift new file mode 100644 index 0000000..6d69bb7 --- /dev/null +++ b/Packages/UI/Sources/UI/RecursiveView.swift @@ -0,0 +1,64 @@ +// +// SwiftUIView.swift +// +// +// Created by Thomas Ricouard on 12/08/2020. +// + +import SwiftUI + +public struct RecursiveView: View where Data: RandomAccessCollection, + Data.Element: Identifiable, + RowContent: View { + let data: Data + let children: KeyPath + let rowContent: (Data.Element) -> RowContent + + public init(data: Data, children: KeyPath, rowContent: @escaping (Data.Element) -> RowContent) { + self.data = data + self.children = children + self.rowContent = rowContent + } + + public var body: some View { + ForEach(data) { child in + if self.containsSub(child) { + CustomDisclosureGroup(content: { + RecursiveView(data: child[keyPath: children]!, + children: children, + rowContent: rowContent) + .padding(.leading, 8) + }, label: { + rowContent(child) + }) + } else { + rowContent(child) + } + } + } + + func containsSub(_ element: Data.Element) -> Bool { + element[keyPath: children] != nil + } +} + +struct CustomDisclosureGroup: View where Label: View, Content: View { + @State var isExpanded: Bool = true + var content: () -> Content + var label: () -> Label + + var body: some View { + HStack(alignment: .top, spacing: 8) { + Image(systemName: "chevron.right") + .rotationEffect(isExpanded ? .degrees(90) : .degrees(0)) + .padding(.top, 4) + .onTapGesture { + isExpanded.toggle() + } + label() + } + if isExpanded { + content() + } + } +} diff --git a/Packages/UI/Tests/UITests/UITests.swift b/Packages/UI/Tests/UITests/UITests.swift new file mode 100644 index 0000000..4fc13f2 --- /dev/null +++ b/Packages/UI/Tests/UITests/UITests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import UI + +final class UITests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(UI().text, "Hello, World!") + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/Packages/UI/Tests/UITests/XCTestManifests.swift b/Packages/UI/Tests/UITests/XCTestManifests.swift new file mode 100644 index 0000000..aff9111 --- /dev/null +++ b/Packages/UI/Tests/UITests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(UITests.allTests), + ] +} +#endif diff --git a/README.md b/README.md index 76560f8..5168441 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # RedditOS A SwiftUI Reddit client for macOS -This is bleeding edge, you need macOS Big Sur beta and Xcode 12 beta. +You'll need Xcode 12 and macOS Big Sur 12 to build & run the app. +This is quite bleeding edge, the performances are not quite there yet as SwiftUI higher order components on macOS (like `List` and `NavigationView`) are not really very smooth yet. I hope it'll improve during Big Sur update cycle and I exept a big boost with SwiftUI 3 next summer. (One can hope) +But I'll continue to work on this application, add features, optimize it and eventually release it on the Mac App Store. ![Image](Images/image1.png?) -If you want to login with your Reddit account building the project from the source you'll need to create a file `secrets.plist` in `Packages/Backend/Sources/Backend/Resources` with your Reddit app secret as `client_id` key/value. +If you want to login with your Reddit account building the project from the source you'll need to create a file `secrets.plist` in `Packages/Backend/Sources/Backend/Resources` with your Reddit app id as `client_id` key/value. Create an reddit app [here](https://www.reddit.com/prefs/apps) and use `redditos://auth` as redirect url. I'll periodically release pre built version of the app in the repository. -The app is still early and SwiftUI on macOS is not that fast (yet) but I'm planning to release it on the Mac App Store with the release of Big Sur. \ No newline at end of file +The app is still early and SwiftUI on macOS is not that fast (yet) but I'm planning to release it on the Mac App Store with the release of Big Sur. diff --git a/RedditOs.xcodeproj/project.pbxproj b/RedditOs.xcodeproj/project.pbxproj index 655271f..6c9da07 100644 --- a/RedditOs.xcodeproj/project.pbxproj +++ b/RedditOs.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 6906880D24B743900067D973 /* SubredditPostRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6906880C24B743900067D973 /* SubredditPostRow.swift */; }; + 691747E224DBEA240017E068 /* GlobalSearchSubRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691747E124DBEA240017E068 /* GlobalSearchSubRow.swift */; }; 6918A8CB24C1FEDC008A74E1 /* FlairView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6918A8CA24C1FEDC008A74E1 /* FlairView.swift */; }; 691FD7B624C75CCD002E2C9C /* SubredditPostThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691FD7B524C75CCD002E2C9C /* SubredditPostThumbnailView.swift */; }; 69222AA124CC015E009F31B4 /* PostsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69222AA024CC015E009F31B4 /* PostsListView.swift */; }; @@ -15,6 +16,7 @@ 69222AA524CD6A89009F31B4 /* UserHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69222AA424CD6A89009F31B4 /* UserHeaderView.swift */; }; 69222AA724CD6D6C009F31B4 /* SubmittedPostsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69222AA624CD6D6C009F31B4 /* SubmittedPostsListView.swift */; }; 69222AAA24CD7518009F31B4 /* CommentRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69222AA924CD7518009F31B4 /* CommentRow.swift */; }; + 6923F8CD250250FC0003870F /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 6923F8CC250250FC0003870F /* KingfisherSwiftUI */; }; 6924D53C24CD949D005487CA /* UserPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6924D53B24CD949D005487CA /* UserPopoverView.swift */; }; 6924D53E24CD94B0005487CA /* UserViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6924D53D24CD94B0005487CA /* UserViewModel.swift */; }; 6924D54024CDCED0005487CA /* UserSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6924D53F24CDCED0005487CA /* UserSheetView.swift */; }; @@ -27,33 +29,43 @@ 692566DA24B8A3830014E0D4 /* PostDetailHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692566D924B8A3830014E0D4 /* PostDetailHeader.swift */; }; 6927894924B9B75200EEFBF2 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6927894824B9B75200EEFBF2 /* ProfileView.swift */; }; 692F237624CB3A7B006C9D40 /* SavedPostsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692F237524CB3A7B006C9D40 /* SavedPostsListView.swift */; }; + 693BD7732518C4FB00CA5214 /* PostDetailToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693BD7722518C4FB00CA5214 /* PostDetailToolbar.swift */; }; 693F85D124D0690500224ADB /* NSTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693F85D024D0690500224ADB /* NSTextField.swift */; }; 693F85D424D0715000224ADB /* ToolbarSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693F85D324D0715000224ADB /* ToolbarSearchBar.swift */; }; 694C634F24C0AA6D0017897D /* SidebarSubredditRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694C634E24C0AA6D0017897D /* SidebarSubredditRow.swift */; }; 6970A0AE24B74A9D00B11031 /* PostInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0AD24B74A9D00B11031 /* PostInfoView.swift */; }; - 6970A0B124B74B7900B11031 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 6970A0B024B74B7900B11031 /* SDWebImageSwiftUI */; }; 6970A0B324B77D1200B11031 /* PostVoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0B224B77D1200B11031 /* PostVoteView.swift */; }; 6970A0B624B783FE00B11031 /* LinkPresentationRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0B524B783FE00B11031 /* LinkPresentationRepresentable.swift */; }; 6970A0B924B79AFD00B11031 /* PopoverSearchSubredditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0B824B79AFD00B11031 /* PopoverSearchSubredditView.swift */; }; - 6970A0BB24B79F5900B11031 /* PopoverSearchSubredditViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0BA24B79F5900B11031 /* PopoverSearchSubredditViewModel.swift */; }; + 6970A0BB24B79F5900B11031 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0BA24B79F5900B11031 /* SearchViewModel.swift */; }; 6970A0BD24B82E1C00B11031 /* LoadingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0BC24B82E1C00B11031 /* LoadingRow.swift */; }; 6970A0BF24B8343100B11031 /* PopoverSearchSubredditRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0BE24B8343100B11031 /* PopoverSearchSubredditRow.swift */; }; 6970A0C124B88BA200B11031 /* PostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0C024B88BA200B11031 /* PostViewModel.swift */; }; 6970A0C324B896CD00B11031 /* PostDetailCommentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6970A0C224B896CD00B11031 /* PostDetailCommentsSection.swift */; }; + 697E324524E3E7D90006F00F /* UI in Frameworks */ = {isa = PBXBuildFile; productRef = 697E324424E3E7D90006F00F /* UI */; }; + 697E324724E3ED620006F00F /* CommentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697E324624E3ED620006F00F /* CommentViewModel.swift */; }; + 697E324924E3EDE70006F00F /* CommentVoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697E324824E3EDE70006F00F /* CommentVoteView.swift */; }; + 697E324B24E3EFCB0006F00F /* CommentActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697E324A24E3EFCB0006F00F /* CommentActionsView.swift */; }; + 697E324D24E3F2900006F00F /* SharingPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697E324C24E3F2900006F00F /* SharingPicker.swift */; }; + 69CCB3EA24E2BEAC003FAAD7 /* SubredditAboutPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CCB3E924E2BEAC003FAAD7 /* SubredditAboutPopoverView.swift */; }; 69D076C824B9E871001619AC /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D076C724B9E871001619AC /* Color.swift */; }; + 69D8663424E568060052A2B0 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D8663324E568060052A2B0 /* Route.swift */; }; + 69DB093824DFCBC60026811F /* SettingsKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DB093724DFCBC60026811F /* SettingsKey.swift */; }; 69EACF0324B63D5800303A16 /* RedditOsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EACF0224B63D5800303A16 /* RedditOsApp.swift */; }; 69EACF0724B63D5900303A16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69EACF0624B63D5900303A16 /* Assets.xcassets */; }; 69EACF0A24B63D5900303A16 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69EACF0924B63D5900303A16 /* Preview Assets.xcassets */; }; - 69EACF1324B668F200303A16 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EACF1224B668F200303A16 /* Sidebar.swift */; }; + 69EACF1324B668F200303A16 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EACF1224B668F200303A16 /* SidebarView.swift */; }; 69EACF1524B6F1E200303A16 /* SubredditPostsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EACF1424B6F1E200303A16 /* SubredditPostsListView.swift */; }; 69EACF1724B6F2BA00303A16 /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EACF1624B6F2BA00303A16 /* PostDetailView.swift */; }; 69EACF1C24B7272E00303A16 /* Backend in Frameworks */ = {isa = PBXBuildFile; productRef = 69EACF1B24B7272E00303A16 /* Backend */; }; - 69EACF1F24B7298800303A16 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EACF1E24B7298800303A16 /* SidebarViewModel.swift */; }; 69EACF2524B73DF400303A16 /* SubredditViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EACF2424B73DF400303A16 /* SubredditViewModel.swift */; }; + 69F74E9324DAE65100E58BD8 /* AwardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F74E9224DAE65100E58BD8 /* AwardView.swift */; }; + 69F74E9624DB0B7300E58BD8 /* GlobalSearchPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F74E9524DB0B7300E58BD8 /* GlobalSearchPopoverView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 6906880C24B743900067D973 /* SubredditPostRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubredditPostRow.swift; sourceTree = ""; }; + 691747E124DBEA240017E068 /* GlobalSearchSubRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchSubRow.swift; sourceTree = ""; }; 6918A8CA24C1FEDC008A74E1 /* FlairView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlairView.swift; sourceTree = ""; }; 691FD7B524C75CCD002E2C9C /* SubredditPostThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubredditPostThumbnailView.swift; sourceTree = ""; }; 69222AA024CC015E009F31B4 /* PostsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsListView.swift; sourceTree = ""; }; @@ -73,6 +85,7 @@ 692566D924B8A3830014E0D4 /* PostDetailHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailHeader.swift; sourceTree = ""; }; 6927894824B9B75200EEFBF2 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 692F237524CB3A7B006C9D40 /* SavedPostsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedPostsListView.swift; sourceTree = ""; }; + 693BD7722518C4FB00CA5214 /* PostDetailToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailToolbar.swift; sourceTree = ""; }; 693F85D024D0690500224ADB /* NSTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSTextField.swift; sourceTree = ""; }; 693F85D324D0715000224ADB /* ToolbarSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarSearchBar.swift; sourceTree = ""; }; 694C634E24C0AA6D0017897D /* SidebarSubredditRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSubredditRow.swift; sourceTree = ""; }; @@ -80,24 +93,33 @@ 6970A0B224B77D1200B11031 /* PostVoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostVoteView.swift; sourceTree = ""; }; 6970A0B524B783FE00B11031 /* LinkPresentationRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPresentationRepresentable.swift; sourceTree = ""; }; 6970A0B824B79AFD00B11031 /* PopoverSearchSubredditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverSearchSubredditView.swift; sourceTree = ""; }; - 6970A0BA24B79F5900B11031 /* PopoverSearchSubredditViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverSearchSubredditViewModel.swift; sourceTree = ""; }; + 6970A0BA24B79F5900B11031 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 6970A0BC24B82E1C00B11031 /* LoadingRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingRow.swift; sourceTree = ""; }; 6970A0BE24B8343100B11031 /* PopoverSearchSubredditRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverSearchSubredditRow.swift; sourceTree = ""; }; 6970A0C024B88BA200B11031 /* PostViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewModel.swift; sourceTree = ""; }; 6970A0C224B896CD00B11031 /* PostDetailCommentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailCommentsSection.swift; sourceTree = ""; }; + 697E324324E3E6BF0006F00F /* UI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = UI; path = Packages/UI; sourceTree = ""; }; + 697E324624E3ED620006F00F /* CommentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentViewModel.swift; sourceTree = ""; }; + 697E324824E3EDE70006F00F /* CommentVoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentVoteView.swift; sourceTree = ""; }; + 697E324A24E3EFCB0006F00F /* CommentActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentActionsView.swift; sourceTree = ""; }; + 697E324C24E3F2900006F00F /* SharingPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingPicker.swift; sourceTree = ""; }; + 69CCB3E924E2BEAC003FAAD7 /* SubredditAboutPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubredditAboutPopoverView.swift; sourceTree = ""; }; 69D076C724B9E871001619AC /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; + 69D8663324E568060052A2B0 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; }; + 69DB093724DFCBC60026811F /* SettingsKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKey.swift; sourceTree = ""; }; 69EACEFF24B63D5800303A16 /* Curiosity.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Curiosity.app; sourceTree = BUILT_PRODUCTS_DIR; }; 69EACF0224B63D5800303A16 /* RedditOsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditOsApp.swift; sourceTree = ""; }; 69EACF0624B63D5900303A16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 69EACF0924B63D5900303A16 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 69EACF0B24B63D5900303A16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69EACF0C24B63D5900303A16 /* RedditOs.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RedditOs.entitlements; sourceTree = ""; }; - 69EACF1224B668F200303A16 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; }; + 69EACF1224B668F200303A16 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; 69EACF1424B6F1E200303A16 /* SubredditPostsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubredditPostsListView.swift; sourceTree = ""; }; 69EACF1624B6F2BA00303A16 /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = ""; }; 69EACF1924B7223E00303A16 /* Backend */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Backend; path = Packages/Backend; sourceTree = ""; }; - 69EACF1E24B7298800303A16 /* SidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewModel.swift; sourceTree = ""; }; 69EACF2424B73DF400303A16 /* SubredditViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubredditViewModel.swift; sourceTree = ""; }; + 69F74E9224DAE65100E58BD8 /* AwardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AwardView.swift; sourceTree = ""; }; + 69F74E9524DB0B7300E58BD8 /* GlobalSearchPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchPopoverView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -105,8 +127,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6970A0B124B74B7900B11031 /* SDWebImageSwiftUI in Frameworks */, + 697E324524E3E7D90006F00F /* UI in Frameworks */, 69EACF1C24B7272E00303A16 /* Backend in Frameworks */, + 6923F8CD250250FC0003870F /* KingfisherSwiftUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -117,6 +140,9 @@ isa = PBXGroup; children = ( 69222AA924CD7518009F31B4 /* CommentRow.swift */, + 697E324624E3ED620006F00F /* CommentViewModel.swift */, + 697E324824E3EDE70006F00F /* CommentVoteView.swift */, + 697E324A24E3EFCB0006F00F /* CommentActionsView.swift */, ); path = Comments; sourceTree = ""; @@ -135,6 +161,8 @@ isa = PBXGroup; children = ( 6924D54224CDD049005487CA /* UIState.swift */, + 69DB093724DFCBC60026811F /* SettingsKey.swift */, + 69D8663324E568060052A2B0 /* Route.swift */, ); path = Environements; sourceTree = ""; @@ -184,14 +212,13 @@ path = Profile; sourceTree = ""; }; - 693F85D224D06AA700224ADB /* Subreddit Popover */ = { + 693F85D224D06AA700224ADB /* Subreddit Search Popover */ = { isa = PBXGroup; children = ( 6970A0B824B79AFD00B11031 /* PopoverSearchSubredditView.swift */, - 6970A0BA24B79F5900B11031 /* PopoverSearchSubredditViewModel.swift */, 6970A0BE24B8343100B11031 /* PopoverSearchSubredditRow.swift */, ); - path = "Subreddit Popover"; + path = "Subreddit Search Popover"; sourceTree = ""; }; 6970A0AC24B7494C00B11031 /* Shared */ = { @@ -203,6 +230,7 @@ 6918A8CA24C1FEDC008A74E1 /* FlairView.swift */, 69222AA024CC015E009F31B4 /* PostsListView.swift */, 69222AA224CC0291009F31B4 /* PostNoSelectionPlaceholder.swift */, + 69F74E9224DAE65100E58BD8 /* AwardView.swift */, ); path = Shared; sourceTree = ""; @@ -211,6 +239,7 @@ isa = PBXGroup; children = ( 6970A0B524B783FE00B11031 /* LinkPresentationRepresentable.swift */, + 697E324C24E3F2900006F00F /* SharingPicker.swift */, ); path = Representables; sourceTree = ""; @@ -218,8 +247,10 @@ 6970A0B724B79AF400B11031 /* Search */ = { isa = PBXGroup; children = ( - 693F85D224D06AA700224ADB /* Subreddit Popover */, + 6970A0BA24B79F5900B11031 /* SearchViewModel.swift */, 693F85D324D0715000224ADB /* ToolbarSearchBar.swift */, + 69F74E9424DB0B5600E58BD8 /* Global Search Popopver */, + 693F85D224D06AA700224ADB /* Subreddit Search Popover */, ); path = Search; sourceTree = ""; @@ -237,6 +268,7 @@ isa = PBXGroup; children = ( 69EACF0124B63D5800303A16 /* RedditOs */, + 697E324324E3E6BF0006F00F /* UI */, 69EACF1924B7223E00303A16 /* Backend */, 69EACF0024B63D5800303A16 /* Products */, 69EACF1A24B7272E00303A16 /* Frameworks */, @@ -301,8 +333,7 @@ 69EACF2124B7299000303A16 /* Sidebar */ = { isa = PBXGroup; children = ( - 69EACF1E24B7298800303A16 /* SidebarViewModel.swift */, - 69EACF1224B668F200303A16 /* Sidebar.swift */, + 69EACF1224B668F200303A16 /* SidebarView.swift */, 694C634E24C0AA6D0017897D /* SidebarSubredditRow.swift */, ); path = Sidebar; @@ -315,6 +346,7 @@ 69EACF1424B6F1E200303A16 /* SubredditPostsListView.swift */, 6906880C24B743900067D973 /* SubredditPostRow.swift */, 691FD7B524C75CCD002E2C9C /* SubredditPostThumbnailView.swift */, + 69CCB3E924E2BEAC003FAAD7 /* SubredditAboutPopoverView.swift */, ); path = Subreddit; sourceTree = ""; @@ -328,10 +360,20 @@ 692566D524B8A25A0014E0D4 /* PostDetailContent.swift */, 6970A0C224B896CD00B11031 /* PostDetailCommentsSection.swift */, 692566D724B8A3190014E0D4 /* PostDetailActionsView.swift */, + 693BD7722518C4FB00CA5214 /* PostDetailToolbar.swift */, ); path = Post; sourceTree = ""; }; + 69F74E9424DB0B5600E58BD8 /* Global Search Popopver */ = { + isa = PBXGroup; + children = ( + 69F74E9524DB0B7300E58BD8 /* GlobalSearchPopoverView.swift */, + 691747E124DBEA240017E068 /* GlobalSearchSubRow.swift */, + ); + path = "Global Search Popopver"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -350,7 +392,8 @@ name = RedditOs; packageProductDependencies = ( 69EACF1B24B7272E00303A16 /* Backend */, - 6970A0B024B74B7900B11031 /* SDWebImageSwiftUI */, + 697E324424E3E7D90006F00F /* UI */, + 6923F8CC250250FC0003870F /* KingfisherSwiftUI */, ); productName = RedditOs; productReference = 69EACEFF24B63D5800303A16 /* Curiosity.app */; @@ -363,7 +406,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1200; - LastUpgradeCheck = 1200; + LastUpgradeCheck = 1220; TargetAttributes = { 69EACEFE24B63D5800303A16 = { CreatedOnToolsVersion = 12.0; @@ -380,7 +423,7 @@ ); mainGroup = 69EACEF624B63D5800303A16; packageReferences = ( - 6970A0AF24B74B7900B11031 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, + 6923F8CB250250FC0003870F /* XCRemoteSwiftPackageReference "Kingfisher" */, ); productRefGroup = 69EACF0024B63D5800303A16 /* Products */; projectDirPath = ""; @@ -408,36 +451,46 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 69EACF1324B668F200303A16 /* Sidebar.swift in Sources */, + 69EACF1324B668F200303A16 /* SidebarView.swift in Sources */, 6970A0BF24B8343100B11031 /* PopoverSearchSubredditRow.swift in Sources */, 69222AA124CC015E009F31B4 /* PostsListView.swift in Sources */, 6918A8CB24C1FEDC008A74E1 /* FlairView.swift in Sources */, 69D076C824B9E871001619AC /* Color.swift in Sources */, + 697E324924E3EDE70006F00F /* CommentVoteView.swift in Sources */, 6970A0AE24B74A9D00B11031 /* PostInfoView.swift in Sources */, 6924D54D24CDF92A005487CA /* SettingsView.swift in Sources */, + 693BD7732518C4FB00CA5214 /* PostDetailToolbar.swift in Sources */, 6970A0B624B783FE00B11031 /* LinkPresentationRepresentable.swift in Sources */, 6924D54324CDD049005487CA /* UIState.swift in Sources */, 692566D624B8A25A0014E0D4 /* PostDetailContent.swift in Sources */, 694C634F24C0AA6D0017897D /* SidebarSubredditRow.swift in Sources */, 693F85D424D0715000224ADB /* ToolbarSearchBar.swift in Sources */, 69EACF1524B6F1E200303A16 /* SubredditPostsListView.swift in Sources */, + 697E324D24E3F2900006F00F /* SharingPicker.swift in Sources */, 692F237624CB3A7B006C9D40 /* SavedPostsListView.swift in Sources */, 6924D53E24CD94B0005487CA /* UserViewModel.swift in Sources */, 69222AA724CD6D6C009F31B4 /* SubmittedPostsListView.swift in Sources */, + 697E324724E3ED620006F00F /* CommentViewModel.swift in Sources */, 692566DA24B8A3830014E0D4 /* PostDetailHeader.swift in Sources */, + 69CCB3EA24E2BEAC003FAAD7 /* SubredditAboutPopoverView.swift in Sources */, + 69D8663424E568060052A2B0 /* Route.swift in Sources */, 6924D54A24CDDDD9005487CA /* UserSheetCommentsView.swift in Sources */, 6970A0C324B896CD00B11031 /* PostDetailCommentsSection.swift in Sources */, 6970A0B924B79AFD00B11031 /* PopoverSearchSubredditView.swift in Sources */, + 691747E224DBEA240017E068 /* GlobalSearchSubRow.swift in Sources */, + 69F74E9624DB0B7300E58BD8 /* GlobalSearchPopoverView.swift in Sources */, 69222AA324CC0291009F31B4 /* PostNoSelectionPlaceholder.swift in Sources */, 6924D53C24CD949D005487CA /* UserPopoverView.swift in Sources */, - 6970A0BB24B79F5900B11031 /* PopoverSearchSubredditViewModel.swift in Sources */, + 69F74E9324DAE65100E58BD8 /* AwardView.swift in Sources */, + 6970A0BB24B79F5900B11031 /* SearchViewModel.swift in Sources */, 6924D54524CDD7D7005487CA /* UserSheetOverviewView.swift in Sources */, 6906880D24B743900067D973 /* SubredditPostRow.swift in Sources */, - 69EACF1F24B7298800303A16 /* SidebarViewModel.swift in Sources */, + 69DB093824DFCBC60026811F /* SettingsKey.swift in Sources */, 6927894924B9B75200EEFBF2 /* ProfileView.swift in Sources */, 691FD7B624C75CCD002E2C9C /* SubredditPostThumbnailView.swift in Sources */, 69EACF1724B6F2BA00303A16 /* PostDetailView.swift in Sources */, 692566D824B8A3190014E0D4 /* PostDetailActionsView.swift in Sources */, + 697E324B24E3EFCB0006F00F /* CommentActionsView.swift in Sources */, 69EACF0324B63D5800303A16 /* RedditOsApp.swift in Sources */, 69EACF2524B73DF400303A16 /* SubredditViewModel.swift in Sources */, 6924D54024CDCED0005487CA /* UserSheetView.swift in Sources */, @@ -571,13 +624,13 @@ 69EACF1024B63D5900303A16 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD_64_BIT)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = RedditOs/RedditOs.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 15122020; DEVELOPMENT_ASSET_PATHS = "\"RedditOs/Preview Content\""; DEVELOPMENT_TEAM = Z6P74P6T99; ENABLE_HARDENED_RUNTIME = YES; @@ -587,9 +640,9 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.16; - MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.RedditOs; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 0.2; + PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.curiosity; PRODUCT_NAME = Curiosity; SWIFT_VERSION = 5.0; }; @@ -598,13 +651,13 @@ 69EACF1124B63D5900303A16 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD_64_BIT)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = RedditOs/RedditOs.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 15122020; DEVELOPMENT_ASSET_PATHS = "\"RedditOs/Preview Content\""; DEVELOPMENT_TEAM = Z6P74P6T99; ENABLE_HARDENED_RUNTIME = YES; @@ -614,9 +667,9 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.16; - MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.RedditOs; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 0.2; + PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.curiosity; PRODUCT_NAME = Curiosity; SWIFT_VERSION = 5.0; }; @@ -646,21 +699,25 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 6970A0AF24B74B7900B11031 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { + 6923F8CB250250FC0003870F /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI"; + repositoryURL = "https://github.com/onevcat/Kingfisher"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.5.0; + minimumVersion = 5.15.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 6970A0B024B74B7900B11031 /* SDWebImageSwiftUI */ = { + 6923F8CC250250FC0003870F /* KingfisherSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + package = 6923F8CB250250FC0003870F /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = KingfisherSwiftUI; + }; + 697E324424E3E7D90006F00F /* UI */ = { isa = XCSwiftPackageProductDependency; - package = 6970A0AF24B74B7900B11031 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; - productName = SDWebImageSwiftUI; + productName = UI; }; 69EACF1B24B7272E00303A16 /* Backend */ = { isa = XCSwiftPackageProductDependency; diff --git a/RedditOs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/RedditOs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d643439..f863fbd 100644 --- a/RedditOs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/RedditOs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,26 +6,17 @@ "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", "state": { "branch": null, - "revision": "3d0ea2c0806791abcc5d7f0d9f62f1cfd4a7264d", - "version": "4.2.0" + "revision": "654d52d30f3dd4592e944c3e0bccb53178c992f6", + "version": "4.2.1" } }, { - "package": "SDWebImage", - "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", + "package": "Kingfisher", + "repositoryURL": "https://github.com/onevcat/Kingfisher", "state": { "branch": null, - "revision": "f876da50318666141d72963e1568afee6f84f008", - "version": "5.8.3" - } - }, - { - "package": "SDWebImageSwiftUI", - "repositoryURL": "https://github.com/SDWebImage/SDWebImageSwiftUI", - "state": { - "branch": null, - "revision": "4c7f169e39bc35d6b80d42b8eb8301bee9cd0907", - "version": "1.5.0" + "revision": "2a6d1135af3915547c4b08c3b154a05e6f1075a3", + "version": "5.15.5" } } ] diff --git a/RedditOs.xcodeproj/xcshareddata/xcschemes/RedditOs.xcscheme b/RedditOs.xcodeproj/xcshareddata/xcschemes/RedditOs.xcscheme index 802f7ed..ee70bc3 100644 --- a/RedditOs.xcodeproj/xcshareddata/xcschemes/RedditOs.xcscheme +++ b/RedditOs.xcodeproj/xcshareddata/xcschemes/RedditOs.xcscheme @@ -1,6 +1,6 @@ Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + case user(user: User, isSheet: Bool) + case subreddit(subreddit: String, isSheet: Bool) + case defaultChannel(chanel: UIState.DefaultChannels) + case none + + var id: String { + switch self { + case .user: + return "user" + case let .subreddit(subreddit, _): + return subreddit + case .none: + return "none" + case let .defaultChannel(chanel): + return chanel.rawValue + } + } + + @ViewBuilder + func makeView() -> some View { + switch self { + case let .user(user, _): + UserSheetView(user: user) + case let .subreddit(subreddit, isSheet): + SubredditPostsListView(name: subreddit, isSheet: isSheet).environmentObject(UIState.shared) + case let .defaultChannel(chanel): + SubredditPostsListView(name: chanel.rawValue) + case .none: + EmptyView() + } + } +} diff --git a/RedditOs/Environements/SettingsKey.swift b/RedditOs/Environements/SettingsKey.swift new file mode 100644 index 0000000..cdccc3c --- /dev/null +++ b/RedditOs/Environements/SettingsKey.swift @@ -0,0 +1,14 @@ +// +// SettingsKey.swift +// RedditOs +// +// Created by Thomas Ricouard on 09/08/2020. +// + +import Foundation + +struct SettingsKey { + static let subreddit_display_mode = "postDisplayMode" + static let subreddit_defaut_sort_order = "defaultSortOrder" + static let comments_default_sort_order = "defaultCommentsSortOrder" +} diff --git a/RedditOs/Environements/UIState.swift b/RedditOs/Environements/UIState.swift index 070e4b4..ad85b7e 100644 --- a/RedditOs/Environements/UIState.swift +++ b/RedditOs/Environements/UIState.swift @@ -11,24 +11,37 @@ import Combine import Backend class UIState: ObservableObject { - enum Route: Identifiable { - case user(user: User) + public static let shared = UIState() + + enum DefaultChannels: String, CaseIterable { + case hot, best, new, top, rising - var id: String { + func icon() -> String { switch self { - case .user: - return "user" + case .best: return "rosette" + case .hot: return "flame" + case .new: return "calendar.circle" + case .top: return "chart.bar" + case .rising: return "waveform.path.ecg" } } + } + + private init() { - @ViewBuilder - func makeSheet() -> some View { - switch self { - case let .user(user): - UserSheetView(user: user) + } + + @Published var selectedSubreddit: SubredditViewModel? + @Published var selectedPost: PostViewModel? + + @Published var presentedSheetRoute: Route? + @Published var presentedNavigationRoute: Route? { + didSet { + if let route = presentedNavigationRoute { + sidebarSelection = route.id } } } - @Published var presentedRoute: Route? + @Published var sidebarSelection: String? = DefaultChannels.hot.rawValue } diff --git a/RedditOs/Features/Comments/CommentActionsView.swift b/RedditOs/Features/Comments/CommentActionsView.swift new file mode 100644 index 0000000..24442f1 --- /dev/null +++ b/RedditOs/Features/Comments/CommentActionsView.swift @@ -0,0 +1,52 @@ +// +// CommentActionsView.swift +// RedditOs +// +// Created by Thomas Ricouard on 12/08/2020. +// + +import SwiftUI +import Backend + +struct CommentActionsView: View { + @ObservedObject var viewModel: CommentViewModel + @State private var showPicker = false + + var body: some View { + HStack(spacing: 16) { + Button(action: { + + }, label: { + Label("Reply", systemImage: "bubble.right") + }).buttonStyle(BorderlessButtonStyle()) + + Button(action: { + showPicker.toggle() + }, label: { + Label("Share", systemImage: "square.and.arrow.up") + }) + .buttonStyle(BorderlessButtonStyle()) + .background(SharingsPicker(isPresented: $showPicker, + sharingItems: [viewModel.comment.permalinkURL ?? ""])) + + Button(action: { + viewModel.toggleSave() + }, label: { + Label("Save", + systemImage: viewModel.comment.saved == true ? "bookmark.fill" : "bookmark") + }).buttonStyle(BorderlessButtonStyle()) + + Button(action: { + + }, label: { + Label("Report", systemImage: "flag") + }).buttonStyle(BorderlessButtonStyle()) + } + } +} + +struct CommentActionsView_Previews: PreviewProvider { + static var previews: some View { + CommentActionsView(viewModel: CommentViewModel(comment: static_comment)) + } +} diff --git a/RedditOs/Features/Comments/CommentRow.swift b/RedditOs/Features/Comments/CommentRow.swift index c65ead5..d68e2bd 100644 --- a/RedditOs/Features/Comments/CommentRow.swift +++ b/RedditOs/Features/Comments/CommentRow.swift @@ -9,29 +9,78 @@ import SwiftUI import Backend struct CommentRow: View { - let comment: Comment + @StateObject private var viewModel: CommentViewModel + @State private var showUserPopover = false + + init(comment: Comment) { + _viewModel = StateObject(wrappedValue: CommentViewModel(comment: comment)) + } + var body: some View { - VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 0) { - Text(comment.author ?? "Unknown") - .font(.callout) - .fontWeight(.bold) - if let score = comment.score { - Text(" · \(score.toRoundedSuffixAsString()) points · ") - .foregroundColor(.gray) - .font(.caption) + HStack(alignment: .top) { + CommentVoteView(viewModel: viewModel).padding(.top, 4) + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 0) { + HStack(spacing: 6) { + if let richText = viewModel.comment.authorFlairRichtext, !richText.isEmpty { + FlairView(richText: richText, + textColorHex: viewModel.comment.authorFlairTextColor, + backgroundColorHex: viewModel.comment.authorFlairBackgroundColor, + display: .small) + } + if let author = viewModel.comment.author { + Button(action: { + showUserPopover = true + }, label: { + HStack(spacing: 4) { + if viewModel.comment.isSubmitter == true { + Image(systemName: "music.mic") + .foregroundColor(.redditBlue) + } else { + Image(systemName: "person") + } + Text(author) + .font(.callout) + .fontWeight(.bold) + } + }) + .buttonStyle(BorderlessButtonStyle()) + .popover(isPresented: $showUserPopover, content: { + UserPopoverView(username: author) + }) + } else { + Text("Deleted user") + .font(.footnote) + } + } + if let score = viewModel.comment.score { + Text(" · \(score.toRoundedSuffixAsString()) points · ") + .foregroundColor(.gray) + .font(.caption) + } + if let date = viewModel.comment.createdUtc { + Text(date, style: .relative) + .foregroundColor(.gray) + .font(.caption) + } + if let awards = viewModel.comment.allAwardings, !awards.isEmpty { + AwardsView(awards: awards).padding(.leading, 8) + } } - if let date = comment.createdUtc { - Text(date, style: .relative) + if let body = viewModel.comment.body { + Text(body) + .font(.body) + .fixedSize(horizontal: false, vertical: true) + } else { + Text("Deleted comment") + .font(.footnote) .foregroundColor(.gray) - .font(.caption) } - } - Text(comment.body ?? "No comment content") - .font(.body) - .padding(.bottom, 4) - Divider() - }.padding(.vertical, 4) + CommentActionsView(viewModel: viewModel) + .foregroundColor(.gray) + Divider() + }.padding(.vertical, 4) + } } } diff --git a/RedditOs/Features/Comments/CommentViewModel.swift b/RedditOs/Features/Comments/CommentViewModel.swift new file mode 100644 index 0000000..8f1036b --- /dev/null +++ b/RedditOs/Features/Comments/CommentViewModel.swift @@ -0,0 +1,46 @@ +// +// CommentViewModel.swift +// RedditOs +// +// Created by Thomas Ricouard on 12/08/2020. +// + +import Foundation +import SwiftUI +import Combine +import Backend + +class CommentViewModel: ObservableObject { + @Published var comment: Comment + + private var cancellableStore: [AnyCancellable] = [] + + init(comment: Comment) { + self.comment = comment + } + + func postVote(vote: Vote) { + let oldValue = comment.likes + let cancellable = comment.vote(vote: vote) + .receive(on: DispatchQueue.main) + .sink{ [weak self] response in + if response.error != nil { + self?.comment.likes = oldValue + } + } + cancellableStore.append(cancellable) + } + + func toggleSave() { + let oldValue = comment.saved + let cancellable = (comment.saved == true ? comment.unsave() : comment.save()) + .receive(on: DispatchQueue.main) + .sink{ [weak self] response in + if response.error != nil { + self?.comment.saved = oldValue + } + } + cancellableStore.append(cancellable) + } + +} diff --git a/RedditOs/Features/Comments/CommentVoteView.swift b/RedditOs/Features/Comments/CommentVoteView.swift new file mode 100644 index 0000000..847f958 --- /dev/null +++ b/RedditOs/Features/Comments/CommentVoteView.swift @@ -0,0 +1,43 @@ +// +// CommentVoteView.swift +// RedditOs +// +// Created by Thomas Ricouard on 12/08/2020. +// + +import SwiftUI +import Backend + +struct CommentVoteView: View { + @ObservedObject var viewModel: CommentViewModel + + var body: some View { + VStack(spacing: 6) { + Button(action: { + viewModel.postVote(vote: viewModel.comment.likes == true ? .neutral : .upvote) + }, + label: { + Image(systemName: "arrowtriangle.up.circle") + .resizable() + .frame(width: 16, height: 16) + .foregroundColor(viewModel.comment.likes == true ? .accentColor : nil) + }).buttonStyle(BorderlessButtonStyle()) + + Button(action: { + viewModel.postVote(vote: viewModel.comment.likes == false ? .neutral : .downvote) + }, + label: { + Image(systemName: "arrowtriangle.down.circle") + .resizable() + .frame(width: 16, height: 16) + .foregroundColor(viewModel.comment.likes == false ? .redditBlue : nil) + }).buttonStyle(BorderlessButtonStyle()) + }.frame(width: 20) + } +} + +struct CommentVoteView_Previews: PreviewProvider { + static var previews: some View { + CommentVoteView(viewModel: CommentViewModel(comment: static_comment)) + } +} diff --git a/RedditOs/Features/Post/PostDetailActionsView.swift b/RedditOs/Features/Post/PostDetailActionsView.swift index e9115b8..db7130f 100644 --- a/RedditOs/Features/Post/PostDetailActionsView.swift +++ b/RedditOs/Features/Post/PostDetailActionsView.swift @@ -10,42 +10,42 @@ import Backend struct PostDetailActionsView: View { @ObservedObject var viewModel: PostViewModel + @State private var showPicker = false var body: some View { HStack(spacing: 16) { - HStack(spacing: 6) { - Image(systemName: "bubble.middle.bottom.fill") - .imageScale(.medium) - Text("\(viewModel.post.numComments) comments") + Button(action: { + + }, label: { + Label("\(viewModel.post.numComments) comments", systemImage: "bubble.middle.bottom.fill") .foregroundColor(.white) - } + }).buttonStyle(BorderlessButtonStyle()) - HStack(spacing: 6) { - Image(systemName: "square.and.arrow.up") - .imageScale(.medium) - Text("Share") + Button(action: { + showPicker.toggle() + }, label: { + Label("Share", systemImage: "square.and.arrow.up") .foregroundColor(.white) - } + }) + .buttonStyle(BorderlessButtonStyle()) + .background(SharingsPicker(isPresented: $showPicker, + sharingItems: [viewModel.post.redditURL ?? ""])) - HStack(spacing: 6) { - Button(action: { - viewModel.toggleSave() - }) { - Image(systemName: viewModel.post.saved ? "bookmark.fill": "bookmark") - .imageScale(.medium) - .foregroundColor(viewModel.post.saved ? .accentColor : .white) - Text("Save") - .foregroundColor(.white) - }.buttonStyle(BorderlessButtonStyle()) - } + + Button(action: { + viewModel.toggleSave() + }, label: { + Label(viewModel.post.saved ? "Saved" : "Save", + systemImage: viewModel.post.saved ? "bookmark.fill": "bookmark") + .foregroundColor(viewModel.post.saved ? .accentColor : .white) + }).buttonStyle(BorderlessButtonStyle()) - HStack(spacing: 6) { - Image(systemName: "flag") - .imageScale(.medium) - Text("Report") + Button(action: { + + }, label: { + Label("Report", systemImage: "flag") .foregroundColor(.white) - } - + }).buttonStyle(BorderlessButtonStyle()) } } } diff --git a/RedditOs/Features/Post/PostDetailCommentsSection.swift b/RedditOs/Features/Post/PostDetailCommentsSection.swift index 31a8bb0..dd55fa0 100644 --- a/RedditOs/Features/Post/PostDetailCommentsSection.swift +++ b/RedditOs/Features/Post/PostDetailCommentsSection.swift @@ -7,24 +7,28 @@ import SwiftUI import Backend +import UI struct PostDetailCommentsSection: View { - let comments: [Comment]? + @ObservedObject var viewModel: PostViewModel private let placeholderComments = Array(repeating: static_comment, count: 10) var body: some View { - OutlineGroup(comments ?? placeholderComments, - children: \.repliesComments) { comment in - CommentRow(comment: comment) - .redacted(reason: comments == nil ? .placeholder : []) + Divider() + + Picker("Sort by", selection: $viewModel.commentsSort) { + ForEach(Comment.Sort.allCases, id: \.self) { sort in + Text(sort.label()).tag(sort) + } } - } -} - -struct PostCommentsSection_Previews: PreviewProvider { - static var previews: some View { - List { - PostDetailCommentsSection(comments: static_comments) + .pickerStyle(MenuPickerStyle()) + .frame(width: 170) + .padding(.bottom, 8) + + RecursiveView(data: viewModel.comments ?? placeholderComments, + children: \.repliesComments) { comment in + CommentRow(comment: comment) + .redacted(reason: viewModel.comments == nil ? .placeholder : []) } } } diff --git a/RedditOs/Features/Post/PostDetailContent.swift b/RedditOs/Features/Post/PostDetailContent.swift index ddb47f3..6fdff8f 100644 --- a/RedditOs/Features/Post/PostDetailContent.swift +++ b/RedditOs/Features/Post/PostDetailContent.swift @@ -8,7 +8,7 @@ import SwiftUI import Backend import AVKit -import SDWebImageSwiftUI +import KingfisherSwiftUI struct PostDetailContent: View { let listing: SubredditPost @@ -17,7 +17,9 @@ struct PostDetailContent: View { @ViewBuilder var body: some View { if let text = listing.selftext ?? listing.description { - Text(text).font(.body) + Text(text) + .font(.body) + .fixedSize(horizontal: false, vertical: true) } if let video = listing.secureMedia?.video { HStack { @@ -31,9 +33,8 @@ struct PostDetailContent: View { if realURL.pathExtension == "jpg" || realURL.pathExtension == "png" { HStack { Spacer() - WebImage(url: realURL) + KFImage(realURL) .resizable() - .indicator(.activity) .aspectRatio(contentMode: .fit) .background(Color.gray) .frame(maxHeight: 400) diff --git a/RedditOs/Features/Post/PostDetailHeader.swift b/RedditOs/Features/Post/PostDetailHeader.swift index 19d921b..82ddc44 100644 --- a/RedditOs/Features/Post/PostDetailHeader.swift +++ b/RedditOs/Features/Post/PostDetailHeader.swift @@ -7,7 +7,7 @@ import SwiftUI import Backend -import SDWebImageSwiftUI +import KingfisherSwiftUI struct PostDetailHeader: View { let listing: SubredditPost @@ -20,7 +20,7 @@ struct PostDetailHeader: View { .multilineTextAlignment(.leading) .truncationMode(.tail) if let url = listing.thumbnailURL, url.pathExtension != "jpg", url.pathExtension != "png" { - WebImage(url: url) + KFImage(url) .frame(width: 80, height: 60) .aspectRatio(contentMode: .fit) .cornerRadius(8) diff --git a/RedditOs/Features/Post/PostDetailToolbar.swift b/RedditOs/Features/Post/PostDetailToolbar.swift new file mode 100644 index 0000000..206fa5e --- /dev/null +++ b/RedditOs/Features/Post/PostDetailToolbar.swift @@ -0,0 +1,35 @@ +// +// PostDetailToolbar.swift +// RedditOs +// +// Created by Thomas Ricouard on 21/09/2020. +// + +import SwiftUI + +struct PostDetailToolbar: ToolbarContent { + let shareURL: URL? + + var body: some ToolbarContent { + ToolbarItemGroup { + SharingView(url: shareURL) + Spacer() + ToolbarSearchBar() + } + } +} + +struct SharingView: View { + let url: URL? + @State private var sharePickerShown = false + + var body: some View { + Button(action: { + sharePickerShown.toggle() + }) { + Image(systemName: "square.and.arrow.up") + }.background(SharingsPicker(isPresented: $sharePickerShown, + sharingItems: [url ?? ""])) + + } +} diff --git a/RedditOs/Features/Post/PostDetailView.swift b/RedditOs/Features/Post/PostDetailView.swift index 59f106a..e980a82 100644 --- a/RedditOs/Features/Post/PostDetailView.swift +++ b/RedditOs/Features/Post/PostDetailView.swift @@ -10,8 +10,10 @@ import Backend import AVKit struct PostDetailView: View { + @EnvironmentObject private var uiState: UIState @ObservedObject var viewModel: PostViewModel @State private var redrawLink = false + @State private var sharePickerShown = false var body: some View { List { @@ -19,17 +21,26 @@ struct PostDetailView: View { HStack { PostVoteView(viewModel: viewModel) VStack(alignment: .leading) { - PostInfoView(post: viewModel.post) + PostInfoView(post: viewModel.post, display: .horizontal) PostDetailHeader(listing: viewModel.post) } } PostDetailContent(listing: viewModel.post, redrawLink: $redrawLink) PostDetailActionsView(viewModel: viewModel) }.padding(.bottom, 16) - PostDetailCommentsSection(comments: viewModel.comments) + PostDetailCommentsSection(viewModel: viewModel) } .onAppear(perform: viewModel.fechComments) .onAppear(perform: viewModel.postVisit) + .onAppear(perform: { + uiState.selectedPost = viewModel + }) + .onDisappear(perform: { + uiState.selectedPost = nil + }) + .toolbar { + PostDetailToolbar(shareURL: viewModel.post.redditURL) + } .frame(minWidth: 500, maxWidth: .infinity, maxHeight: .infinity) diff --git a/RedditOs/Features/Post/PostViewModel.swift b/RedditOs/Features/Post/PostViewModel.swift index ba53ee9..f768851 100644 --- a/RedditOs/Features/Post/PostViewModel.swift +++ b/RedditOs/Features/Post/PostViewModel.swift @@ -13,6 +13,11 @@ import Backend class PostViewModel: ObservableObject { @Published var post: SubredditPost @Published var comments: [Comment]? + @AppStorage(SettingsKey.comments_default_sort_order) var commentsSort = Comment.Sort.top { + didSet { + fechComments() + } + } private var cancellableStore: [AnyCancellable] = [] @@ -32,7 +37,7 @@ class PostViewModel: ObservableObject { cancellableStore.append(cancellable) } - func postVote(vote: SubredditPost.Vote) { + func postVote(vote: Vote) { let oldValue = post.likes let cancellable = post.vote(vote: vote) .receive(on: DispatchQueue.main) @@ -57,7 +62,8 @@ class PostViewModel: ObservableObject { } func fechComments() { - let cancellable = Comment.fetch(subreddit: post.subreddit, id: post.id) + comments = nil + let cancellable = Comment.fetch(subreddit: post.subreddit, id: post.id, sort: commentsSort) .receive(on: DispatchQueue.main) .map{ $0.last?.comments } .sink{ [weak self] comments in @@ -66,3 +72,14 @@ class PostViewModel: ObservableObject { cancellableStore.append(cancellable) } } + +extension Comment.Sort { + public func label() -> String { + switch self { + case .best: + return "Best" + default: + return self.rawValue.capitalized + } + } +} diff --git a/RedditOs/Features/Profile/ProfileView.swift b/RedditOs/Features/Profile/ProfileView.swift index ea9d80f..1ea45ab 100644 --- a/RedditOs/Features/Profile/ProfileView.swift +++ b/RedditOs/Features/Profile/ProfileView.swift @@ -7,17 +7,11 @@ import SwiftUI import Backend -import SDWebImageSwiftUI struct ProfileView: View { - enum OverviewFilter: String, CaseIterable { - case posts, comments - } - @EnvironmentObject private var oauthClient: OauthClient @EnvironmentObject private var currentUser: CurrentUserStore @Environment(\.openURL) private var openURL - @State private var overviewFilter = OverviewFilter.posts private let loadingPlaceholders = Array(repeating: static_listing, count: 10) @@ -29,6 +23,8 @@ struct ProfileView: View { userOverview } } + .listStyle(InsetListStyle()) + .frame(width: 500) PostNoSelectionPlaceholder() } @@ -61,33 +57,6 @@ struct ProfileView: View { @ViewBuilder private var userOverview: some View { if let overview = currentUser.overview { - Picker("", selection: $overviewFilter) { - ForEach(OverviewFilter.allCases, id: \.self) { filter in - Text(filter.rawValue.capitalized) - .tag(filter) - } - } - .pickerStyle(SegmentedPickerStyle()) - .padding(.vertical, 12) - - if overviewFilter == .posts { - let posts = overview.compactMap{ $0.post } - ForEach(posts) { post in - SubredditPostRow(post: post, displayMode: .constant(.large)) - } - } else if overviewFilter == .comments { - let comments = overview.compactMap{ $0.comment } - ForEach(comments) { comment in - CommentRow(comment: comment) - } - } - LoadingRow(text: "Loading next page") - .onAppear { - currentUser.fetchOverview() - } - - // This is crashing SwiftUI for now. - /* ForEach(overview) { content in switch content { case let .post(post): @@ -98,10 +67,14 @@ struct ProfileView: View { Text("Unsupported view") } } - */ + LoadingRow(text: "Loading next page") + .onAppear { + currentUser.fetchOverview() + } } else { ForEach(loadingPlaceholders) { post in - SubredditPostRow(post: post, displayMode: .constant(.large)) + SubredditPostRow(post: post, + displayMode: .constant(.large)) .redacted(reason: .placeholder) } } diff --git a/RedditOs/Features/Search/Global Search Popopver/GlobalSearchPopoverView.swift b/RedditOs/Features/Search/Global Search Popopver/GlobalSearchPopoverView.swift new file mode 100644 index 0000000..3fdbda0 --- /dev/null +++ b/RedditOs/Features/Search/Global Search Popopver/GlobalSearchPopoverView.swift @@ -0,0 +1,91 @@ +// +// GlobalSearchPopoverView.swift +// RedditOs +// +// Created by Thomas Ricouard on 05/08/2020. +// + +import SwiftUI +import Backend + +struct GlobalSearchPopoverView: View { + @EnvironmentObject private var uiState: UIState + @EnvironmentObject private var currentUser: CurrentUserStore + + @ObservedObject var viewModel: SearchViewModel + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + makeTitle("Quick access") + makeQuickAccess() + + Divider() + + makeTitle("My subscriptions") + makeMySubscriptionsSearch() + + Divider() + + makeTitle("Subreddit search") + makeSubredditSearch() + + }.padding() + }.frame(width: 300, height: 500) + } + + private func makeTitle(_ title: String) -> some View { + Text(title) + .font(.headline) + .fontWeight(.bold) + } + + private func makeQuickAccess() -> some View { + Group { + GlobalSearchSubRow(icon: nil, + name: "Go to r/\(viewModel.searchText)") + .onTapGesture { + uiState.presentedNavigationRoute = .subreddit(subreddit: viewModel.searchText, isSheet: false) + } + GlobalSearchSubRow(icon: nil, + name: "Go to u/\(viewModel.searchText)") + }.padding(4) + } + + private func makeMySubscriptionsSearch() -> some View { + Group { + if let subs = viewModel.filteredSubscriptions { + if subs.isEmpty { + Label("No matching subscriptions for \(viewModel.searchText)", systemImage: "magnifyingglass") + } else { + ForEach(subs) { sub in + makeSubRow(icon: sub.iconImg, name: sub.displayName) + } + } + } + }.padding(4) + } + + private func makeSubredditSearch() -> some View { + Group { + if let results = viewModel.results { + if results.isEmpty { + Label("No matching search for \(viewModel.searchText)", systemImage: "magnifyingglass") + } else { + ForEach(results) { sub in + makeSubRow(icon: sub.iconImg, name: sub.name) + } + } + } else if viewModel.isLoading { + LoadingRow(text: nil) + } + }.padding(4) + } + + private func makeSubRow(icon: String?, name: String) -> some View { + GlobalSearchSubRow(icon: icon, name: name) + .onTapGesture { + uiState.presentedNavigationRoute = .subreddit(subreddit: name, isSheet: false) + } + } +} diff --git a/RedditOs/Features/Search/Global Search Popopver/GlobalSearchSubRow.swift b/RedditOs/Features/Search/Global Search Popopver/GlobalSearchSubRow.swift new file mode 100644 index 0000000..506fb2b --- /dev/null +++ b/RedditOs/Features/Search/Global Search Popopver/GlobalSearchSubRow.swift @@ -0,0 +1,38 @@ +// +// GlobalSearchSubRow.swift +// RedditOs +// +// Created by Thomas Ricouard on 06/08/2020. +// + +import SwiftUI +import KingfisherSwiftUI + +struct GlobalSearchSubRow: View { + let icon: String? + let name: String + + @State private var isHovered = false + + var body: some View { + HStack { + if let image = icon, + let url = URL(string: image) { + KFImage(url) + .resizable() + .frame(width: 16, height: 16) + .cornerRadius(8) + } else { + Image(systemName: "globe") + .resizable() + .frame(width: 16, height: 16) + } + Text(name).foregroundColor(isHovered ? .accentColor : nil) + } + .scaleEffect(isHovered ? 1.05 : 1.0) + .animation(.interactiveSpring()) + .onHover { hovered in + isHovered = hovered + } + } +} diff --git a/RedditOs/Features/Search/Subreddit Popover/PopoverSearchSubredditViewModel.swift b/RedditOs/Features/Search/SearchViewModel.swift similarity index 53% rename from RedditOs/Features/Search/Subreddit Popover/PopoverSearchSubredditViewModel.swift rename to RedditOs/Features/Search/SearchViewModel.swift index 1237003..95f1448 100644 --- a/RedditOs/Features/Search/Subreddit Popover/PopoverSearchSubredditViewModel.swift +++ b/RedditOs/Features/Search/SearchViewModel.swift @@ -10,25 +10,46 @@ import SwiftUI import Combine import Backend -class PopoverSearchSubredditViewModel: ObservableObject { +class SearchViewModel: ObservableObject { @Published var searchText = "" @Published var results: [SubredditSmall]? + @Published var filteredSubscriptions: [Subreddit]? @Published var isLoading = false - private var searchCancellable: AnyCancellable? + private var currentUser: CurrentUserStore + + private var delayedSearchCancellable: AnyCancellable? + private var instantSearchCancellable: AnyCancellable? private var apiPublisher: AnyPublisher? private var apiCancellable: AnyCancellable? - init() { - searchCancellable = $searchText + init(currentUser: CurrentUserStore = .shared) { + self.currentUser = currentUser + + delayedSearchCancellable = $searchText .subscribe(on: DispatchQueue.global()) .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) .removeDuplicates() - .filter { !$0.isEmpty } .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] text in - self?.isLoading = true - self?.search(with: text) + if text.isEmpty { + self?.isLoading = false + self?.results = nil + } else { + self?.isLoading = true + self?.search(with: text) + } + }) + + instantSearchCancellable = $searchText + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] text in + guard let w = self else { return } + if text.isEmpty { + w.filteredSubscriptions = nil + } else { + w.filteredSubscriptions = w.currentUser.subscriptions.filter{ $0.displayName.lowercased().contains(text.lowercased()) } + } }) } diff --git a/RedditOs/Features/Search/Subreddit Popover/PopoverSearchSubredditRow.swift b/RedditOs/Features/Search/Subreddit Search Popover/PopoverSearchSubredditRow.swift similarity index 96% rename from RedditOs/Features/Search/Subreddit Popover/PopoverSearchSubredditRow.swift rename to RedditOs/Features/Search/Subreddit Search Popover/PopoverSearchSubredditRow.swift index 2ae1b15..691bb45 100644 --- a/RedditOs/Features/Search/Subreddit Popover/PopoverSearchSubredditRow.swift +++ b/RedditOs/Features/Search/Subreddit Search Popover/PopoverSearchSubredditRow.swift @@ -7,7 +7,7 @@ import SwiftUI import Backend -import SDWebImageSwiftUI +import KingfisherSwiftUI struct PopoverSearchSubredditRow: View { let subreddit: SubredditSmall @@ -17,7 +17,7 @@ struct PopoverSearchSubredditRow: View { HStack { if let image = subreddit.iconImg, let url = URL(string: image) { - WebImage(url: url) + KFImage(url) .resizable() .frame(width: 30, height: 30) .cornerRadius(15) diff --git a/RedditOs/Features/Search/Subreddit Popover/PopoverSearchSubredditView.swift b/RedditOs/Features/Search/Subreddit Search Popover/PopoverSearchSubredditView.swift similarity index 94% rename from RedditOs/Features/Search/Subreddit Popover/PopoverSearchSubredditView.swift rename to RedditOs/Features/Search/Subreddit Search Popover/PopoverSearchSubredditView.swift index dca8308..157fa76 100644 --- a/RedditOs/Features/Search/Subreddit Popover/PopoverSearchSubredditView.swift +++ b/RedditOs/Features/Search/Subreddit Search Popover/PopoverSearchSubredditView.swift @@ -10,7 +10,7 @@ import Backend struct PopoverSearchSubredditView: View { @EnvironmentObject private var userData: LocalDataStore - @StateObject private var viewModel = PopoverSearchSubredditViewModel() + @StateObject private var viewModel = SearchViewModel() var body: some View { List { diff --git a/RedditOs/Features/Search/ToolbarSearchBar.swift b/RedditOs/Features/Search/ToolbarSearchBar.swift index 30471e6..bf8f046 100644 --- a/RedditOs/Features/Search/ToolbarSearchBar.swift +++ b/RedditOs/Features/Search/ToolbarSearchBar.swift @@ -8,14 +8,15 @@ import SwiftUI struct ToolbarSearchBar: View { + @EnvironmentObject private var uiState: UIState @State private var isFocused = false - @State private var searchText = "" + @StateObject private var searchViewModel = SearchViewModel() var body: some View { - TextField("Search anything", text: $searchText) { editing in + TextField("Search anything", text: $searchViewModel.searchText) { editing in isFocused = editing } onCommit: { - + uiState.presentedNavigationRoute = .subreddit(subreddit: searchViewModel.searchText, isSheet: false) } .keyboardShortcut("f", modifiers: .command) .padding(8) @@ -23,7 +24,10 @@ struct ToolbarSearchBar: View { .stroke(isFocused ? Color.accentColor : Color.clear) .background(Color.black.opacity(0.2).cornerRadius(8))) .textFieldStyle(PlainTextFieldStyle()) - .frame(width: 500) + .frame(width: 300) + .popover(isPresented: $isFocused) { + GlobalSearchPopoverView(viewModel: searchViewModel) + } } } diff --git a/RedditOs/Features/Settings/SettingsView.swift b/RedditOs/Features/Settings/SettingsView.swift index 74ccd69..5c0d6f5 100644 --- a/RedditOs/Features/Settings/SettingsView.swift +++ b/RedditOs/Features/Settings/SettingsView.swift @@ -8,66 +8,63 @@ import SwiftUI struct SettingsView: View { + @AppStorage(SettingsKey.subreddit_display_mode) private var displayMode = SubredditPostRow.DisplayMode.large + @AppStorage(SettingsKey.subreddit_defaut_sort_order) private var sortOrder = SubredditViewModel.SortOrder.hot + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - .toolbar { - ToolbarItem { - Button(action: { - - }, label: { - VStack { - Image(systemName: "gearshape").imageScale(.large) - Text("General") - } - }) - } - - ToolbarItem { - Button(action: { - - }, label: { - VStack { - Image(systemName: "textformat.alt").imageScale(.large) - Text("Apperance") - } - }) - } - - - ToolbarItem { - Button(action: { - - }, label: { - VStack { - Image(systemName: "stop.circle").imageScale(.large) - Text("Filters") - } - }) - } - - ToolbarItem { - Button(action: { - - }, label: { - VStack { - Image(systemName: "magnifyingglass").imageScale(.large) - Text("Search") + TabView { + generalView + .tabItem { + Image(systemName: "gearshape").imageScale(.large) + Text("General") + } + + Text("Apperance") + .tabItem { + Image(systemName: "textformat.alt").imageScale(.large) + Text("Apperance") + } + + Text("Filters") + .tabItem { + Image(systemName: "stop.circle").imageScale(.large) + Text("Filters") + } + + Text("Search") + .tabItem { + Image(systemName: "magnifyingglass").imageScale(.large) + Text("Search") + } + + Text("Accounts") + .tabItem { + Image(systemName: "person").imageScale(.large) + Text("Accounts") + } + }.frame(width: 1000, height: 500) + } + + private var generalView: some View { + Form { + Section(header: Text("Subreddit settings")) { + Picker("Display mode", + selection: $displayMode, + content: { + ForEach(SubredditPostRow.DisplayMode.allCases, id: \.self) { mode in + Label(mode.rawValue, systemImage: mode.iconName()).tag(mode) } - }) - } + }) - - ToolbarItem { - Button(action: { - - }, label: { - VStack { - Image(systemName: "person").imageScale(.large) - Text("Accounts") + Picker(selection: $sortOrder, + label: Text("Default sort"), + content: { + ForEach(SubredditViewModel.SortOrder.allCases, id: \.self) { sort in + Text(sort.rawValue.capitalized).tag(sort) } - }) - } - }.frame(width: 1000, height: 500) + }) + } + }.frame(width: 500) } } diff --git a/RedditOs/Features/Sidebar/Sidebar.swift b/RedditOs/Features/Sidebar/Sidebar.swift index 305ac2d..fa20f30 100644 --- a/RedditOs/Features/Sidebar/Sidebar.swift +++ b/RedditOs/Features/Sidebar/Sidebar.swift @@ -9,22 +9,27 @@ import SwiftUI import Backend import SDWebImageSwiftUI -struct Sidebar: View { +struct SidebarView: View { + @EnvironmentObject private var uiState: UIState @EnvironmentObject private var localData: LocalDataStore @EnvironmentObject private var currentUser: CurrentUserStore - @StateObject private var viewModel = SidebarViewModel() + @State private var isSearchPopoverPresented = false @State private var isHovered = false @State private var isInEditMode = false var body: some View { - List(selection: $viewModel.selection) { + List(selection: $uiState.sidebarSelection) { Section { - ForEach(SidebarViewModel.MainSubreddits.allCases, id: \.self) { item in + ForEach(UIState.DefaultChannels.allCases, id: \.self) { item in NavigationLink(destination: SubredditPostsListView(name: item.rawValue)) { Label(LocalizedStringKey(item.rawValue.capitalized), systemImage: item.icon()) }.tag(item.rawValue) - } + }.animation(nil) + NavigationLink(destination: SubredditPostsListView(name: uiState.searchedSubreddit), + isActive: $uiState.displaySearch) { + EmptyView() + }.hidden() } Section(header: Text("Account")) { @@ -38,11 +43,11 @@ struct Sidebar: View { Label("Inbox", systemImage: "envelope") NavigationLink(destination: SubmittedPostsListView()) { Label("Posts", systemImage: "square.and.pencil") - } + }.tag("Posts") Label("Comments", systemImage: "text.bubble") NavigationLink(destination: SavedPostsListView()) { Label("Saved", systemImage: "archivebox") - } + }.tag("Saved") }.listItemTint(.redditBlue) Section(header: subredditsHeader) { @@ -63,15 +68,13 @@ struct Sidebar: View { .buttonStyle(BorderlessButtonStyle()) } } - } + }.animation(nil) } .listItemTint(.redditGold) .animation(.easeInOut) if let subs = currentUser.subscriptions, currentUser.user != nil { Section(header: Text("Subscriptions")) { - TextField("Filter", text: $viewModel.subscriptionFilter) - .textFieldStyle(RoundedBorderTextFieldStyle()) ForEach(subs) { reddit in HStack { SidebarSubredditRow(name: reddit.displayName, @@ -95,17 +98,16 @@ struct Sidebar: View { .buttonStyle(BorderlessButtonStyle()) } } - - } + }.animation(nil) }.listItemTint(.redditBlue) } } + .animation(nil) .listStyle(SidebarListStyle()) .frame(minWidth: 200, idealWidth: 200, maxWidth: 200, maxHeight: .infinity) .onHover { hovered in isHovered = hovered } - .padding(.top, 16) } private var subredditsHeader: some View { @@ -121,7 +123,7 @@ struct Sidebar: View { } .buttonStyle(BorderlessButtonStyle()) .popover(isPresented: $isSearchPopoverPresented) { - PopoverSearchSubredditView().environmentObject(localData) + PopoverSearchSubredditView() } Button { @@ -140,6 +142,6 @@ struct Sidebar: View { struct Sidebar_Previews: PreviewProvider { static var previews: some View { - Sidebar() + SidebarView() } } diff --git a/RedditOs/Features/Sidebar/SidebarSubredditRow.swift b/RedditOs/Features/Sidebar/SidebarSubredditRow.swift index cf17d2d..0bd0007 100644 --- a/RedditOs/Features/Sidebar/SidebarSubredditRow.swift +++ b/RedditOs/Features/Sidebar/SidebarSubredditRow.swift @@ -7,7 +7,7 @@ import SwiftUI import Backend -import SDWebImageSwiftUI +import KingfisherSwiftUI struct SidebarSubredditRow: View { let name: String @@ -18,7 +18,7 @@ struct SidebarSubredditRow: View { HStack { if let image = iconURL, let url = URL(string: image) { - WebImage(url: url) + KFImage(url) .resizable() .frame(width: 16, height: 16) .cornerRadius(8) diff --git a/RedditOs/Features/Sidebar/SidebarView.swift b/RedditOs/Features/Sidebar/SidebarView.swift new file mode 100644 index 0000000..b7b1c47 --- /dev/null +++ b/RedditOs/Features/Sidebar/SidebarView.swift @@ -0,0 +1,154 @@ +// +// Sidebar.swift +// RedditOs +// +// Created by Thomas Ricouard on 08/07/2020. +// + +import SwiftUI +import Backend + +struct SidebarView: View { + @EnvironmentObject private var uiState: UIState + @EnvironmentObject private var localData: LocalDataStore + @EnvironmentObject private var currentUser: CurrentUserStore + + @State private var isSearchPopoverPresented = false + @State private var isHovered = false + @State private var isInEditMode = false + + var body: some View { + List(selection: $uiState.sidebarSelection) { + if let route = uiState.presentedNavigationRoute { + Section { + NavigationLink( + destination: route.makeView(), + tag: route, + selection: $uiState.presentedNavigationRoute, + label: { + Label("Search", systemImage: "magnifyingglass") + }) + } + } + + Section { + ForEach(UIState.DefaultChannels.allCases, id: \.self) { item in + NavigationLink(destination: SubredditPostsListView(name: item.rawValue)) { + Label(LocalizedStringKey(item.rawValue.capitalized), systemImage: item.icon()) + }.tag(item.rawValue) + }.animation(nil) + } + + Section(header: Text("Account")) { + NavigationLink(destination: ProfileView()) { + if let user = currentUser.user { + Label(user.name, systemImage: "person.crop.circle") + } else { + Label("Profile", systemImage: "person.crop.circle") + } + }.tag("profile") + Label("Inbox", systemImage: "envelope") + NavigationLink(destination: SubmittedPostsListView()) { + Label("Posts", systemImage: "square.and.pencil") + }.tag("Posts") + Label("Comments", systemImage: "text.bubble") + NavigationLink(destination: SavedPostsListView()) { + Label("Saved", systemImage: "archivebox") + }.tag("Saved") + }.listItemTint(.redditBlue) + + Section(header: subredditsHeader) { + ForEach(localData.favorites) { reddit in + HStack { + SidebarSubredditRow(name: reddit.name, + iconURL: reddit.iconImg) + .tag("local\(reddit.name)") + if isInEditMode { + Spacer() + Button { + localData.remove(favorite: reddit) + } label: { + Image(systemName: "minus.circle.fill") + .imageScale(.large) + .foregroundColor(.red) + } + .buttonStyle(BorderlessButtonStyle()) + } + } + }.animation(nil) + } + .listItemTint(.redditGold) + .animation(.easeInOut) + + if let subs = currentUser.subscriptions, currentUser.user != nil { + Section(header: Text("Subscriptions")) { + ForEach(subs) { reddit in + HStack { + SidebarSubredditRow(name: reddit.displayName, + iconURL: reddit.iconImg) + .tag(reddit.displayName) + Spacer() + if isHovered { + let isfavorite = localData.favorites.first(where: { $0.name == reddit.displayName}) != nil + Button { + if isfavorite { + localData.remove(favoriteNamed: reddit.displayName) + } else { + localData.add(favorite: SubredditSmall.makeSubredditSmall(with: reddit)) + } + } label: { + Image(systemName: isfavorite ? "star.fill" : "star") + .imageScale(.large) + .foregroundColor(.yellow) + .opacity(/*@START_MENU_TOKEN@*/0.8/*@END_MENU_TOKEN@*/) + } + .buttonStyle(BorderlessButtonStyle()) + } + } + }.animation(nil) + }.listItemTint(.redditBlue) + } + } + .animation(nil) + .listStyle(SidebarListStyle()) + .frame(minWidth: 200, idealWidth: 200, maxWidth: 200, maxHeight: .infinity) + .onHover { hovered in + isHovered = hovered + } + } + + private var subredditsHeader: some View { + HStack(spacing: 8) { + Text("Favorites") + if isHovered { + Button { + isSearchPopoverPresented = true + } label: { + Image(systemName: "plus.circle") + .imageScale(.large) + .foregroundColor(.blue) + } + .buttonStyle(BorderlessButtonStyle()) + .popover(isPresented: $isSearchPopoverPresented) { + PopoverSearchSubredditView() + } + + Button { + isInEditMode.toggle() + } label: { + Image(systemName: isInEditMode ? "trash.circle.fill" : "trash.circle") + .imageScale(.large) + .foregroundColor(.blue) + } + .buttonStyle(BorderlessButtonStyle()) + } + + } + } +} + +struct Sidebar_Previews: PreviewProvider { + static var previews: some View { + SidebarView() + } +} diff --git a/RedditOs/Features/Sidebar/SidebarViewModel.swift b/RedditOs/Features/Sidebar/SidebarViewModel.swift deleted file mode 100644 index 1d56e00..0000000 --- a/RedditOs/Features/Sidebar/SidebarViewModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SidebarViewModel.swift -// RedditOs -// -// Created by Thomas Ricouard on 09/07/2020. -// - -import Foundation -import SwiftUI - -class SidebarViewModel: ObservableObject { - enum MainSubreddits: String, CaseIterable { - case hot, best, new, top, rising - - func icon() -> String { - switch self { - case .best: return "rosette" - case .hot: return "flame" - case .new: return "calendar.circle" - case .top: return "chart.bar" - case .rising: return "waveform.path.ecg" - } - } - } - - @Published var selection: Set = [MainSubreddits.hot.rawValue] - @Published var subscriptionFilter = "" -} diff --git a/RedditOs/Features/Subreddit/SubredditAboutPopoverView.swift b/RedditOs/Features/Subreddit/SubredditAboutPopoverView.swift new file mode 100644 index 0000000..6cd681e --- /dev/null +++ b/RedditOs/Features/Subreddit/SubredditAboutPopoverView.swift @@ -0,0 +1,98 @@ +// +// SubredditAboutPopoverView.swift +// RedditOs +// +// Created by Thomas Ricouard on 11/08/2020. +// + +import SwiftUI +import Backend + +struct SubredditAboutPopoverView: View { + @EnvironmentObject private var localData: LocalDataStore + @ObservedObject var viewModel: SubredditViewModel + @State private var isSubscribeHovered = false + + var isFavorite: Bool { + guard let subreddit = viewModel.subreddit else { + return false + } + return localData.favorites.contains(SubredditSmall.makeSubredditSmall(with: subreddit)) + } + + var isSubscriber: Bool { + viewModel.subreddit?.userIsSubscriber == true + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + HStack(alignment: .center, spacing: 16) { + if let subreddit = viewModel.subreddit { + Button(action: { + viewModel.toggleSubscribe() + }, label: { + if isSubscribeHovered { + Text(isSubscriber ? "Unsubscribe?" : "Subscribe?") + } else { + Text(isSubscriber ? "Subscribed" : "Subscribe") + } + }).onHover(perform: { hovering in + isSubscribeHovered = hovering + }) + + Button(action: { + if isFavorite { + localData.remove(favorite: SubredditSmall.makeSubredditSmall(with: subreddit)) + } else { + localData.add(favorite: SubredditSmall.makeSubredditSmall(with: subreddit)) + } + }, label: { + Image(systemName: isFavorite ? "star.fill" : "star") + .resizable() + .imageScale(.large) + .foregroundColor(isFavorite ? .redditGold : nil) + }).buttonStyle(BorderlessButtonStyle()) + } + } + Text("About Community") + .font(.title3) + Text(viewModel.subreddit?.publicDescription ?? "") + .font(.body) + if let subscribers = viewModel.subreddit?.subscribers, + let connected = viewModel.subreddit?.accountsActive { + HStack(spacing: 16) { + VStack(alignment: .leading) { + Text("\(subscribers.toRoundedSuffixAsString())") + .fontWeight(.bold) + Text("Members") + } + + VStack(alignment: .leading) { + Text("\(connected.toRoundedSuffixAsString())") + .fontWeight(.bold) + Text("Online") + } + } + } + + Divider() + + HStack(spacing: 4) { + Image(systemName: "calendar.circle") + .font(.title3) + .foregroundColor(.white) + Text(" Created ") + + Text(viewModel.subreddit?.createdUtc ?? Date(), style: .date) + } + + }.padding() + }.frame(width: 250, height: 350) + } +} + +struct SubredditAboutPopoverView_Previews: PreviewProvider { + static var previews: some View { + SubredditAboutPopoverView(viewModel: SubredditViewModel(name: static_subreddit.name)).environmentObject(LocalDataStore()) + } +} diff --git a/RedditOs/Features/Subreddit/SubredditPostRow.swift b/RedditOs/Features/Subreddit/SubredditPostRow.swift index 95db4a9..2505253 100644 --- a/RedditOs/Features/Subreddit/SubredditPostRow.swift +++ b/RedditOs/Features/Subreddit/SubredditPostRow.swift @@ -7,7 +7,6 @@ import SwiftUI import Backend -import SDWebImageSwiftUI struct SubredditPostRow: View { enum DisplayMode: String, CaseIterable { @@ -30,7 +29,7 @@ struct SubredditPostRow: View { _viewModel = StateObject(wrappedValue: PostViewModel(post: post)) _displayMode = displayMode } - + var body: some View { NavigationLink(destination: PostDetailView(viewModel: viewModel)) { HStack { @@ -44,11 +43,16 @@ struct SubredditPostRow: View { VStack(alignment: .leading, spacing: 4) { Text(viewModel.post.title) .fontWeight(.bold) - .font(.headline) + .font(.body) .lineLimit(displayMode == .compact ? 2 : nil) .foregroundColor(viewModel.post.visited ? .gray : nil) HStack { - FlairView(post: viewModel.post) + if let richText = viewModel.post.linkFlairRichtext, !richText.isEmpty { + FlairView(richText: richText, + textColorHex: viewModel.post.linkFlairTextColor, + backgroundColorHex: viewModel.post.linkFlairBackgroundColor, + display: .normal) + } if (viewModel.post.selftext == nil || viewModel.post.selftext?.isEmpty == true), displayMode == .large, let urlString = viewModel.post.url, @@ -58,7 +62,7 @@ struct SubredditPostRow: View { } } } - PostInfoView(post: viewModel.post) + PostInfoView(post: viewModel.post, display: .vertical) } } } @@ -95,16 +99,18 @@ struct SubredditPostRow: View { struct SubredditPostRow_Previews: PreviewProvider { static var previews: some View { - List { - SubredditPostRow(post: static_listing, displayMode: .constant(.large)) - SubredditPostRow(post: static_listing, displayMode: .constant(.large)) - SubredditPostRow(post: static_listing, displayMode: .constant(.large)) - - Divider() - - SubredditPostRow(post: static_listing, displayMode: .constant(.compact)) - SubredditPostRow(post: static_listing, displayMode: .constant(.compact)) - SubredditPostRow(post: static_listing, displayMode: .constant(.compact)) + NavigationView { + List { + SubredditPostRow(post: static_listing, displayMode: .constant(.large)) + SubredditPostRow(post: static_listing, displayMode: .constant(.large)) + SubredditPostRow(post: static_listing, displayMode: .constant(.large)) + + Divider() + + SubredditPostRow(post: static_listing, displayMode: .constant(.compact)) + SubredditPostRow(post: static_listing, displayMode: .constant(.compact)) + SubredditPostRow(post: static_listing, displayMode: .constant(.compact)) + }.frame(width: 500) } } } diff --git a/RedditOs/Features/Subreddit/SubredditPostThumbnailView.swift b/RedditOs/Features/Subreddit/SubredditPostThumbnailView.swift index d7e9bab..26657d6 100644 --- a/RedditOs/Features/Subreddit/SubredditPostThumbnailView.swift +++ b/RedditOs/Features/Subreddit/SubredditPostThumbnailView.swift @@ -7,7 +7,7 @@ import SwiftUI import Backend -import SDWebImageSwiftUI +import KingfisherSwiftUI struct SubredditPostThumbnailView: View { @ObservedObject var viewModel: PostViewModel @@ -19,7 +19,7 @@ struct SubredditPostThumbnailView: View { var body: some View { ZStack(alignment: .topLeading) { if let url = post.thumbnailURL ?? post.secureMedia?.oembed?.thumbnailUrlAsURL { - WebImage(url: url) + KFImage(url) .frame(width: 80, height: 60) .aspectRatio(contentMode: .fit) .cornerRadius(8) diff --git a/RedditOs/Features/Subreddit/SubredditPostsListView.swift b/RedditOs/Features/Subreddit/SubredditPostsListView.swift index d6b66f9..08e9a01 100644 --- a/RedditOs/Features/Subreddit/SubredditPostsListView.swift +++ b/RedditOs/Features/Subreddit/SubredditPostsListView.swift @@ -7,22 +7,30 @@ import SwiftUI import Backend +import UI +import KingfisherSwiftUI struct SubredditPostsListView: View { let posts = Array(repeating: 0, count: 20) + private let isSheet: Bool private let loadingPlaceholders = Array(repeating: static_listing, count: 10) + @EnvironmentObject private var uiState: UIState @EnvironmentObject private var localData: LocalDataStore + @StateObject private var viewModel: SubredditViewModel - @AppStorage("postDisplayMode") private var displayMode = SubredditPostRow.DisplayMode.large + @AppStorage(SettingsKey.subreddit_display_mode) private var displayMode = SubredditPostRow.DisplayMode.large + + @State private var subredditAboutPopoverShown = false - init(name: String) { + init(name: String, isSheet: Bool = false) { + self.isSheet = isSheet _viewModel = StateObject(wrappedValue: SubredditViewModel(name: name)) } var isDefaultChannel: Bool { - SidebarViewModel.MainSubreddits.allCases.map{ $0.rawValue }.contains(viewModel.name) + UIState.DefaultChannels.allCases.map{ $0.rawValue }.contains(viewModel.name) } var subtitle: String { @@ -30,7 +38,7 @@ struct SubredditPostsListView: View { return "" } if let subscribers = viewModel.subreddit?.subscribers, let connected = viewModel.subreddit?.accountsActive { - return "\(subscribers.toRoundedSuffixAsString()) subscribers - \(connected.toRoundedSuffixAsString()) active" + return "\(subscribers.toRoundedSuffixAsString()) members - \(connected.toRoundedSuffixAsString()) online" } return "" } @@ -40,67 +48,69 @@ struct SubredditPostsListView: View { PostsListView(posts: viewModel.listings, displayMode: .constant(displayMode)) { viewModel.fetchListings() - }.onAppear(perform: viewModel.fetchListings) - PostNoSelectionPlaceholder() - } - .navigationTitle(viewModel.name.capitalized) - .navigationSubtitle(subtitle) - .toolbar { - ToolbarItem(placement: .primaryAction) { - ToolbarSearchBar() - } - - ToolbarItem(placement: .primaryAction) { - Picker(selection: $displayMode, - label: Text("Display"), - content: { - ForEach(SubredditPostRow.DisplayMode.allCases, id: \.self) { mode in - HStack { - Text(mode.rawValue.capitalized) - Image(systemName: mode.iconName()) - .tag(mode) - } + }.toolbar { + ToolbarItem(placement: .navigation) { + Group { + if isDefaultChannel { + EmptyView() + } else if let icon = viewModel.subreddit?.iconImg, let url = URL(string: icon) { + KFImage(url) + .resizable() + .frame(width: 20, height: 20) + .cornerRadius(10) + } else { + Image(systemName: "globe") + .resizable() + .frame(width: 20, height: 20) } - }) - } - - ToolbarItem(placement: .primaryAction) { - if isDefaultChannel { - Text("") - } else { - Picker(selection: $viewModel.sortOrder, - label: Text("Sorting"), + } + .onTapGesture { + subredditAboutPopoverShown = true + } + .popover(isPresented: $subredditAboutPopoverShown, + content: { SubredditAboutPopoverView(viewModel: viewModel) }) + } + + ToolbarItem { + Picker("", + selection: $displayMode, content: { - ForEach(SubredditViewModel.SortOrder.allCases, id: \.self) { sort in - Text(sort.rawValue.capitalized).tag(sort) + ForEach(SubredditPostRow.DisplayMode.allCases, id: \.self) { mode in + Image(systemName: mode.iconName()) + .tag(mode) } - }) + }).pickerStyle(InlinePickerStyle()) } - } - - ToolbarItem(placement: .primaryAction) { - Button(action: { - - }) { - Image(systemName: "info") + + ToolbarItem { + if isDefaultChannel { + Text("") + } else { + Picker(selection: $viewModel.sortOrder, + label: Text("Sorting"), + content: { + ForEach(SubredditViewModel.SortOrder.allCases, id: \.self) { sort in + Text(sort.rawValue.capitalized).tag(sort) + } + }) + } } - .keyboardShortcut("i", modifiers: .command) } + .onAppear(perform: viewModel.fetchListings) - ToolbarItem(placement: .primaryAction) { - Button(action: { - - }) { - Image(systemName: "square.and.arrow.up") + PostNoSelectionPlaceholder() + .toolbar { + PostDetailToolbar(shareURL: viewModel.subreddit?.redditURL) } - .keyboardShortcut("s", modifiers: .command) - } } - .onAppear(perform: viewModel.fetchListings) + .navigationTitle(viewModel.name.capitalized) + .navigationSubtitle(subtitle) + .frame(minHeight: 600) .onAppear { if !isDefaultChannel { viewModel.fetchAbout() } + uiState.selectedSubreddit = viewModel } } } diff --git a/RedditOs/Features/Subreddit/SubredditViewModel.swift b/RedditOs/Features/Subreddit/SubredditViewModel.swift index 65aa26b..63642b6 100644 --- a/RedditOs/Features/Subreddit/SubredditViewModel.swift +++ b/RedditOs/Features/Subreddit/SubredditViewModel.swift @@ -19,15 +19,17 @@ class SubredditViewModel: ObservableObject { private var subredditCancellable: AnyCancellable? private var listingCancellable: AnyCancellable? + private var subscribeCancellable: AnyCancellable? @Published var subreddit: Subreddit? @Published var listings: [SubredditPost]? - @Published var sortOrder = SortOrder.hot { + @AppStorage(SettingsKey.subreddit_defaut_sort_order) var sortOrder = SortOrder.hot { didSet { listings = nil fetchListings() } } + @Published var errorLoadingAbout = false init(name: String) { self.name = name @@ -37,6 +39,7 @@ class SubredditViewModel: ObservableObject { subredditCancellable = Subreddit.fetchAbout(name: name) .receive(on: DispatchQueue.main) .sink { [weak self] holder in + self?.errorLoadingAbout = holder == nil self?.subreddit = holder?.data } } @@ -55,4 +58,24 @@ class SubredditViewModel: ObservableObject { } } } + + func toggleSubscribe() { + if subreddit?.userIsSubscriber == true { + subscribeCancellable = subreddit?.unSubscribe() + .receive(on: DispatchQueue.main) + .sink { [weak self] response in + if response.error != nil { + self?.subreddit?.userIsSubscriber = true + } + } + } else { + subscribeCancellable = subreddit?.subscribe() + .receive(on: DispatchQueue.main) + .sink { [weak self] response in + if response.error != nil { + self?.subreddit?.userIsSubscriber = false + } + } + } + } } diff --git a/RedditOs/Features/Users/popover/UserPopoverView.swift b/RedditOs/Features/Users/popover/UserPopoverView.swift index 40445fc..b451eae 100644 --- a/RedditOs/Features/Users/popover/UserPopoverView.swift +++ b/RedditOs/Features/Users/popover/UserPopoverView.swift @@ -29,7 +29,7 @@ struct UserPopoverView: View { Button(action: { if let user = viewModel.user { presentation.wrappedValue.dismiss() - uiState.presentedRoute = .user(user: user) + uiState.presentedSheetRoute = .user(user: user, isSheet: true) } }) { Text("View full profile") @@ -45,6 +45,6 @@ struct UserPopoverView: View { struct UserPopoverView_Previews: PreviewProvider { static var previews: some View { - UserPopoverView(username: "").environmentObject(UIState()) + UserPopoverView(username: "").environmentObject(UIState.shared) } } diff --git a/RedditOs/Features/Users/shared/UserHeaderView.swift b/RedditOs/Features/Users/shared/UserHeaderView.swift index f161199..fe1a25a 100644 --- a/RedditOs/Features/Users/shared/UserHeaderView.swift +++ b/RedditOs/Features/Users/shared/UserHeaderView.swift @@ -7,7 +7,7 @@ import SwiftUI import Backend -import SDWebImageSwiftUI +import KingfisherSwiftUI struct UserHeaderView: View { let user: User @@ -16,7 +16,7 @@ struct UserHeaderView: View { HStack(spacing: 32) { Spacer() if let avatar = user.avatarURL { - WebImage(url: avatar) + KFImage(avatar) .resizable() .frame(width: 100, height: 100) .cornerRadius(8) diff --git a/RedditOs/Features/Users/sheet/UserSheetView.swift b/RedditOs/Features/Users/sheet/UserSheetView.swift index fb99d38..d2e24b8 100644 --- a/RedditOs/Features/Users/sheet/UserSheetView.swift +++ b/RedditOs/Features/Users/sheet/UserSheetView.swift @@ -24,15 +24,6 @@ struct UserSheetView: View { var body: some View { NavigationView { List(selection: $sidebarSelection) { - Button { - presentation.wrappedValue.dismiss() - } label: { - Label("Close", systemImage: "xmark.circle") - .foregroundColor(.accentColor) - } - .buttonStyle(BorderlessButtonStyle()) - .tag(0) - Section(header: Text(viewModel.username)) { Label("Overview", systemImage: "square.and.pencil").tag(1) Label("Posts", systemImage: "square.and.pencil").tag(2) @@ -40,7 +31,8 @@ struct UserSheetView: View { Label("Awards", systemImage: "rosette").tag(4) }.accentColor(.redditBlue) - }.listStyle(SidebarListStyle()) + } + .listStyle(SidebarListStyle()) NavigationView { if sidebarSelection.first == 1 { @@ -57,7 +49,17 @@ struct UserSheetView: View { PostNoSelectionPlaceholder() } - }.frame(width: 1500, height: 700) + } + .frame(width: 1500, height: 700) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button { + presentation.wrappedValue.dismiss() + } label: { + Text("Close") + } + } + } } } diff --git a/RedditOs/RedditOsApp.swift b/RedditOs/RedditOsApp.swift index de50c22..5640058 100644 --- a/RedditOs/RedditOsApp.swift +++ b/RedditOs/RedditOsApp.swift @@ -11,42 +11,88 @@ import Backend @main struct RedditOsApp: App { - @StateObject private var uiState = UIState() + @StateObject private var uiState = UIState.shared + @StateObject private var localData = LocalDataStore() @SceneBuilder var body: some Scene { WindowGroup { NavigationView { - Sidebar() + SidebarView() } - .frame(minHeight: 400, idealHeight: 800) - .environmentObject(LocalDataStore()) + .frame(minWidth: 1300, minHeight: 800) + .environmentObject(localData) .environmentObject(OauthClient.shared) - .environmentObject(CurrentUserStore()) + .environmentObject(CurrentUserStore.shared) .environmentObject(uiState) .onOpenURL { url in OauthClient.shared.handleNextURL(url: url) } - .sheet(item: $uiState.presentedRoute, content: { $0.makeSheet() }) + .sheet(item: $uiState.presentedSheetRoute, content: { $0.makeView() }) } - .commands{ + .commands { CommandMenu("Subreddit") { Button(action: { - + uiState.selectedSubreddit?.listings = nil + uiState.selectedSubreddit?.fetchListings() }) { - Text("Search") + Text("Refresh") } + .disabled(uiState.selectedSubreddit != nil) + .keyboardShortcut("r", modifiers: [.command]) + + Divider() + + Button(action: { + if let subreddit = uiState.selectedSubreddit?.subreddit { + let small = SubredditSmall.makeSubredditSmall(with: subreddit) + if localData.favorites.contains(small) { + localData.remove(favorite: small) + } else { + localData.add(favorite: small) + } + } + + }) { + Text("Toggle favorite") + } + .disabled(uiState.selectedSubreddit != nil) + .keyboardShortcut("f", modifiers: [.command, .shift]) + } + + CommandMenu("Post") { + Button(action: { + uiState.selectedPost?.fechComments() + }) { + Text("Refresh comments") + } + .disabled(uiState.selectedPost != nil) + .keyboardShortcut("r", modifiers: [.command, .shift]) + Button(action: { - + uiState.selectedPost?.toggleSave() }) { - Text("Navigate to") + Text(uiState.selectedPost?.post.saved == true ? "Unsave" : "Save") } + .disabled(uiState.selectedPost != nil) + .keyboardShortcut("s", modifiers: .command) + Divider() Button(action: { - + uiState.selectedPost?.postVote(vote: .upvote) + }) { + Text("Upvote") + } + .disabled(uiState.selectedPost != nil) + .keyboardShortcut(.upArrow, modifiers: .shift) + + Button(action: { + uiState.selectedPost?.postVote(vote: .downvote) }) { - Text("Favorite") + Text("Downvote") } + .disabled(uiState.selectedPost != nil) + .keyboardShortcut(.downArrow, modifiers: .shift) } #if DEBUG diff --git a/RedditOs/Representables/SharingPicker.swift b/RedditOs/Representables/SharingPicker.swift new file mode 100644 index 0000000..a0f5d5d --- /dev/null +++ b/RedditOs/Representables/SharingPicker.swift @@ -0,0 +1,47 @@ +// +// SharingPicker.swift +// RedditOs +// +// Created by Thomas Ricouard on 12/08/2020. +// + +import Foundation +import AppKit +import SwiftUI + +struct SharingsPicker: NSViewRepresentable { + @Binding var isPresented: Bool + var sharingItems: [Any] = [] + + func makeNSView(context: Context) -> NSView { + let view = NSView() + return view + } + + func updateNSView(_ nsView: NSView, context: Context) { + if isPresented { + let picker = NSSharingServicePicker(items: sharingItems) + picker.delegate = context.coordinator + DispatchQueue.main.async { + picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY) + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(owner: self) + } + + class Coordinator: NSObject, NSSharingServicePickerDelegate { + let owner: SharingsPicker + + init(owner: SharingsPicker) { + self.owner = owner + } + + func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) { + sharingServicePicker.delegate = nil + self.owner.isPresented = false + } + } +} diff --git a/RedditOs/Shared/AwardView.swift b/RedditOs/Shared/AwardView.swift new file mode 100644 index 0000000..e092914 --- /dev/null +++ b/RedditOs/Shared/AwardView.swift @@ -0,0 +1,60 @@ +// +// PostAwardView.swift +// RedditOs +// +// Created by Thomas Ricouard on 05/08/2020. +// + +import SwiftUI +import Backend +import KingfisherSwiftUI + +struct AwardsView: View { + let awards: [Award] + + @State private var popoverPresented = false + @State private var descriptionPopoverPresented = false + + var body: some View { + HStack { + ForEach(awards.prefix(3)) { award in + HStack(spacing: 2) { + KFImage(award.staticIconUrl) + .resizable() + .frame(width: 16, height: 16) + } + } + Text("\(awards.map{ $0.count }.reduce(0, +))") + .foregroundColor(.gray) + .font(.footnote) + } + .onTapGesture(count: 1, perform: { + popoverPresented = true + }) + .popover(isPresented: $popoverPresented, content: { + ScrollView { + VStack { + ForEach(awards) { award in + HStack(spacing: 8) { + KFImage(award.staticIconUrl) + .resizable() + .frame(width: 30, height: 30) + Text(award.name) + Spacer() + Text("\(award.count)") + } + Divider() + } + }.padding() + }.frame(width: 250, height: 300) + }) + } +} + +struct PostAwardsView_Previews: PreviewProvider { + static var previews: some View { + AwardsView(awards: [Award.default, Award.default, + Award.default, Award.default, + Award.default, Award.default]) + } +} diff --git a/RedditOs/Shared/FlairView.swift b/RedditOs/Shared/FlairView.swift index 9ec751e..63a0154 100644 --- a/RedditOs/Shared/FlairView.swift +++ b/RedditOs/Shared/FlairView.swift @@ -7,12 +7,20 @@ import SwiftUI import Backend +import KingfisherSwiftUI struct FlairView: View { - let post: SubredditPost + enum Display { + case small, normal + } + + let richText: [FlairRichText]? + let textColorHex: String? + let backgroundColorHex: String? + let display: Display var backgroundColor: Color { - if let color = post.linkFlairBackgroundColor{ + if let color = backgroundColorHex { if color.isEmpty { return .gray } @@ -25,29 +33,45 @@ struct FlairView: View { if backgroundColor == .gray { return .white } - return post.linkFlairTextColor == "dark" ? .black : .white + return textColorHex == "dark" ? .black : .white } @ViewBuilder var body: some View { - if post.linkFlairText == nil || post.linkFlairText?.isEmpty == true { - EmptyView() + if let texts = richText { + HStack(spacing: 4) { + ForEach(texts, id: \.self) { text in + if text.e == "emoji" { + KFImage(text.u!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + } else if text.e == "text" { + Text(text.t!) + .foregroundColor(textColor) + .font(display == .small ? .footnote : .callout) + .fontWeight(.semibold) + } else { + EmptyView() + } + } + } + .padding(4) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(backgroundColor) + ) } else { - Text(post.linkFlairText!) - .foregroundColor(textColor) - .font(.callout) - .fontWeight(.light) - .padding(4) - .background( - RoundedRectangle(cornerRadius: 6) - .fill(backgroundColor) - ) + EmptyView() } } } struct FlairView_Previews: PreviewProvider { static var previews: some View { - FlairView(post: static_listing) + FlairView(richText: nil, + textColorHex: nil, + backgroundColorHex: "#dadada", + display: .normal) } } diff --git a/RedditOs/Shared/PostInfoView.swift b/RedditOs/Shared/PostInfoView.swift index 45d7684..bf196bb 100644 --- a/RedditOs/Shared/PostInfoView.swift +++ b/RedditOs/Shared/PostInfoView.swift @@ -9,49 +9,83 @@ import SwiftUI import Backend struct PostInfoView: View { + enum Display { + case vertical, horizontal + } + @EnvironmentObject private var uiState: UIState let post: SubredditPost + let display: Display + @State private var showUserPopover = false var body: some View { - VStack(alignment: .leading, spacing: 6) { - Text("r/\(post.subreddit)") - .fontWeight(.bold) - .font(.subheadline) + switch display { + case .vertical: + VStack(alignment: .leading, spacing: 6) { + content + }.font(.callout) + case .horizontal: HStack(spacing: 12) { - Button(action: { - showUserPopover = true - }, label: { - HStack(spacing: 4) { - Image(systemName: "person") - Text("u/\(post.author)") - } - }) - .buttonStyle(BorderlessButtonStyle()) - .popover(isPresented: $showUserPopover, content: { - UserPopoverView(username: post.author) - .environmentObject(uiState) - }) - - HStack(spacing: 4) { - Image(systemName: "clock") - Text(post.createdUtc, style: .offset) - } + content + }.font(.callout) + } + } + + @ViewBuilder + var content: some View { + HStack(spacing: 6) { + Button(action: { + uiState.presentedSheetRoute = .subreddit(subreddit: post.subreddit, isSheet: true) + }, label: { + Text("r/\(post.subreddit)") + .fontWeight(.bold) + }) + .buttonStyle(BorderlessButtonStyle()) + + Button(action: { + showUserPopover = true + }, label: { HStack(spacing: 4) { - Image(systemName: "bubble.middle.bottom") - .imageScale(.small) - Text("\(post.numComments)") + Image(systemName: "person") + Text("u/\(post.author)") } + }) + .buttonStyle(BorderlessButtonStyle()) + .popover(isPresented: $showUserPopover, content: { + UserPopoverView(username: post.author) + }) + if let richText = post.authorFlairRichtext, !richText.isEmpty { + FlairView(richText: richText, + textColorHex: post.authorFlairTextColor, + backgroundColorHex: post.authorFlairBackgroundColor, + display: .small) + } + } + HStack(spacing: 12) { + HStack(spacing: 4) { + Image(systemName: "clock") + Text(post.createdUtc, style: .offset) + } + HStack(spacing: 4) { + Image(systemName: "bubble.middle.bottom") + .imageScale(.small) + Text("\(post.numComments)") + } + if !post.allAwardings.isEmpty { + AwardsView(awards: post.allAwardings) } - .font(.callout) - .foregroundColor(.gray) } + .foregroundColor(.gray) } } struct ListingInfoView_Previews: PreviewProvider { static var previews: some View { - PostInfoView(post: static_listing) + Group { + PostInfoView(post: static_listing, display: .horizontal) + PostInfoView(post: static_listing, display: .vertical) + } } } diff --git a/RedditOs/Shared/PostsListView.swift b/RedditOs/Shared/PostsListView.swift index 6068102..45b8b3b 100644 --- a/RedditOs/Shared/PostsListView.swift +++ b/RedditOs/Shared/PostsListView.swift @@ -20,7 +20,7 @@ struct PostsListView: View { SubredditPostRow(post: post, displayMode: $displayMode) .redacted(reason: posts == nil ? .placeholder : []) - } + }.animation(nil) if posts != nil { LoadingRow(text: "Loading next page") .onAppear {