import Dependencies import DependenciesMacros import Foundation import Models import Sharing package struct MediaUpload: Hashable, Sendable { package var data: Data package var mimeType: String package init(data: Data, mimeType: String) { self.data = data self.mimeType = mimeType } } /// Changes to the signed-in account's profile. Pictures are JPEG, already sized: a square avatar and a 3:1 header. package struct ProfileEdit: Equatable, Sendable { package var name: String package var bio: String package var location: String package var website: String package var avatar: Data? package var banner: Data? package init( name: String, bio: String, location: String, website: String, avatar: Data? = nil, banner: Data? = nil ) { self.name = name self.bio = bio self.location = location self.website = website self.avatar = avatar self.banner = banner } } @DependencyClient package struct TwitterClient: Sendable { package var deleteTweet: @Sendable (_ id: String) async throws -> Void package var feed: @Sendable (_ source: FeedSource, _ cursor: String?) async throws -> FeedPage package var lists: @Sendable (_ userID: String, _ cursor: String?) async throws -> ListPage /// Tells Twitter Notifications have been read up to `cursor` (a notifications page's top cursor), which clears /// the unread count and the unread marks. package var markNotificationsRead: @Sendable (_ cursor: String) async throws -> Void package var post: @Sendable ( _ text: String, _ replyTo: String?, _ quoteURL: URL?, _ media: [MediaUpload] ) async throws -> String package var restoreSession: @Sendable () async throws -> User? package var setBlocking: @Sendable (_ isBlocking: Bool, _ userID: String) async throws -> Void package var setBookmarked: @Sendable (_ isBookmarked: Bool, _ tweetID: String) async throws -> Void package var setFollowing: @Sendable (_ isFollowing: Bool, _ userID: String) async throws -> Void package var setLiked: @Sendable (_ isLiked: Bool, _ tweetID: String) async throws -> Void package var setMuting: @Sendable (_ isMuting: Bool, _ userID: String) async throws -> Void /// Pins one of the viewer's own Tweets to the top of their profile (replacing any pinned one), or unpins it. package var setPinned: @Sendable (_ isPinned: Bool, _ tweetID: String) async throws -> Void package var setReposted: @Sendable (_ isReposted: Bool, _ tweetID: String) async throws -> Void package var signIn: @Sendable (_ cookies: [String: String]) async throws -> User package var signOut: @Sendable () async -> Void package var trends: @Sendable () async throws -> [Trend] /// Accounts matching what's been typed after an @, for completing a mention. package var typeahead: @Sendable (_ query: String) async throws -> [User] /// Saves the viewer's profile and returns it as Twitter now has it. package var updateProfile: @Sendable (_ edit: ProfileEdit) async throws -> User /// How many notifications are unread, as the web client's badge shows. package var unreadNotificationCount: @Sendable () async throws -> Int package var user: @Sendable (_ screenName: String) async throws -> User package var users: @Sendable (_ source: UserListSource, _ cursor: String?) async throws -> UserPage /// Votes for `choice` (zero-based) in a post's poll. package var vote: @Sendable (_ tweetID: String, _ poll: Poll, _ choice: Int) async throws -> Void } extension TwitterClient: TestDependencyKey { package static var testValue: TwitterClient { TwitterClient() } package static var previewValue: TwitterClient { TwitterClient( deleteTweet: { _ in }, feed: { source, cursor in .mock(source, cursor: cursor) }, lists: { _, _ in .mock }, markNotificationsRead: { _ in }, post: { _, _, _, _ in "preview" }, restoreSession: { .mock }, setBlocking: { _, _ in }, setBookmarked: { _, _ in }, setFollowing: { _, _ in }, setLiked: { _, _ in }, setMuting: { _, _ in }, setPinned: { _, _ in }, setReposted: { _, _ in }, signIn: { _ in .mock }, signOut: {}, trends: { Trend.mocks }, typeahead: { query in ([User.mock, .mockFriend, .mockBusiness, .mockAffiliate] + User.mockCrowd).filter { $0.screenName.localizedCaseInsensitiveContains(query) || $0.name.localizedCaseInsensitiveContains(query) } }, updateProfile: { edit in var user = User.mock user.name = edit.name user.bio = edit.bio user.location = edit.location user.website = edit.website.isEmpty ? nil : edit.website user.websiteURL = edit.website.isEmpty ? nil : URL(string: edit.website) return user }, unreadNotificationCount: { 3 }, user: { screenName in User.mocks.first { $0.screenName.lowercased() == screenName.lowercased() } ?? .mock }, users: { source, cursor in .mock(source, cursor: cursor) }, vote: { _, _, _ in } ) } } extension SharedKey where Self == FileStorageKey.Default { /// The signed-in account, shared by every window. The main window owns sign-in and sign-out and writes it; other /// windows watch it so they can step aside when the account goes away. /// /// It's kept on disk so the next launch can show the app, and its cached timelines, right away while /// `restoreSession` checks with Twitter that the account is still signed in. package static var viewer: Self { Self[.fileStorage(.applicationSupportDirectory.appending(component: "viewer.json")), default: nil] } }