├── .gitignore ├── Mactodon ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── PushNotificationController.swift ├── Mactodon.entitlements ├── FeedRendering │ ├── FlippedView.swift │ ├── FeedViewCell.swift │ ├── NSTextView+Label.swift │ ├── ProgressIndicatorItem.swift │ ├── FeedViewStatusCellProvider.swift │ ├── PullToRefreshCell.swift │ └── FeedProvider.swift ├── Helpers │ ├── CGRectHelpers.swift │ ├── NSView+BackgroundColor.swift │ └── LayoutHelpers.swift ├── ValuePromise.swift ├── Promise+Helpers.swift ├── Relative Date │ ├── RelativeDate.swift │ └── RelativeDateTextView.swift ├── URLComponents+Helper.swift ├── AppDelegate.swift ├── Settings.swift ├── Mastodon │ ├── Client.swift │ ├── StreamingController.swift │ ├── TokenController.swift │ └── StreamingClient.swift ├── Keychain+MastodonKit.swift ├── AvatarView.swift ├── Info.plist ├── LoginViewController.swift ├── NSTextView+HTML.swift ├── Keychain.swift ├── NotificationFeed │ ├── FollowingItem.swift │ └── FeedViewNotificationCellProvider.swift ├── MultiFeedViewController.swift ├── Promise.swift └── InstanceViewController.swift ├── Pods ├── Target Support Files │ ├── Nuke │ │ ├── Nuke.modulemap │ │ ├── Nuke-dummy.m │ │ ├── Nuke-prefix.pch │ │ ├── Nuke-umbrella.h │ │ ├── Nuke.xcconfig │ │ └── Info.plist │ ├── Atributika │ │ ├── Atributika.modulemap │ │ ├── Atributika-dummy.m │ │ ├── Atributika-prefix.pch │ │ ├── Atributika-umbrella.h │ │ ├── Atributika.xcconfig │ │ └── Info.plist │ ├── Starscream │ │ ├── Starscream.modulemap │ │ ├── Starscream-dummy.m │ │ ├── Starscream-prefix.pch │ │ ├── Starscream-umbrella.h │ │ ├── Starscream.xcconfig │ │ └── Info.plist │ ├── MastodonKit │ │ ├── MastodonKit.modulemap │ │ ├── MastodonKit-dummy.m │ │ ├── MastodonKit-prefix.pch │ │ ├── MastodonKit-umbrella.h │ │ ├── MastodonKit.xcconfig │ │ └── Info.plist │ ├── Pods-Mactodon │ │ ├── Pods-Mactodon.modulemap │ │ ├── Pods-Mactodon-dummy.m │ │ ├── Pods-Mactodon-umbrella.h │ │ ├── Info.plist │ │ ├── Pods-Mactodon.debug.xcconfig │ │ ├── Pods-Mactodon.release.xcconfig │ │ └── Pods-Mactodon-resources.sh │ ├── ReachabilitySwift │ │ ├── ReachabilitySwift.modulemap │ │ ├── ReachabilitySwift-dummy.m │ │ ├── ReachabilitySwift-prefix.pch │ │ ├── ReachabilitySwift-umbrella.h │ │ ├── ReachabilitySwift.xcconfig │ │ └── Info.plist │ └── Pods-MactodonTests │ │ ├── Pods-MactodonTests.modulemap │ │ ├── Pods-MactodonTests-dummy.m │ │ ├── Pods-MactodonTests-acknowledgements.markdown │ │ ├── Pods-MactodonTests-umbrella.h │ │ ├── Info.plist │ │ ├── Pods-MactodonTests-acknowledgements.plist │ │ ├── Pods-MactodonTests.debug.xcconfig │ │ └── Pods-MactodonTests.release.xcconfig ├── MastodonKit │ ├── Sources │ │ └── MastodonKit │ │ │ ├── Models │ │ │ ├── Empty.swift │ │ │ ├── List.swift │ │ │ ├── Tag.swift │ │ │ ├── Application.swift │ │ │ ├── RequestError.swift │ │ │ ├── Context.swift │ │ │ ├── Results.swift │ │ │ ├── Card.swift │ │ │ ├── Report.swift │ │ │ ├── NotificationType.swift │ │ │ ├── AttachmentType.swift │ │ │ ├── Mention.swift │ │ │ ├── AccessScope.swift │ │ │ ├── Emoji.swift │ │ │ ├── Instance.swift │ │ │ ├── ClientApplication.swift │ │ │ ├── Notification.swift │ │ │ ├── LoginSettings.swift │ │ │ ├── Visibility.swift │ │ │ ├── Attachment.swift │ │ │ ├── Relationship.swift │ │ │ ├── Account.swift │ │ │ └── Status.swift │ │ │ ├── Parameter.swift │ │ │ ├── Foundation │ │ │ ├── HTTPURLResponse.swift │ │ │ ├── CharacterSet.swift │ │ │ ├── String.swift │ │ │ ├── Decodable.swift │ │ │ ├── DateFormatter.swift │ │ │ ├── URLRequest.swift │ │ │ ├── URLComponents.swift │ │ │ └── Data.swift │ │ │ ├── Request.swift │ │ │ ├── ClientError.swift │ │ │ ├── Requests │ │ │ ├── Media.swift │ │ │ ├── Instances.swift │ │ │ ├── Mutes.swift │ │ │ ├── Blocks.swift │ │ │ ├── Favourites.swift │ │ │ ├── Search.swift │ │ │ ├── Reports.swift │ │ │ ├── Clients.swift │ │ │ ├── FollowRequests.swift │ │ │ ├── DomainBlocks.swift │ │ │ ├── Notifications.swift │ │ │ ├── Timelines.swift │ │ │ ├── Login.swift │ │ │ ├── Lists.swift │ │ │ └── Statuses.swift │ │ │ ├── ClientType.swift │ │ │ ├── Pagination.swift │ │ │ ├── Result.swift │ │ │ ├── Payload.swift │ │ │ ├── RequestRange.swift │ │ │ ├── HTTPMethod.swift │ │ │ ├── MediaAttachment.swift │ │ │ ├── PaginationItem.swift │ │ │ ├── Functions.swift │ │ │ └── Client.swift │ ├── LICENSE │ └── Readme.md ├── Atributika │ ├── Sources │ │ ├── NSAttributedString+Utils.swift │ │ └── NSScanner+Swift.swift │ └── LICENSE ├── Local Podspecs │ └── MastodonKit.podspec.json ├── ReachabilitySwift │ └── LICENSE ├── Nuke │ ├── LICENSE │ └── Sources │ │ └── ImageTaskMetrics.swift ├── Manifest.lock └── Starscream │ └── Sources │ └── Starscream │ ├── SSLClientCertificate.swift │ └── Compression.swift ├── Mactodon.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── IDETemplateMacros.plist └── xcuserdata │ └── maxvw.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Mactodon.xcworkspace ├── xcshareddata │ ├── WorkspaceSettings.xcsettings │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── Podfile ├── MactodonTests ├── Info.plist ├── RelativeDateTests.swift └── ValuePromiseTests.swift └── Podfile.lock /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata -------------------------------------------------------------------------------- /Mactodon/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Pods/Target Support Files/Nuke/Nuke.modulemap: -------------------------------------------------------------------------------- 1 | framework module Nuke { 2 | umbrella header "Nuke-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Nuke/Nuke-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Nuke : NSObject 3 | @end 4 | @implementation PodsDummy_Nuke 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Atributika/Atributika.modulemap: -------------------------------------------------------------------------------- 1 | framework module Atributika { 2 | umbrella header "Atributika-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Starscream/Starscream.modulemap: -------------------------------------------------------------------------------- 1 | framework module Starscream { 2 | umbrella header "Starscream-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MastodonKit/MastodonKit.modulemap: -------------------------------------------------------------------------------- 1 | framework module MastodonKit { 2 | umbrella header "MastodonKit-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Atributika/Atributika-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Atributika : NSObject 3 | @end 4 | @implementation PodsDummy_Atributika 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MastodonKit/MastodonKit-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_MastodonKit : NSObject 3 | @end 4 | @implementation PodsDummy_MastodonKit 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Mactodon/Pods-Mactodon.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Mactodon { 2 | umbrella header "Pods-Mactodon-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Starscream/Starscream-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Starscream : NSObject 3 | @end 4 | @implementation PodsDummy_Starscream 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Mactodon/Pods-Mactodon-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Mactodon : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Mactodon 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/ReachabilitySwift/ReachabilitySwift.modulemap: -------------------------------------------------------------------------------- 1 | framework module Reachability { 2 | umbrella header "ReachabilitySwift-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MactodonTests/Pods-MactodonTests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_MactodonTests { 2 | umbrella header "Pods-MactodonTests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Mactodon/PushNotificationController.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | 5 | class PushNotificationController { 6 | static let shared = PushNotificationController() 7 | } 8 | -------------------------------------------------------------------------------- /Pods/Target Support Files/ReachabilitySwift/ReachabilitySwift-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_ReachabilitySwift : NSObject 3 | @end 4 | @implementation PodsDummy_ReachabilitySwift 5 | @end 6 | -------------------------------------------------------------------------------- /Mactodon/Mactodon.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MactodonTests/Pods-MactodonTests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_MactodonTests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_MactodonTests 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MactodonTests/Pods-MactodonTests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /Mactodon.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Mactodon.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Mactodon/FeedRendering/FlippedView.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | class FlippedView: NSView { 6 | override var isFlipped: Bool { 7 | get { 8 | return true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Mactodon/FeedRendering/FeedViewCell.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | protocol FeedViewCell { 6 | static var identifier: NSUserInterfaceItemIdentifier { get } 7 | func willDisplay() 8 | func didEndDisplaying() 9 | } 10 | -------------------------------------------------------------------------------- /Mactodon/FeedRendering/NSTextView+Label.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | extension NSTextView { 6 | func prepareAsLabel() { 7 | isEditable = false 8 | backgroundColor = NSColor.clear 9 | isSelectable = true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Nuke/Nuke-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Empty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Empty.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 6/7/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Empty: Codable { } 12 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Atributika/Atributika-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MastodonKit/MastodonKit-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Starscream/Starscream-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Mactodon.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Mactodon.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Pods/Target Support Files/ReachabilitySwift/ReachabilitySwift-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Mactodon.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Mactodon.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | Copyright Max von Webel. All Rights Reserved. 7 | 8 | 9 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :macos, '10.13' 2 | 3 | target 'Mactodon' do 4 | use_frameworks! 5 | 6 | pod 'Atributika' 7 | pod 'MastodonKit', :git => 'https://github.com/MastodonKit/MastodonKit.git', :branch => 'master' 8 | pod 'Nuke' 9 | pod 'ReachabilitySwift' 10 | pod 'Starscream' 11 | 12 | target 'MactodonTests' do 13 | inherit! :search_paths 14 | # Pods for testing 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Nuke/Nuke-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double NukeVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char NukeVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Parameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parameter.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/2/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Parameter { 12 | let name: String 13 | let value: String? 14 | } 15 | 16 | // MARK: - Equatable 17 | 18 | extension Parameter: Equatable {} 19 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/List.swift: -------------------------------------------------------------------------------- 1 | // 2 | // List.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 1/2/18. 6 | // Copyright © 2018 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class List: Codable { 12 | /// The ID of the list. 13 | public let id: String 14 | /// The Title of the list. 15 | public let title: String 16 | } 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Atributika/Atributika-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double AtributikaVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char AtributikaVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Starscream/Starscream-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double StarscreamVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char StarscreamVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MastodonKit/MastodonKit-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double MastodonKitVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char MastodonKitVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Tag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tag.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Tag: Codable { 12 | /// The hashtag, not including the preceding #. 13 | public let name: String 14 | /// The URL of the hashtag. 15 | public let url: String 16 | } 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Mactodon/Pods-Mactodon-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_MactodonVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_MactodonVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Application: Codable { 12 | /// Name of the app. 13 | public let name: String 14 | /// Homepage URL of the app. 15 | public let website: String? 16 | } 17 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Foundation/HTTPURLResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPURLResponse.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 6/1/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension HTTPURLResponse { 12 | var pagination: Pagination? { 13 | return allHeaderFields["Link"].flatMap { $0 as? String }.map(Pagination.init) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Pods/Target Support Files/ReachabilitySwift/ReachabilitySwift-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double ReachabilityVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MactodonTests/Pods-MactodonTests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_MactodonTestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_MactodonTestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Mactodon/Helpers/CGRectHelpers.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | 5 | extension CGRect { 6 | func alignedRect(size: CGSize, horizontalOffset: Float, verticalOffset: Float) -> CGRect { 7 | return CGRect(origin: CGPoint(x: round((width - size.width) * CGFloat(horizontalOffset)), 8 | y: round((height - size.height) * CGFloat(verticalOffset))), 9 | size: size) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Foundation/CharacterSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterSet.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/4/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension CharacterSet { 12 | static let bodyAllowed: CharacterSet = { 13 | CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~/?") 14 | }() 15 | } 16 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/RequestError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestError.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/15/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class MastodonError: Codable { 12 | /// Reason why Mastodon returned an error. 13 | let description: String 14 | 15 | private enum CodingKeys: String, CodingKey { 16 | case description = "error" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/17/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Request { 12 | let path: String 13 | let method: HTTPMethod 14 | 15 | init(path: String, method: HTTPMethod = .get(.empty)) { 16 | self.path = path 17 | self.method = method 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Foundation/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/31/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | func condensed(separator: String = "") -> String { 13 | let components = self.components(separatedBy: .whitespaces) 14 | return components.filter { !$0.isEmpty }.joined(separator: separator) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Foundation/Decodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Decodable.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 12/31/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Decodable { 12 | static func decode(data: Data) throws -> Self { 13 | let decoder = JSONDecoder() 14 | decoder.dateDecodingStrategy = .formatted(.mastodonFormatter) 15 | return try decoder.decode(Self.self, from: data) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Nuke/Nuke.xcconfig: -------------------------------------------------------------------------------- 1 | CODE_SIGN_IDENTITY = 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Nuke 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Nuke 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Context.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Context: Codable { 12 | /// The ancestors of the status in the conversation, as a list of statuses. 13 | public let ancestors: [Status] 14 | /// The descendants of the status in the conversation, as a list of statuses. 15 | public let descendants: [Status] 16 | } 17 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Results.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Results.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/19/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Results: Codable { 12 | /// An array of matched accounts. 13 | public let accounts: [Account] 14 | /// An array of matchhed statuses. 15 | public let statuses: [Status] 16 | /// An array of matched hashtags, as strings. 17 | public let hashtags: [String] 18 | } 19 | -------------------------------------------------------------------------------- /Mactodon/ValuePromise.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | 5 | class ValuePromise { 6 | var value: T { 7 | willSet(newValue) { 8 | willChange.fulfill(newValue) 9 | } 10 | didSet { 11 | didChange.fulfill(value) 12 | } 13 | } 14 | let willChange: Promise 15 | let didChange: Promise 16 | 17 | init(initialValue: T) { 18 | self.value = initialValue 19 | self.didChange = Promise(multiCall: true) 20 | self.willChange = Promise(multiCall: true) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Atributika/Atributika.xcconfig: -------------------------------------------------------------------------------- 1 | CODE_SIGN_IDENTITY = 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Atributika 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Atributika 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MastodonKit/MastodonKit.xcconfig: -------------------------------------------------------------------------------- 1 | CODE_SIGN_IDENTITY = 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/MastodonKit 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/MastodonKit 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Starscream/Starscream.xcconfig: -------------------------------------------------------------------------------- 1 | CODE_SIGN_IDENTITY = 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Starscream 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Starscream 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Mactodon/Promise+Helpers.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | 5 | extension Promise where T: Any { 6 | var mainQueue: Promise { 7 | get { 8 | let promise = Promise(multiCall: self.multiCall) 9 | self.then { (result: T) in 10 | DispatchQueue.main.async { 11 | promise.fulfill(result) 12 | } 13 | } 14 | self.fail { (error) in 15 | DispatchQueue.main.async { 16 | promise.throw(error: error) 17 | } 18 | } 19 | return promise 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Card.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Card.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Card: Codable { 12 | /// The url associated with the card. 13 | public let url: URL 14 | /// The title of the card. 15 | public let title: String 16 | /// The card description. 17 | public let description: String 18 | /// The image associated with the card, if any. 19 | public let image: URL? 20 | } 21 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Report.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Report.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Report: Codable { 12 | /// The ID of the report. 13 | public let id: String 14 | /// The action taken in response to the report. 15 | public let actionTaken: String 16 | 17 | private enum CodingKeys: String, CodingKey { 18 | case id 19 | case actionTaken = "action_taken" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Mactodon.xcodeproj/xcuserdata/maxvw.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Mactodon.xcscheme 8 | 9 | orderHint 10 | 5 11 | 12 | Mactodon.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 7 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Mactodon/Relative Date/RelativeDate.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | 5 | extension TimeInterval { 6 | func relativeString(useSeconds: Bool = true) -> String { 7 | switch self { 8 | case 0..<60: 9 | if useSeconds { 10 | return "\(Int(self))s" 11 | } else { 12 | return "now" 13 | } 14 | case 60..<3600: 15 | return "\(Int(self / 60))m" 16 | case 3600..<(3600*48): 17 | return "\(Int(self / 3600))h" 18 | default: 19 | return "\(Int(self / (3600 * 24)))d" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/NotificationType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationType.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/17/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum NotificationType: String, Codable { 12 | /// The user has been mentioned. 13 | case mention 14 | /// The status message has been reblogged. 15 | case reblog 16 | /// The status message has been favourited. 17 | case favourite 18 | /// The user has a new follower. 19 | case follow 20 | } 21 | -------------------------------------------------------------------------------- /Mactodon/Helpers/NSView+BackgroundColor.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | extension NSView { 6 | var backgroundColor: NSColor? { 7 | set { 8 | guard let backgroundColor = backgroundColor else { 9 | layer?.backgroundColor = nil 10 | return 11 | } 12 | wantsLayer = true 13 | layer!.backgroundColor = backgroundColor.cgColor 14 | } 15 | 16 | get { 17 | guard let color = layer?.backgroundColor else { 18 | return nil 19 | } 20 | return NSColor(cgColor: color) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/AttachmentType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentType.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/17/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum AttachmentType: String, Codable { 12 | /// The attachment contains a static image. 13 | case image 14 | /// The attachment contains a video. 15 | case video 16 | /// The attachment contains a gif image. 17 | case gifv 18 | /// The attachment contains an unknown image file. 19 | case unknown 20 | } 21 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Mention.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mention.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Mention: Codable { 12 | /// Account ID. 13 | public let id: String 14 | /// The username of the account. 15 | public let username: String 16 | /// Equals username for local users, includes @domain for remote ones. 17 | public let acct: String 18 | /// URL of user's profile (can be remote). 19 | public let url: String 20 | } 21 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/AccessScope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessScope.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/17/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum AccessScope: String, Codable { 12 | /// Allows reading data. 13 | case read 14 | /// Allows posting statuses and uploading media for statuses. 15 | case write 16 | /// Allows following, unfollowing, blocking, and unblocking users. 17 | case follow 18 | /// Allows subscribing to push notifications. 19 | case push 20 | } 21 | -------------------------------------------------------------------------------- /Pods/Target Support Files/ReachabilitySwift/ReachabilitySwift.xcconfig: -------------------------------------------------------------------------------- 1 | CODE_SIGN_IDENTITY = 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | OTHER_LDFLAGS = -framework "SystemConfiguration" 5 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_ROOT = ${SRCROOT} 9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/ReachabilitySwift 10 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 11 | SKIP_INSTALL = YES 12 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Foundation/DateFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/22/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension DateFormatter { 12 | static let mastodonFormatter: DateFormatter = { 13 | let dateFormatter = DateFormatter() 14 | 15 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" 16 | dateFormatter.timeZone = TimeZone(abbreviation: "UTC") 17 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 18 | 19 | return dateFormatter 20 | }() 21 | } 22 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Emoji.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Emoji.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 1/1/18. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Emoji: Codable { 12 | /// The shortcode of the emoji 13 | public let shortcode: String 14 | /// URL to the emoji static image 15 | public let staticURL: URL 16 | /// URL to the emoji image 17 | public let url: URL 18 | 19 | private enum CodingKeys: String, CodingKey { 20 | case shortcode 21 | case staticURL = "static_url" 22 | case url 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Mactodon/URLComponents+Helper.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | 5 | extension URLComponents { 6 | init?(string: String, queryItems: [String: String]) { 7 | self.init(string: string) 8 | self.queryItems = queryItems.compactMap({ (name, value) -> URLQueryItem in 9 | return URLQueryItem(name: name, value: value) 10 | }) 11 | } 12 | } 13 | 14 | extension URLComponents { 15 | var queryDict: Dictionary { 16 | get { 17 | return queryItems?.reduce(into: Dictionary(), { (dict, item) in 18 | dict[item.name] = item.value 19 | }) ?? [:] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/ClientError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientError.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/22/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ClientError: Error { 12 | /// Failed to build the URL to make the request. 13 | case malformedURL 14 | /// Failed to parse the Mastodon's JSON reponse. 15 | case malformedJSON 16 | /// Failed to parse Mastodon's model. 17 | case invalidModel 18 | /// Generic error. 19 | case genericError 20 | /// The Mastodon service returned an error. 21 | case mastodonError(String) 22 | } 23 | -------------------------------------------------------------------------------- /Mactodon/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | @NSApplicationMain 6 | class AppDelegate: NSObject, NSApplicationDelegate { 7 | @IBOutlet var switchToUserTimeline: NSMenuItem! 8 | @IBOutlet var switchToLocalTimeline: NSMenuItem! 9 | @IBOutlet var switchToFederatedTimeline: NSMenuItem! 10 | @IBOutlet var switchToNotifications: NSMenuItem! 11 | 12 | func application(_ application: NSApplication, open urls: [URL]) { 13 | urls.forEach { (url) in 14 | TokenController.handleCallback(url: url) 15 | } 16 | } 17 | 18 | static func Shared() -> AppDelegate { 19 | return NSApplication.shared.delegate as! AppDelegate 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Foundation/URLRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/22/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URLRequest { 12 | init(url: URL, request: Request, accessToken: String?) { 13 | self.init(url: url, timeoutInterval: 30) 14 | 15 | httpMethod = request.method.name 16 | httpBody = request.method.httpBody 17 | 18 | setValue(accessToken.map { "Bearer \($0)" }, forHTTPHeaderField: "Authorization") 19 | setValue(request.method.contentType, forHTTPHeaderField: "Content-Type") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Media.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Media.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Media` requests. 12 | public enum Media { 13 | /// Uploads a media attachment. 14 | /// 15 | /// - Parameter mediaAttachment: The media attachment to upload. 16 | /// - Returns: Request for `Attachment`. 17 | public static func upload(media mediaAttachment: MediaAttachment) -> Request { 18 | let method = HTTPMethod.post(.media(mediaAttachment)) 19 | return Request(path: "/api/v1/media", method: method) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Instance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Instance.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Instance: Codable { 12 | /// URI of the current instance. 13 | public let uri: String 14 | /// The instance's title. 15 | public let title: String 16 | /// A description for the instance. 17 | public let description: String 18 | /// An email address which can be used to contact the instance administrator. 19 | public let email: String 20 | /// The Mastodon version used by instance (as of version 1.3). 21 | public let version: String? 22 | } 23 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Foundation/URLComponents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLComponents.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/22/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URLComponents { 12 | init?(baseURL: String, request: Request) { 13 | guard 14 | let realBaseURL = URL(string: baseURL), 15 | let completeURL = URL(string: request.path, relativeTo: realBaseURL) 16 | else { 17 | return nil 18 | } 19 | 20 | self.init(url: completeURL, resolvingAgainstBaseURL: true) 21 | 22 | path = request.path 23 | queryItems = request.method.queryItems 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MactodonTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Instances.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Instances.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/17/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Instances` requests. 12 | public enum Instances { 13 | /// Gets instance information. 14 | /// 15 | /// - Returns: Request for `Instance`. 16 | public static func current() -> Request { 17 | return Request(path: "/api/v1/instance") 18 | } 19 | 20 | /// Fetches current instance's custom emojis. 21 | /// 22 | /// - Returns: Request for `[Emoji]`. 23 | public static func customEmojis() -> Request<[Emoji]> { 24 | return Request<[Emoji]>(path: "/api/v1/custom_emojis") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Mutes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mutes.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Mutes` requests 12 | public enum Mutes { 13 | /// Fetches a user's mutes. 14 | /// 15 | /// - Parameter range: The bounds used when requesting data from Mastodon. 16 | /// - Returns: Request for `[Account]`. 17 | public static func all(range: RequestRange = .default) -> Request<[Account]> { 18 | let parameters = range.parameters(limit: between(1, and: 80, default: 40)) 19 | let method = HTTPMethod.get(.parameters(parameters)) 20 | 21 | return Request<[Account]>(path: "/api/v1/mutes", method: method) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Blocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Blocks.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Blocks` requests. 12 | public enum Blocks { 13 | /// Fetches a user's blocks. 14 | /// 15 | /// - Parameter range: The bounds used when requesting data from Mastodon. 16 | /// - Returns: Request for `[Account]`. 17 | public static func all(range: RequestRange = .default) -> Request<[Account]> { 18 | let parameters = range.parameters(limit: between(1, and: 80, default: 40)) 19 | let method = HTTPMethod.get(.parameters(parameters)) 20 | 21 | return Request<[Account]>(path: "/api/v1/blocks", method: method) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Pods/Atributika/Sources/NSAttributedString+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Pavel Sharanda on 21.02.17. 3 | // Copyright © 2017 psharanda. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | public func + (lhs: NSAttributedString, rhs: NSAttributedString) -> NSAttributedString { 9 | let s = NSMutableAttributedString(attributedString: lhs) 10 | s.append(rhs) 11 | return s 12 | } 13 | 14 | public func + (lhs: String, rhs: NSAttributedString) -> NSAttributedString { 15 | let s = NSMutableAttributedString(string: lhs) 16 | s.append(rhs) 17 | return s 18 | } 19 | 20 | public func + (lhs: NSAttributedString, rhs: String) -> NSAttributedString { 21 | let s = NSMutableAttributedString(attributedString: lhs) 22 | s.append(NSAttributedString(string: rhs)) 23 | return s 24 | } 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Favourites.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Favourites.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Favourites` requests. 12 | public enum Favourites { 13 | /// Fetches a user's favourites. 14 | /// 15 | /// - Parameter range: The bounds used when requesting data from Mastodon. 16 | /// - Returns: Request for `[Status]`. 17 | public static func all(range: RequestRange = .default) -> Request<[Status]> { 18 | let parameters = range.parameters(limit: between(1, and: 40, default: 20)) 19 | let method = HTTPMethod.get(.parameters(parameters)) 20 | 21 | return Request<[Status]>(path: "/api/v1/favourites", method: method) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/ClientApplication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientApplication.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/17/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class ClientApplication: Codable { 12 | /// The application ID. 13 | public let id: String 14 | /// Where the user should be redirected after authorization. 15 | public let redirectURI: String 16 | /// The application client ID. 17 | public let clientID: String 18 | /// The application client secret. 19 | public let clientSecret: String 20 | 21 | private enum CodingKeys: String, CodingKey { 22 | case id 23 | case redirectURI = "redirect_uri" 24 | case clientID = "client_id" 25 | case clientSecret = "client_secret" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Pods/Local Podspecs/MastodonKit.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MastodonKit", 3 | "version": "2.0.0", 4 | "license": "MIT", 5 | "summary": "MastodonKit is a Swift Framework that wraps the Mastodon API.", 6 | "description": "MastodonKit covers all the endpoints and entities from Mastodon's API and is designed to be simple to use.", 7 | "homepage": "https://github.com/MastodonKit/MastodonKit", 8 | "documentation_url": "https://mastodonkit.github.io/MastodonKit", 9 | "social_media_url": "https://mastodon.technology/@ornithocoder", 10 | "authors": { 11 | "Ornithologist Coder": "ornithocoder@users.noreply.github.com" 12 | }, 13 | "source": { 14 | "git": "https://github.com/MastodonKit/MastodonKit.git", 15 | "tag": "2.0.0" 16 | }, 17 | "platforms": { 18 | "ios": "10.0", 19 | "osx": "10.12" 20 | }, 21 | "swift_version": "4.2", 22 | "source_files": "Sources/**/*.swift" 23 | } 24 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Notification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Notification: Codable { 12 | /// The notification ID. 13 | public let id: String 14 | /// The notification type. 15 | public let type: NotificationType 16 | /// The time the notification was created. 17 | public let createdAt: Date 18 | /// The Account sending the notification to the user. 19 | public let account: Account 20 | /// The Status associated with the notification, if applicable. 21 | public let status: Status? 22 | 23 | private enum CodingKeys: String, CodingKey { 24 | case id 25 | case type 26 | case createdAt = "created_at" 27 | case account 28 | case status 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Foundation/Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Data { 12 | init?(mediaAttachment: MediaAttachment) { 13 | guard let mediaData = mediaAttachment.data else { return nil } 14 | 15 | self.init() 16 | 17 | append("--MastodonKitBoundary\r\n") 18 | append("Content-Disposition: form-data; name=\"file\"; filename=\"\(mediaAttachment.fileName)\"\r\n") 19 | append("Content-Type: \(mediaAttachment.mimeType)\r\n\r\n") 20 | append(mediaData) 21 | append("\r\n") 22 | append("--MastodonKitBoundary--\r\n") 23 | } 24 | 25 | mutating func append(_ string: String?) { 26 | guard let data = string?.data(using: .utf8) else { return } 27 | append(data) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Search.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Search.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Search` requests. 12 | public enum Search { 13 | /// Searches for content. 14 | /// 15 | /// - Parameters: 16 | /// - query: The search query. 17 | /// - resolve: Whether to resolve non-local accounts. 18 | /// - Returns: Request for `Results`. 19 | public static func search(query: String, resolve: Bool? = nil) -> Request { 20 | let parameters = [ 21 | Parameter(name: "q", value: query), 22 | Parameter(name: "resolve", value: resolve.flatMap(trueOrNil)) 23 | ] 24 | 25 | let method = HTTPMethod.get(.parameters(parameters)) 26 | return Request(path: "/api/v1/search", method: method) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Nuke/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 7.5.2 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Atributika/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 4.6.8 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/MastodonKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Starscream/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 3.0.6 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Mactodon/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/ReachabilitySwift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 4.3.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MactodonTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MactodonTests/Pods-MactodonTests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /MactodonTests/RelativeDateTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import XCTest 4 | @testable import Mactodon 5 | 6 | class RelativeDateTests: XCTestCase { 7 | 8 | func testSeconds() { 9 | let string = TimeInterval(exactly: 32.5)!.relativeString(useSeconds: true) 10 | XCTAssertEqual(string, "32s") 11 | } 12 | 13 | func testNow() { 14 | let string = TimeInterval(exactly: 42)!.relativeString(useSeconds: false) 15 | XCTAssertEqual(string, "now") 16 | } 17 | 18 | func testMinutes() { 19 | let string = TimeInterval(exactly: 650)!.relativeString(useSeconds: false) 20 | XCTAssertEqual(string, "10m") 21 | } 22 | 23 | func testHours() { 24 | let string = TimeInterval(exactly: 8000)!.relativeString(useSeconds: false) 25 | XCTAssertEqual(string, "2h") 26 | } 27 | 28 | func testDays() { 29 | let string = TimeInterval(exactly: 7 * 24 * 3600 + 5000)!.relativeString(useSeconds: false) 30 | XCTAssertEqual(string, "7d") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/LoginSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginSettings.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/18/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class LoginSettings: Codable { 12 | /// The user's access token. 13 | public let accessToken: String 14 | /// Access token type. 15 | public let accessTokenType: String 16 | /// Date when the access token was retrieved. 17 | public let createdAt: TimeInterval 18 | /// Access scope. 19 | private let scope: String 20 | /// Access scopes. 21 | public var scopes: [AccessScope] { 22 | return scope 23 | .components(separatedBy: .whitespaces) 24 | .compactMap(toAccessScope) 25 | } 26 | 27 | private enum CodingKeys: String, CodingKey { 28 | case accessToken = "access_token" 29 | case accessTokenType = "token_type" 30 | case scope 31 | case createdAt = "created_at" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Mactodon/FeedRendering/ProgressIndicatorItem.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | class ProgressIndicatorItem: NSCollectionViewItem, FeedViewCell { 6 | static var identifier: NSUserInterfaceItemIdentifier = NSUserInterfaceItemIdentifier(rawValue: "ProgressIndicatorItem") 7 | 8 | func willDisplay() { 9 | // 10 | } 11 | 12 | func didEndDisplaying() { 13 | // 14 | } 15 | 16 | override func loadView() { 17 | view = NSView() 18 | view.backgroundColor = NSColor.orange 19 | } 20 | 21 | override func viewDidLoad() { 22 | let spinner = NSProgressIndicator(frame: .zero) 23 | spinner.style = .spinning 24 | spinner.sizeToFit() 25 | spinner.frame = view.bounds.alignedRect(size: spinner.bounds.size, horizontalOffset: 0.5, verticalOffset: 0) 26 | spinner.autoresizingMask = [.minXMargin, .maxXMargin, .maxYMargin] 27 | spinner.startAnimation(nil) 28 | view.addSubview(spinner) 29 | } 30 | 31 | static func size(width: CGFloat) -> CGSize { 32 | return CGSize(width: width, height: 200) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Mactodon/Settings.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | 5 | struct Settings: Codable { 6 | struct Account: Codable, CustomDebugStringConvertible { 7 | let username: String 8 | let instance: String 9 | 10 | init(_ username: String, _ instance: String) { 11 | self.username = username 12 | self.instance = instance 13 | } 14 | 15 | var debugDescription: String { 16 | get { 17 | return "\(username)@\(instance)" 18 | } 19 | } 20 | } 21 | var accounts: [Account] = [] 22 | 23 | private static var shared: Settings? 24 | 25 | static func load() -> Settings { 26 | if let data = UserDefaults.standard.object(forKey: "Settings") as? Data { 27 | return (try? PropertyListDecoder().decode(Settings.self, from: data)) ?? Settings() 28 | } else { 29 | return Settings() 30 | } 31 | } 32 | 33 | func save() { 34 | Settings.shared = self 35 | UserDefaults.standard.set(try! PropertyListEncoder().encode(self), forKey: "Settings") 36 | UserDefaults.standard.synchronize() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Pods/MastodonKit/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ornithologist Coder and MastodonKit Contributors. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Pods/ReachabilitySwift/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Ashley Mills 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Mactodon/Mastodon/Client.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | import MastodonKit 5 | 6 | extension Client { 7 | struct Response { 8 | let value: Model 9 | let pagination: Pagination? 10 | } 11 | 12 | func runPaginated(_ request: Request) -> Promise> { 13 | return Promise({ (completion, promise) in 14 | self.run(request, completion: { (result) in 15 | switch result { 16 | case .failure(let error): 17 | promise.throw(error: error) 18 | case .success(let value, let pagination): 19 | completion(Response(value: value, pagination: pagination)) 20 | } 21 | }) 22 | }) 23 | } 24 | 25 | func run(_ request: Request) -> Promise { 26 | return Promise({ (completion, promise) in 27 | self.run(request, completion: { (result) in 28 | switch result { 29 | case .failure(let error): 30 | promise.throw(error: error) 31 | case .success(let model, _): 32 | completion(model) 33 | } 34 | }) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MactodonTests/Pods-MactodonTests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CODE_SIGN_IDENTITY = 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Atributika" "${PODS_CONFIGURATION_BUILD_DIR}/MastodonKit" "${PODS_CONFIGURATION_BUILD_DIR}/Nuke" "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift" "${PODS_CONFIGURATION_BUILD_DIR}/Starscream" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Atributika/Atributika.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/MastodonKit/MastodonKit.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Nuke/Nuke.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/Reachability.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Starscream/Starscream.framework/Headers" 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 9 | PODS_ROOT = ${SRCROOT}/Pods 10 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-MactodonTests/Pods-MactodonTests.release.xcconfig: -------------------------------------------------------------------------------- 1 | CODE_SIGN_IDENTITY = 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Atributika" "${PODS_CONFIGURATION_BUILD_DIR}/MastodonKit" "${PODS_CONFIGURATION_BUILD_DIR}/Nuke" "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift" "${PODS_CONFIGURATION_BUILD_DIR}/Starscream" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Atributika/Atributika.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/MastodonKit/MastodonKit.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Nuke/Nuke.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/Reachability.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Starscream/Starscream.framework/Headers" 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 9 | PODS_ROOT = ${SRCROOT}/Pods 10 | -------------------------------------------------------------------------------- /Pods/Atributika/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Pavel Sharanda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Pods/Nuke/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Alexander Grebenyuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/ClientType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientType.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 6/12/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ClientType { 12 | /// The user access token used to perform the network requests. 13 | var accessToken: String? { get set } 14 | 15 | /// Mastodon Client's initializer. 16 | /// 17 | /// - Parameters: 18 | /// - baseURL: The Mastodon instance URL 19 | /// - accessToken: The user access token used to perform the network requests (optional). 20 | /// - session: The URLSession used to perform the network requests. 21 | init(baseURL: String, accessToken: String?, session: URLSession) 22 | 23 | /// Performs the network request. 24 | /// 25 | /// - Parameters: 26 | /// - request: The request to be performed. 27 | /// - completion: The completion block to be called when the request is complete. 28 | /// - result: The request result. 29 | func run(_ request: Request, completion: @escaping (_ result: Result) -> Void) 30 | } 31 | -------------------------------------------------------------------------------- /Mactodon/Keychain+MastodonKit.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | import MastodonKit 5 | 6 | extension Keychain { 7 | private static let appName = "Mactodon" 8 | static func set(clientApplication: ClientApplication, instance: String) throws { 9 | try set(service: appName, account: instance, value: clientApplication) 10 | } 11 | 12 | static func getClientApplication(instance: String) throws -> ClientApplication? { 13 | return try get(service: appName, account: instance, type: ClientApplication.self) 14 | } 15 | } 16 | 17 | extension Keychain { 18 | private static func full(username: String, _ instance: String) -> String { 19 | return "\(username)@\(instance)" 20 | } 21 | 22 | static func set(loginSettings: LoginSettings, forUser username: String, instance: String) throws { 23 | try set(service: appName, account: full(username: username, instance), value: loginSettings) 24 | } 25 | 26 | static func getLoginSettings(forUser username: String, instance: String) throws -> LoginSettings? { 27 | return try get(service: appName, account: full(username: username, instance), type: LoginSettings.self) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Mactodon/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Visibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Visibility.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/22/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Visibility: String, Codable { 12 | /// The status message is public. 13 | /// - Visible on Profile: Anyone incl. anonymous viewers. 14 | /// - Visible on Public Timeline: Yes. 15 | /// - Federates to other instances: Yes. 16 | case `public` 17 | /// The status message is unlisted. 18 | /// - Visible on Profile: Anyone incl. anonymous viewers. 19 | /// - Visible on Public Timeline: No. 20 | /// - Federates to other instances: Yes. 21 | case unlisted 22 | /// The status message is private. 23 | /// - Visible on Profile: Followers only. 24 | /// - Visible on Public Timeline: No. 25 | /// - Federates to other instances: Only remote @mentions. 26 | case `private` 27 | /// The status message is direct. 28 | /// - Visible on Profile: No. 29 | /// - Visible on Public Timeline: No. 30 | /// - Federates to other instances: Only remote @mentions. 31 | case direct 32 | } 33 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Pagination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pagination.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 6/1/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Pagination { 12 | /// The request range for fetching the next page. 13 | public let next: RequestRange? 14 | /// The request range for fetching the previous page. 15 | public let previous: RequestRange? 16 | } 17 | 18 | extension Pagination { 19 | init(string: String) { 20 | let links = string 21 | .components(separatedBy: ",") 22 | .compactMap(PaginationItem.init) 23 | 24 | var nextLink: RequestRange? 25 | var previousLink: RequestRange? 26 | 27 | for link in links { 28 | switch link.type { 29 | case .next: nextLink = .max(id: link.id, limit: link.limit) 30 | case .prev: previousLink = .since(id: link.id, limit: link.limit) 31 | } 32 | } 33 | 34 | self.next = nextLink 35 | self.previous = previousLink 36 | } 37 | } 38 | 39 | // MARK: - Equatable 40 | 41 | extension Pagination: Equatable {} 42 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Atributika (4.6.8) 3 | - MastodonKit (2.0.0) 4 | - Nuke (7.5.2) 5 | - ReachabilitySwift (4.3.0) 6 | - Starscream (3.0.6) 7 | 8 | DEPENDENCIES: 9 | - Atributika 10 | - MastodonKit (from `https://github.com/MastodonKit/MastodonKit.git`, branch `master`) 11 | - Nuke 12 | - ReachabilitySwift 13 | - Starscream 14 | 15 | SPEC REPOS: 16 | https://github.com/cocoapods/specs.git: 17 | - Atributika 18 | - Nuke 19 | - ReachabilitySwift 20 | - Starscream 21 | 22 | EXTERNAL SOURCES: 23 | MastodonKit: 24 | :branch: master 25 | :git: https://github.com/MastodonKit/MastodonKit.git 26 | 27 | CHECKOUT OPTIONS: 28 | MastodonKit: 29 | :commit: e11d68e75b604cc8da701286581ed8c7a17057b9 30 | :git: https://github.com/MastodonKit/MastodonKit.git 31 | 32 | SPEC CHECKSUMS: 33 | Atributika: 7510276c8c58194244f1b111f1c9b90350bc37a9 34 | MastodonKit: 3bc591ea7e626dd160711f9a10cd50f192e46ed3 35 | Nuke: 0350d346a688426e8f2331253ef28dc2fc4f6178 36 | ReachabilitySwift: 408477d1b6ed9779dba301953171e017c31241f3 37 | Starscream: ef3ece99d765eeccb67de105bfa143f929026cf5 38 | 39 | PODFILE CHECKSUM: 9ded5ca726a4ea9c68ac1312827af6d14be541ca 40 | 41 | COCOAPODS: 1.5.3 42 | -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Atributika (4.6.8) 3 | - MastodonKit (2.0.0) 4 | - Nuke (7.5.2) 5 | - ReachabilitySwift (4.3.0) 6 | - Starscream (3.0.6) 7 | 8 | DEPENDENCIES: 9 | - Atributika 10 | - MastodonKit (from `https://github.com/MastodonKit/MastodonKit.git`, branch `master`) 11 | - Nuke 12 | - ReachabilitySwift 13 | - Starscream 14 | 15 | SPEC REPOS: 16 | https://github.com/cocoapods/specs.git: 17 | - Atributika 18 | - Nuke 19 | - ReachabilitySwift 20 | - Starscream 21 | 22 | EXTERNAL SOURCES: 23 | MastodonKit: 24 | :branch: master 25 | :git: https://github.com/MastodonKit/MastodonKit.git 26 | 27 | CHECKOUT OPTIONS: 28 | MastodonKit: 29 | :commit: e11d68e75b604cc8da701286581ed8c7a17057b9 30 | :git: https://github.com/MastodonKit/MastodonKit.git 31 | 32 | SPEC CHECKSUMS: 33 | Atributika: 7510276c8c58194244f1b111f1c9b90350bc37a9 34 | MastodonKit: 3bc591ea7e626dd160711f9a10cd50f192e46ed3 35 | Nuke: 0350d346a688426e8f2331253ef28dc2fc4f6178 36 | ReachabilitySwift: 408477d1b6ed9779dba301953171e017c31241f3 37 | Starscream: ef3ece99d765eeccb67de105bfa143f929026cf5 38 | 39 | PODFILE CHECKSUM: 9ded5ca726a4ea9c68ac1312827af6d14be541ca 40 | 41 | COCOAPODS: 1.5.3 42 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Attachment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attachment.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Attachment: Codable { 12 | /// ID of the attachment. 13 | public let id: String 14 | /// Type of the attachment. 15 | public let type: AttachmentType 16 | /// URL of the locally hosted version of the image. 17 | public let url: String 18 | /// For remote images, the remote URL of the original image. 19 | public let remoteURL: String? 20 | /// URL of the preview image. 21 | public let previewURL: String 22 | /// Shorter URL for the image, for insertion into text (only present on local images). 23 | public let textURL: String? 24 | /// A description of the image for the visually impaired. 25 | public let description: String? 26 | 27 | private enum CodingKeys: String, CodingKey { 28 | case id 29 | case type 30 | case url 31 | case remoteURL = "remote_url" 32 | case previewURL = "preview_url" 33 | case textURL = "text_url" 34 | case description 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Mactodon/AvatarView.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | class AvatarView: NSImageView { 6 | var clickURL: URL? 7 | 8 | enum Sizes { 9 | case regular 10 | case small 11 | } 12 | 13 | static func size(_ size: Sizes) -> CGSize { 14 | switch size { 15 | case .regular: 16 | return CGSize(width: 48, height: 48) 17 | case .small: 18 | return CGSize(width: 20, height: 20) 19 | } 20 | } 21 | 22 | override init(frame frameRect: NSRect) { 23 | super.init(frame: frameRect) 24 | wantsLayer = true 25 | layer!.masksToBounds = true 26 | backgroundColor = NSColor.textColor.withAlphaComponent(0.1) 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | override func layout() { 34 | super.layout() 35 | layer?.cornerRadius = min(bounds.height, bounds.width) / 2.0 36 | } 37 | 38 | override func mouseDown(with event: NSEvent) { 39 | if clickURL != nil { 40 | alphaValue = 0.6 41 | } 42 | } 43 | 44 | override func mouseUp(with event: NSEvent) { 45 | alphaValue = 1.0 46 | 47 | if let clickURL = clickURL { 48 | NSWorkspace.shared.open(clickURL) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Reports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reports.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Reports` requests. 12 | public enum Reports { 13 | /// Fetches a user's reports. 14 | /// 15 | /// - Returns: Request for `[Report]`. 16 | public static func all() -> Request<[Report]> { 17 | return Request<[Report]>(path: "/api/v1/reports") 18 | } 19 | 20 | /// Reports a user. 21 | /// 22 | /// - Parameters: 23 | /// - accountID: The ID of the account to report. 24 | /// - statusIDs: The IDs of statuses to report. 25 | /// - reason: A comment to associate with the report. 26 | /// - Returns: Request for `Report`. 27 | public static func report(accountID: String, statusIDs: [String], reason: String) -> Request { 28 | let parameters = [ 29 | Parameter(name: "account_id", value: accountID), 30 | Parameter(name: "comment", value: reason) 31 | ] + statusIDs.map(toArrayOfParameters(withName: "status_ids")) 32 | 33 | let method = HTTPMethod.post(.parameters(parameters)) 34 | return Request(path: "/api/v1/reports", method: method) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Mactodon/Mastodon/StreamingController.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | import MastodonKit 5 | 6 | private class InstanceUrls: Codable { 7 | public let urls: [String: String] 8 | } 9 | 10 | class StreamingController { 11 | let client: Client 12 | let instanceDomain: String 13 | 14 | lazy var userStream: StreamingClient = { 15 | return streamingClient(timeline: .User) 16 | }() 17 | 18 | lazy var localStream: StreamingClient = { 19 | return streamingClient(timeline: .Local) 20 | }() 21 | 22 | lazy var federatedStream: StreamingClient = { 23 | return streamingClient(timeline: .Federated) 24 | }() 25 | 26 | init(client: Client, instanceDomain: String) { 27 | assert(client.accessToken != nil) 28 | self.client = client 29 | self.instanceDomain = instanceDomain 30 | } 31 | 32 | static func controller(client: Client) -> Promise { 33 | return client.run(Instances.current()).map { return StreamingController(client: client, instanceDomain: $0.uri) } 34 | } 35 | 36 | func streamingClient(timeline: StreamingClient.Timeline) -> StreamingClient { 37 | let client = StreamingClient(instance: instanceDomain, timeline: timeline, accessToken: self.client.accessToken!) 38 | client.connect() 39 | return client 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Mactodon/Pods-Mactodon.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CODE_SIGN_IDENTITY = 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Atributika" "${PODS_CONFIGURATION_BUILD_DIR}/MastodonKit" "${PODS_CONFIGURATION_BUILD_DIR}/Nuke" "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift" "${PODS_CONFIGURATION_BUILD_DIR}/Starscream" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' 6 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Atributika/Atributika.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/MastodonKit/MastodonKit.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Nuke/Nuke.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/Reachability.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Starscream/Starscream.framework/Headers" 7 | OTHER_LDFLAGS = $(inherited) -framework "Atributika" -framework "MastodonKit" -framework "Nuke" -framework "Reachability" -framework "Starscream" 8 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 9 | PODS_BUILD_DIR = ${BUILD_DIR} 10 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 11 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 12 | PODS_ROOT = ${SRCROOT}/Pods 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Mactodon/Pods-Mactodon.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CODE_SIGN_IDENTITY = 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Atributika" "${PODS_CONFIGURATION_BUILD_DIR}/MastodonKit" "${PODS_CONFIGURATION_BUILD_DIR}/Nuke" "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift" "${PODS_CONFIGURATION_BUILD_DIR}/Starscream" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' 6 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Atributika/Atributika.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/MastodonKit/MastodonKit.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Nuke/Nuke.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/Reachability.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Starscream/Starscream.framework/Headers" 7 | OTHER_LDFLAGS = $(inherited) -framework "Atributika" -framework "MastodonKit" -framework "Nuke" -framework "Reachability" -framework "Starscream" 8 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 9 | PODS_BUILD_DIR = ${BUILD_DIR} 10 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 11 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 12 | PODS_ROOT = ${SRCROOT}/Pods 13 | -------------------------------------------------------------------------------- /Mactodon/FeedRendering/FeedViewStatusCellProvider.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | import MastodonKit 5 | 6 | class FeedViewStatusCellProvider: FeedViewCellProvider { 7 | weak var delegate: FeedViewCellProviderDelegate? 8 | 9 | var feedProvider: TypelessFeedProvider { 10 | get { 11 | return statusFeedProvider 12 | } 13 | } 14 | 15 | private let statusFeedProvider: FeedProvider 16 | 17 | init(feedProvider: FeedProvider) { 18 | self.statusFeedProvider = feedProvider 19 | } 20 | 21 | func prepare(collectionView: NSCollectionView) { 22 | collectionView.register(TootItem.self, forItemWithIdentifier: TootItem.identifier) 23 | } 24 | 25 | var itemCount: Int { 26 | get { 27 | return statusFeedProvider.items.count 28 | } 29 | } 30 | 31 | func item(collectionView: NSCollectionView, indexPath: IndexPath, index: Int) -> NSCollectionViewItem { 32 | let view = collectionView.makeItem(withIdentifier: TootItem.identifier, for: indexPath) as! TootItem 33 | view.model = TootItemModel(status: statusFeedProvider.items[index]) 34 | return view 35 | } 36 | 37 | func itemSize(collectionView: NSCollectionView, indexPath: IndexPath, index: Int) -> CGSize { 38 | return TootItem.size(width: collectionView.bounds.width, toot: TootItemModel(status: statusFeedProvider.items[index])) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 6/6/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Result { 12 | /// Success wraps a model and an optional pagination 13 | case success(Model, Pagination?) 14 | /// Failure wraps an ErrorType 15 | case failure(Error) 16 | } 17 | 18 | public extension Result { 19 | /// Convenience getter for the value. 20 | var value: Model? { 21 | switch self { 22 | case .success(let value, _): return value 23 | case .failure: return nil 24 | } 25 | } 26 | 27 | /// Convenience getter for the pagination. 28 | var pagination: Pagination? { 29 | switch self { 30 | case .success(_, let pagination): return pagination 31 | case .failure: return nil 32 | } 33 | } 34 | 35 | /// Convenience getter for the error. 36 | var error: Error? { 37 | switch self { 38 | case .success: return nil 39 | case .failure(let error): return error 40 | } 41 | } 42 | 43 | /// Convenience getter to test whether the result is an error or not. 44 | var isError: Bool { 45 | switch self { 46 | case .success: return false 47 | case .failure: return true 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Relationship.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Relationship.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Relationship: Codable { 12 | /// Target account id. 13 | public let id: String 14 | /// Whether the user is currently following the account. 15 | public let following: Bool 16 | /// Whether the user is currently being followed by the account. 17 | public let followedBy: Bool 18 | /// Whether the user is currently blocking the account. 19 | public let blocking: Bool 20 | /// Whether the user is currently muting the account. 21 | public let muting: Bool 22 | /// Whether the user is also muting notifications 23 | public let mutingNotifications: Bool 24 | /// Whether the user has requested to follow the account. 25 | public let requested: Bool 26 | /// Whether the user is currently blocking the user's domain. 27 | public let domainBlocking: Bool 28 | 29 | private enum CodingKeys: String, CodingKey { 30 | case id 31 | case following 32 | case followedBy = "followed_by" 33 | case blocking 34 | case muting 35 | case mutingNotifications = "muting_notifications" 36 | case requested 37 | case domainBlocking = "domain_blocking" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Payload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Payload.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/28/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Payload { 12 | case parameters([Parameter]?) 13 | case media(MediaAttachment?) 14 | case empty 15 | } 16 | 17 | extension Payload { 18 | var items: [URLQueryItem]? { 19 | switch self { 20 | case .parameters(let parameters): return parameters?.compactMap(toQueryItem) 21 | case .media: return nil 22 | case .empty: return nil 23 | } 24 | } 25 | 26 | var data: Data? { 27 | switch self { 28 | case .parameters(let parameters): 29 | return parameters? 30 | .compactMap(toString) 31 | .joined(separator: "&") 32 | .data(using: .utf8) 33 | case .media(let mediaAttachment): return mediaAttachment.flatMap(Data.init) 34 | case .empty: return nil 35 | } 36 | } 37 | 38 | var type: String? { 39 | switch self { 40 | case .parameters(let parameters): 41 | return parameters.map { _ in "application/x-www-form-urlencoded; charset=utf-8" } 42 | case .media(let mediaAttachment): 43 | return mediaAttachment.map { _ in "multipart/form-data; boundary=MastodonKitBoundary" } 44 | case .empty: return nil 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Clients.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Clients.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/17/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Clients` requests. 12 | public enum Clients { 13 | /// Registers an application. 14 | /// 15 | /// - Parameters: 16 | /// - appName: Name of your application. 17 | /// - redirectURI: Where the user should be redirected after authorization (for no redirect, omit this parameter). 18 | /// - scopes: Application's access scopes. 19 | /// - website: URL to the homepage of your app. 20 | /// - Returns: Request for `ClientApplication`. 21 | public static func register(clientName: String, 22 | redirectURI: String = "urn:ietf:wg:oauth:2.0:oob", 23 | scopes: [AccessScope], 24 | website: String? = nil) -> Request { 25 | let parameters = [ 26 | Parameter(name: "client_name", value: clientName), 27 | Parameter(name: "redirect_uris", value: redirectURI), 28 | Parameter(name: "website", value: website), 29 | Parameter(name: "scopes", value: scopes.map(toString).joined(separator: " ")) 30 | ] 31 | 32 | let method = HTTPMethod.post(.parameters(parameters)) 33 | return Request(path: "/api/v1/apps", method: method) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/FollowRequests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowRequests.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/17/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `FollowRequests` requests. 12 | public enum FollowRequests { 13 | /// Fetches a list of follow requests. 14 | /// 15 | /// - Parameter range: The bounds used when requesting data from Mastodon. 16 | /// - Returns: Request for `[Account]`. 17 | public static func all(range: RequestRange = .default) -> Request<[Account]> { 18 | let parameters = range.parameters(limit: between(1, and: 80, default: 40)) 19 | let method = HTTPMethod.get(.parameters(parameters)) 20 | 21 | return Request<[Account]>(path: "/api/v1/follow_requests", method: method) 22 | } 23 | 24 | /// Authorizes a follow request. 25 | /// 26 | /// - Parameter id: The accound id. 27 | /// - Returns: Request for `Empty`. 28 | public static func authorize(id: String) -> Request { 29 | return Request(path: "/api/v1/follow_requests/\(id)/authorize", method: .post(.empty)) 30 | } 31 | 32 | /// Rejects a follow request. 33 | /// 34 | /// - Parameter id: The accound id. 35 | /// - Returns: Request for `Empty`. 36 | public static func reject(id: String) -> Request { 37 | return Request(path: "/api/v1/follow_requests/\(id)/reject", method: .post(.empty)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Mactodon/Relative Date/RelativeDateTextView.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | class RelativeDateTextView: NSTextView { 6 | var showSeconds = false 7 | var date: Date? { 8 | didSet { 9 | updateDate() 10 | updateRelativeDate() 11 | } 12 | } 13 | 14 | var shouldUpdate: Bool = false { 15 | didSet { 16 | if shouldUpdate { 17 | self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] (_) in 18 | self?.updateRelativeDate() 19 | }) 20 | } else { 21 | self.timer?.invalidate() 22 | self.timer = nil 23 | } 24 | } 25 | } 26 | 27 | var timer: Timer? 28 | 29 | static var dateFormatter: DateFormatter = { 30 | var dateFormatter = DateFormatter() 31 | dateFormatter.dateStyle = .short 32 | dateFormatter.timeStyle = .short 33 | return dateFormatter 34 | }() 35 | 36 | func updateDate() { 37 | guard let date = date else { 38 | toolTip = nil 39 | return 40 | } 41 | 42 | toolTip = RelativeDateTextView.dateFormatter.string(from: date) 43 | } 44 | 45 | func updateRelativeDate() { 46 | guard let date = date else { 47 | string = "" 48 | return 49 | } 50 | 51 | string = Date().timeIntervalSince(date).relativeString(useSeconds: showSeconds) 52 | setAlignment(.right, range: NSRange(location: 0, length: string.count)) 53 | } 54 | 55 | deinit { 56 | shouldUpdate = false 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/RequestRange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestRange.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/3/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum RequestRange { 12 | /// Gets a list with IDs less than or equal this value. 13 | case max(id: String, limit: Int?) 14 | /// Gets a list with IDs greater than this value. 15 | case since(id: String, limit: Int?) 16 | /// Sets the maximum number of entities to get. 17 | case limit(Int) 18 | /// Applies the default values. 19 | case `default` 20 | } 21 | 22 | extension RequestRange { 23 | func parameters(limit limitFunction: (Int) -> Int) -> [Parameter]? { 24 | switch self { 25 | case .max(let id, let limit): 26 | return [ 27 | Parameter(name: "max_id", value: id), 28 | Parameter(name: "limit", value: limit.map(limitFunction).flatMap(toOptionalString)) 29 | ] 30 | case .since(let id, let limit): 31 | return [ 32 | Parameter(name: "since_id", value: id), 33 | Parameter(name: "limit", value: limit.map(limitFunction).flatMap(toOptionalString)) 34 | ] 35 | case .limit(let limit): 36 | return [Parameter(name: "limit", value: String(limitFunction(limit)))] 37 | default: 38 | return nil 39 | } 40 | } 41 | } 42 | 43 | // MARK: - Equatable 44 | 45 | extension RequestRange: Equatable {} 46 | -------------------------------------------------------------------------------- /Mactodon/Helpers/LayoutHelpers.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | struct CellLayout { 6 | static let margin = NSEdgeInsets(top: 10, left: 5, bottom: 15, right: 15) 7 | static let columnSpacing = CGFloat(10) 8 | static let textViewYOffset = CGFloat(3) 9 | 10 | static func textColumn(width: CGFloat) -> CGRect { 11 | let left = margin.left + AvatarView.size(.regular).width + columnSpacing 12 | let width = width - left - margin.right 13 | 14 | return CGRect(x: left, y: margin.top, width: width, height: 0) 15 | } 16 | 17 | @discardableResult static func layoutActorRow(hasActorRow: Bool, width: CGFloat, avatar: AvatarView, description: NSTextView) -> CGRect { 18 | if hasActorRow == false { 19 | avatar.isHidden = true 20 | description.isHidden = true 21 | return textColumn(width: width) 22 | } else { 23 | avatar.isHidden = false 24 | description.isHidden = false 25 | 26 | var textColumn = self.textColumn(width: width) 27 | let actorFrame = CGRect(origin: textColumn.origin, size: AvatarView.size(.small)) 28 | avatar.frame = actorFrame 29 | 30 | let descriptionLeft: CGFloat = actorFrame.maxX + textViewYOffset 31 | let descriptionFrame = CGRect(origin: CGPoint(x: descriptionLeft, y: margin.top), size: description.sizeFor(width: width - descriptionLeft - margin.right)) 32 | description.frame = descriptionFrame 33 | textColumn.origin.y = max(actorFrame.maxY, descriptionFrame.maxY) + 3 34 | return textColumn 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Mactodon/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Viewer 26 | CFBundleURLName 27 | $(PRODUCT_BUNDLE_IDENTIFIER) 28 | CFBundleURLSchemes 29 | 30 | $(PRODUCT_BUNDLE_IDENTIFIER) 31 | 32 | 33 | 34 | CFBundleVersion 35 | 1 36 | LSApplicationCategoryType 37 | 38 | LSMinimumSystemVersion 39 | $(MACOSX_DEPLOYMENT_TARGET) 40 | NSHumanReadableCopyright 41 | Copyright © 2018 Max von Webel. All rights reserved. 42 | NSMainStoryboardFile 43 | Main 44 | NSPrincipalClass 45 | NSApplication 46 | 47 | 48 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/28/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum HTTPMethod { 12 | case get(Payload) 13 | case post(Payload) 14 | case put(Payload) 15 | case patch(Payload) 16 | case delete(Payload) 17 | } 18 | 19 | extension HTTPMethod { 20 | var name: String { 21 | switch self { 22 | case .get: return "GET" 23 | case .post: return "POST" 24 | case .put: return "PUT" 25 | case .delete: return "DELETE" 26 | case .patch: return "PATCH" 27 | } 28 | } 29 | 30 | var queryItems: [URLQueryItem]? { 31 | switch self { 32 | case .get(let payload): return payload.items 33 | default: return nil 34 | } 35 | } 36 | 37 | var httpBody: Data? { 38 | switch self { 39 | case .post(let payload): return payload.data 40 | case .put(let payload): return payload.data 41 | case .patch(let payload): return payload.data 42 | case .delete(let payload): return payload.data 43 | default: return nil 44 | } 45 | } 46 | 47 | var contentType: String? { 48 | switch self { 49 | case .post(let payload): return payload.type 50 | case .put(let payload): return payload.type 51 | case .patch(let payload): return payload.type 52 | case .delete(let payload): return payload.type 53 | default: return nil 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/MediaAttachment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaAttachment.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum MediaAttachment { 12 | /// JPEG (Joint Photographic Experts Group) image 13 | case jpeg(Data?) 14 | /// GIF (Graphics Interchange Format) image 15 | case gif(Data?) 16 | /// PNG (Portable Network Graphics) image 17 | case png(Data?) 18 | /// Other media file 19 | case other(Data?, fileExtension: String, mimeType: String) 20 | } 21 | 22 | extension MediaAttachment { 23 | var data: Data? { 24 | switch self { 25 | case .jpeg(let data): return data 26 | case .gif(let data): return data 27 | case .png(let data): return data 28 | case .other(let data, _, _): return data 29 | } 30 | } 31 | 32 | var fileName: String { 33 | switch self { 34 | case .jpeg: return "file.jpg" 35 | case .gif: return "file.gif" 36 | case .png: return "file.png" 37 | case .other(_, let fileExtension, _): return "file.\(fileExtension)" 38 | } 39 | } 40 | 41 | var mimeType: String { 42 | switch self { 43 | case .jpeg: return "image/jpg" 44 | case .gif: return "image/gif" 45 | case .png: return "image/png" 46 | case .other(_, _, let mimeType): return mimeType 47 | } 48 | } 49 | 50 | var base64EncondedString: String? { 51 | return data.map { "data:" + mimeType + ";base64," + $0.base64EncodedString() } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MactodonTests/ValuePromiseTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import XCTest 4 | @testable import Mactodon 5 | 6 | class ValuePromiseTests: XCTestCase { 7 | func testDidSetValue() { 8 | let promise = ValuePromise(initialValue: 0) 9 | var fired = false 10 | promise.didChange.then { 11 | XCTAssert($0 == 42) 12 | fired = true 13 | } 14 | 15 | promise.value = 42 16 | 17 | XCTAssert(fired) 18 | } 19 | 20 | func testDidSetValuesMultiple() { 21 | let promise = ValuePromise(initialValue: "first") 22 | var fireCount = 0 23 | promise.didChange.then { 24 | fireCount += 1 25 | } 26 | 27 | promise.value = "second" 28 | promise.value = "third" 29 | 30 | XCTAssertEqual(2, fireCount) 31 | } 32 | 33 | func testWillSetValue() { 34 | let promise = ValuePromise(initialValue: 0) 35 | var fireCount = 0 36 | promise.willChange.then { 37 | fireCount += 1 38 | } 39 | 40 | promise.value = 5 41 | promise.value = 42 42 | 43 | XCTAssertEqual(fireCount, 2) 44 | } 45 | 46 | func testDontFireIfValueIsInitial() { 47 | let promise = ValuePromise(initialValue: 42) 48 | var wasCalled = false 49 | promise.didChange.then { 50 | wasCalled = true 51 | } 52 | XCTAssertFalse(wasCalled) 53 | 54 | promise.value = 23 55 | XCTAssertTrue(wasCalled) 56 | } 57 | 58 | func testDoFireIfValueIsntInitial() { 59 | let promise = ValuePromise(initialValue: 0) 60 | promise.value = 42 61 | var wasCalled = false 62 | promise.didChange.then { 63 | wasCalled = true 64 | } 65 | XCTAssertTrue(wasCalled) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/PaginationItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaginationItem.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/31/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum PaginationItemType: String { 12 | case next, prev 13 | } 14 | 15 | struct PaginationItem { 16 | let type: PaginationItemType 17 | let id: String 18 | let limit: Int? 19 | } 20 | 21 | extension PaginationItem { 22 | init?(webLink: String) { 23 | let segments = webLink 24 | .condensed() 25 | .components(separatedBy: ";") 26 | 27 | let url = segments.first.map(trim(left: "<", right: ">")) 28 | let rel = segments.last? 29 | .replacingOccurrences(of: "\"", with: "") 30 | .trimmingCharacters(in: .whitespaces) 31 | .components(separatedBy: "=") 32 | 33 | guard 34 | let validURL = url, 35 | let referenceKey = rel?.first, referenceKey == "rel", 36 | let referenceValue = rel?.last, 37 | let type = PaginationItemType(rawValue: referenceValue), 38 | let queryItems = URLComponents(string: validURL)?.queryItems 39 | else { 40 | return nil 41 | } 42 | 43 | let sinceID = queryItems.first { $0.name == "since_id" }?.value 44 | let maxID = queryItems.first { $0.name == "max_id" }?.value 45 | 46 | guard let id = maxID ?? sinceID else { return nil } 47 | 48 | self.type = type 49 | self.id = id 50 | self.limit = queryItems 51 | .first { $0.name == "limit" } 52 | .flatMap(toInteger) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/DomainBlocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainBlocks.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 6/5/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `DomainBlocks` requests. 12 | public enum DomainBlocks { 13 | /// Fetches a user's blocked domains. 14 | /// 15 | /// - Parameter range: The bounds used when requesting data from Mastodon. 16 | /// - Returns: Request for `[String]`. 17 | public static func all(range: RequestRange = .default) -> Request<[String]> { 18 | let parameters = range.parameters(limit: between(1, and: 80, default: 40)) 19 | let method = HTTPMethod.get(.parameters(parameters)) 20 | 21 | return Request<[String]>(path: "/api/v1/domain_blocks", method: method) 22 | } 23 | 24 | /// Blocks a domain. 25 | /// 26 | /// - Parameter domain: The domain to block. 27 | /// - Returns: Request for `Empty`. 28 | public static func block(domain: String) -> Request { 29 | let parameter = [Parameter(name: "domain", value: domain)] 30 | let method = HTTPMethod.post(.parameters(parameter)) 31 | 32 | return Request(path: "/api/v1/domain_blocks", method: method) 33 | } 34 | 35 | /// Unblocks a domain. 36 | /// 37 | /// - Parameter domain: The domain to unblock. 38 | /// - Returns: Request for `Empty`. 39 | public static func unblock(domain: String) -> Request { 40 | let parameter = [Parameter(name: "domain", value: domain)] 41 | let method = HTTPMethod.delete(.parameters(parameter)) 42 | 43 | return Request(path: "/api/v1/domain_blocks", method: method) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Pods/Atributika/Sources/NSScanner+Swift.swift: -------------------------------------------------------------------------------- 1 | // NSScanner+Swift.swift 2 | // A set of Swift-idiomatic methods for NSScanner 3 | // 4 | // (c) 2015 Nate Cook, licensed under the MIT license 5 | 6 | import Foundation 7 | 8 | extension Scanner { 9 | 10 | // MARK: Strings 11 | 12 | /// Returns a string, scanned as long as characters from a given character set are encountered, or `nil` if none are found. 13 | func scanCharacters(from set: CharacterSet) -> String? { 14 | var value: NSString? = "" 15 | if scanCharacters(from: set, into: &value) { 16 | return value as String? 17 | } 18 | return nil 19 | } 20 | 21 | /// Returns a string, scanned until a character from a given character set are encountered, or the remainder of the scanner's string. Returns `nil` if the scanner is already `atEnd`. 22 | func scanUpToCharacters(from set: CharacterSet) -> String? { 23 | var value: NSString? = "" 24 | if scanUpToCharacters(from: set, into: &value) { 25 | return value as String? 26 | } 27 | return nil 28 | } 29 | 30 | /// Returns the given string if scanned, or `nil` if not found. 31 | @discardableResult func scanString(_ str: String) -> String? { 32 | var value: NSString? = "" 33 | if scanString(str, into: &value) { 34 | return value as String? 35 | } 36 | return nil 37 | } 38 | 39 | /// Returns a string, scanned until the given string is found, or the remainder of the scanner's string. Returns `nil` if the scanner is already `atEnd`. 40 | func scanUpTo(_ str: String) -> String? { 41 | var value: NSString? = "" 42 | if scanUpTo(str, into: &value) { 43 | return value as String? 44 | } 45 | return nil 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Functions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Functions.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/13/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Map 12 | 13 | func toString(scope: AccessScope) -> String { 14 | return scope.rawValue 15 | } 16 | 17 | func toArrayOfParameters(withName name: String) -> (A) -> Parameter { 18 | return { value in Parameter(name: "\(name)[]", value: String(describing: value)) } 19 | } 20 | 21 | func between(_ min: Int, and max: Int, default: Int) -> (Int) -> Int { 22 | return { limit in (limit >= min && limit <= max) ? limit : `default` } 23 | } 24 | 25 | // MARK: - Flat-map 26 | 27 | func toOptionalString(optional: A?) -> String? { 28 | return optional.map(String.init(describing:)) 29 | } 30 | 31 | func toQueryItem(parameter: Parameter) -> URLQueryItem? { 32 | guard let value = parameter.value else { return nil } 33 | return URLQueryItem(name: parameter.name, value: value) 34 | } 35 | 36 | func toString(parameter: Parameter) -> String? { 37 | return parameter.value? 38 | .addingPercentEncoding(withAllowedCharacters: .bodyAllowed) 39 | .map { value in "\(parameter.name)=\(value)" } 40 | } 41 | 42 | func trueOrNil(_ flag: Bool) -> String? { 43 | return flag ? "true" : nil 44 | } 45 | 46 | func trim(left: Character, right: Character) -> (String) -> String { 47 | return { string in 48 | guard string.hasPrefix("\(left)"), string.hasSuffix("\(right)") else { return string } 49 | return String(string[string.index(after: string.startIndex).. Int? { 54 | guard let value = item.value else { return nil } 55 | return Int(value) 56 | } 57 | 58 | func toAccessScope(string: String) -> AccessScope? { 59 | return AccessScope(rawValue: string) 60 | } 61 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Notifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notifications.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 5/17/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Notifications` requests. 12 | public enum Notifications { 13 | /// Fetches a user's notifications. 14 | /// 15 | /// - Parameter range: The bounds used when requesting data from Mastodon. 16 | /// - Returns: Request for `[Notification]`. 17 | public static func all(range: RequestRange = .default) -> Request<[Notification]> { 18 | let parameters = range.parameters(limit: between(1, and: 15, default: 30)) 19 | let method = HTTPMethod.get(.parameters(parameters)) 20 | 21 | return Request<[Notification]>(path: "/api/v1/notifications", method: method) 22 | } 23 | 24 | /// Gets a single notification. 25 | /// 26 | /// - Parameter id: The notification id. 27 | /// - Returns: Request for `Notification`. 28 | public static func notification(id: String) -> Request { 29 | return Request(path: "/api/v1/notifications/\(id)") 30 | } 31 | 32 | /// Deletes all notifications for the authenticated user. 33 | /// 34 | /// - Returns: Request for `Empty`. 35 | public static func dismissAll() -> Request { 36 | return Request(path: "/api/v1/notifications/clear", method: .post(.empty)) 37 | } 38 | 39 | /// Deletes a single notification for the authenticated user. 40 | /// 41 | /// - Parameter id: The notification id. 42 | /// - Returns: Request for `Empty`. 43 | public static func dismiss(id: String) -> Request { 44 | let parameter = [Parameter(name: "id", value: String(id))] 45 | let method = HTTPMethod.post(.parameters(parameter)) 46 | 47 | return Request(path: "/api/v1/notifications/dismiss", method: method) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Mactodon/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | import MastodonKit 5 | 6 | extension NSStoryboard { 7 | func instantiateLoginViewController() -> LoginViewController { 8 | return instantiateController(withIdentifier: "LoginSheet") as! LoginViewController 9 | } 10 | } 11 | 12 | protocol LoginViewControllerDelegate: NSObjectProtocol { 13 | func registered(baseURL: URL) 14 | } 15 | 16 | class LoginViewController: NSViewController { 17 | @IBOutlet weak var instanceNameField: NSTextField! 18 | @IBOutlet weak var connectButton: NSButton! 19 | @IBOutlet weak var errorLabel: NSTextField! 20 | 21 | weak var delegate: LoginViewControllerDelegate? 22 | 23 | var client: Client? 24 | var url: URL? { 25 | get { 26 | return URL(string: "https://\(instanceNameField.stringValue)/") 27 | } 28 | } 29 | 30 | let defaults = UserDefaults.standard 31 | static let instanceKey = "DefaultInstance" 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | errorLabel.isHidden = true 37 | 38 | instanceNameField.delegate = self 39 | instanceNameField.stringValue = defaults.string(forKey: LoginViewController.instanceKey) ?? "" 40 | connectButton.isEnabled = url != nil 41 | } 42 | 43 | override func viewDidAppear() { 44 | assert(delegate != nil) 45 | } 46 | 47 | @IBAction func cancel(_ sender: Any) { 48 | dismiss(nil) 49 | } 50 | 51 | @IBAction func connect(_ sender: Any) { 52 | guard let baseURL = self.url else { 53 | return 54 | } 55 | 56 | self.errorLabel.isHidden = true 57 | 58 | defaults.set(baseURL.host, forKey: LoginViewController.instanceKey) 59 | defaults.synchronize() 60 | 61 | delegate!.registered(baseURL: baseURL) 62 | dismiss(nil) 63 | } 64 | } 65 | 66 | extension LoginViewController: NSTextFieldDelegate { 67 | func controlTextDidChange(_ obj: Cocoa.Notification) { 68 | guard instanceNameField as AnyObject === obj.object as AnyObject else { 69 | return 70 | } 71 | 72 | connectButton.isEnabled = url != nil && instanceNameField.stringValue.count > 2 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/22/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Client: ClientType { 12 | let baseURL: String 13 | let session: URLSession 14 | public var accessToken: String? 15 | 16 | public init(baseURL: String, accessToken: String? = nil, session: URLSession = .shared) { 17 | self.baseURL = baseURL 18 | self.session = session 19 | self.accessToken = accessToken 20 | } 21 | 22 | public func run(_ request: Request, completion: @escaping (Result) -> Void) { 23 | guard 24 | let components = URLComponents(baseURL: baseURL, request: request), 25 | let url = components.url 26 | else { 27 | completion(.failure(ClientError.malformedURL)) 28 | return 29 | } 30 | 31 | let urlRequest = URLRequest(url: url, request: request, accessToken: accessToken) 32 | let task = session.dataTask(with: urlRequest) { data, response, error in 33 | if let error = error { 34 | completion(.failure(error)) 35 | return 36 | } 37 | 38 | guard let data = data else { 39 | completion(.failure(ClientError.malformedJSON)) 40 | return 41 | } 42 | 43 | guard 44 | let httpResponse = response as? HTTPURLResponse, 45 | httpResponse.statusCode == 200 46 | else { 47 | let mastodonError = try? MastodonError.decode(data: data) 48 | let error: ClientError = mastodonError.map { .mastodonError($0.description) } ?? .genericError 49 | completion(.failure(error)) 50 | return 51 | } 52 | 53 | guard let model = try? Model.decode(data: data) else { 54 | completion(.failure(ClientError.invalidModel)) 55 | return 56 | } 57 | 58 | completion(.success(model, httpResponse.pagination)) 59 | } 60 | 61 | task.resume() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Mactodon/NSTextView+HTML.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Atributika 4 | import Cocoa 5 | 6 | extension NSAttributedString { 7 | func sizeFor(width: CGFloat) -> CGSize { 8 | let textStorage = NSTextStorage(attributedString: self) 9 | let textContainer = NSTextContainer(containerSize: CGSize(width: width, height: 0)) 10 | let layoutManager = NSLayoutManager() 11 | layoutManager.addTextContainer(textContainer) 12 | textStorage.addLayoutManager(layoutManager) 13 | layoutManager.glyphRange(for: textContainer) 14 | return layoutManager.usedRect(for: textContainer).size 15 | } 16 | } 17 | 18 | extension NSTextView { 19 | var attributedString: NSAttributedString { 20 | get { 21 | return attributedString() 22 | } 23 | set { 24 | let textStorage = self.textStorage! 25 | let range = NSRange(location: 0, length: textStorage.length) 26 | textStorage.replaceCharacters(in: range, with: newValue) 27 | } 28 | } 29 | 30 | func sizeFor(width: CGFloat) -> CGSize { 31 | return attributedString.sizeFor(width: width) 32 | } 33 | 34 | func set(html: String) { 35 | self.linkTextAttributes = [ 36 | NSAttributedString.Key.cursor: NSCursor.pointingHand, 37 | ] 38 | 39 | let hrefLinkReplacement = "###" 40 | let fontSize = NSFont.systemFontSize(for: .regular) 41 | let allStyle = Style() 42 | .font(NSFont.systemFont(ofSize: fontSize)) 43 | .foregroundColor(NSColor.labelColor) 44 | let aStyle = Style("a") 45 | .foregroundColor(NSColor.controlAccentColor, .normal) 46 | .link(hrefLinkReplacement) 47 | let displayName = Style("displayName").font(NSFont.boldSystemFont(ofSize: fontSize)).foregroundColor(NSColor.textColor) 48 | let at = Style("at").foregroundColor(NSColor.labelColor.withAlphaComponent(0.6)) 49 | attributedString = html 50 | .style(tags: [displayName, at, aStyle], tuner: { style, tag in 51 | switch tag.name.lowercased() { 52 | case "a": 53 | if style.typedAttributes[.normal]?[.link] as? String == hrefLinkReplacement, let href = tag.attributes["href"] { 54 | return style.link(href) 55 | } else { 56 | return style 57 | } 58 | default: 59 | return style 60 | } 61 | }) 62 | .styleAll(allStyle) 63 | .attributedString 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Timelines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timelines.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Timelines` requests. 12 | public enum Timelines { 13 | /// Retrieves the home timeline. 14 | /// 15 | /// - Parameter range: The bounds used when requesting data from Mastodon. 16 | /// - Returns: Request for `[Status]`. 17 | public static func home(range: RequestRange = .default) -> Request<[Status]> { 18 | let parameters = range.parameters(limit: between(1, and: 40, default: 20)) 19 | let method = HTTPMethod.get(.parameters(parameters)) 20 | 21 | return Request<[Status]>(path: "/api/v1/timelines/home", method: method) 22 | } 23 | 24 | /// Retrieves the public timeline. 25 | /// 26 | /// - Parameters: 27 | /// - local: Only return statuses originating from this instance. 28 | /// - range: The bounds used when requesting data from Mastodon. 29 | /// - Returns: Request for `[Status]`. 30 | public static func `public`(local: Bool? = nil, range: RequestRange = .default) -> Request<[Status]> { 31 | let rangeParameters = range.parameters(limit: between(1, and: 40, default: 20)) ?? [] 32 | let localParameter = [Parameter(name: "local", value: local.flatMap(trueOrNil))] 33 | let method = HTTPMethod.get(.parameters(localParameter + rangeParameters)) 34 | 35 | return Request<[Status]>(path: "/api/v1/timelines/public", method: method) 36 | } 37 | 38 | /// Retrieves a tag timeline. 39 | /// 40 | /// - Parameters: 41 | /// - hashtag: The hashtag. 42 | /// - local: Only return statuses originating from this instance. 43 | /// - range: The bounds used when requesting data from Mastodon. 44 | /// - Returns: Request for `[Status]`. 45 | public static func tag(_ hashtag: String, local: Bool? = nil, range: RequestRange = .default) -> Request<[Status]> { 46 | let rangeParameters = range.parameters(limit: between(1, and: 40, default: 20)) ?? [] 47 | let localParameter = [Parameter(name: "local", value: local.flatMap(trueOrNil))] 48 | let method = HTTPMethod.get(.parameters(localParameter + rangeParameters)) 49 | 50 | return Request<[Status]>(path: "/api/v1/timelines/tag/\(hashtag)", method: method) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Account.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Account.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Account: Codable { 12 | /// The ID of the account. 13 | public let id: String 14 | /// The username of the account. 15 | public let username: String 16 | /// Equals username for local users, includes @domain for remote ones. 17 | public let acct: String 18 | /// The account's display name. 19 | public let displayName: String 20 | /// Biography of user. 21 | public let note: String 22 | /// URL of the user's profile page (can be remote). 23 | public let url: String 24 | /// URL to the avatar image. 25 | public let avatar: String 26 | /// URL to the avatar static image 27 | public let avatarStatic: String 28 | /// URL to the header image. 29 | public let header: String 30 | /// URL to the header static image 31 | public let headerStatic: String 32 | /// Boolean for when the account cannot be followed without waiting for approval first. 33 | public let locked: Bool 34 | /// The time the account was created. 35 | public let createdAt: Date 36 | /// The number of followers for the account. 37 | public let followersCount: Int 38 | /// The number of accounts the given account is following. 39 | public let followingCount: Int 40 | /// The number of statuses the account has made. 41 | public let statusesCount: Int 42 | 43 | /// An array of `Emoji`. 44 | public var emojis: [Emoji] { 45 | return _emojis ?? [] 46 | } 47 | 48 | /// Real storage of emojis. 49 | /// 50 | /// According to the [documentation](https://docs.joinmastodon.org/api/entities/#account), 51 | /// property emoji is added in 2.4.0, and it is non-optional. But for compibility with older version instance, we 52 | /// use `[Emoji]?` as storage and use `[Emoji]` as public API. 53 | private let _emojis: [Emoji]? 54 | 55 | private enum CodingKeys: String, CodingKey { 56 | case id 57 | case username 58 | case acct 59 | case displayName = "display_name" 60 | case note 61 | case url 62 | case avatar 63 | case avatarStatic = "avatar_static" 64 | case header 65 | case headerStatic = "header_static" 66 | case locked 67 | case createdAt = "created_at" 68 | case followersCount = "followers_count" 69 | case followingCount = "following_count" 70 | case statusesCount = "statuses_count" 71 | case _emojis = "emojis" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Login.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Login.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/18/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Login` requests. 12 | public enum Login { 13 | /// Performs a silent login. 14 | /// 15 | /// - Parameters: 16 | /// - clientID: The client ID. 17 | /// - clientSecret: The client secret. 18 | /// - scopes: The access scopes. 19 | /// - username: The user's username or e-mail address. 20 | /// - password: The user's password. 21 | /// - Returns: Request for `LoginSettings`. 22 | public static func silent(clientID: String, 23 | clientSecret: String, 24 | scopes: [AccessScope], 25 | username: String, 26 | password: String) -> Request { 27 | let parameters = [ 28 | Parameter(name: "client_id", value: clientID), 29 | Parameter(name: "client_secret", value: clientSecret), 30 | Parameter(name: "scope", value: scopes.map(toString).joined(separator: " ")), 31 | Parameter(name: "grant_type", value: "password"), 32 | Parameter(name: "username", value: username), 33 | Parameter(name: "password", value: password) 34 | ] 35 | 36 | let method = HTTPMethod.post(.parameters(parameters)) 37 | return Request(path: "/oauth/token", method: method) 38 | } 39 | 40 | /// Completes an OAuth login. 41 | /// 42 | /// - Parameters: 43 | /// - clientID: The client ID. 44 | /// - clientSecret: The client secret. 45 | /// - scopes: The access scopes. 46 | /// - redirectURI: The client redirectURI. 47 | /// - code: The authorization code. 48 | /// - Returns: Request for `LoginSettings`. 49 | public static func oauth(clientID: String, 50 | clientSecret: String, 51 | scopes: [AccessScope], 52 | redirectURI: String, 53 | code: String) -> Request { 54 | let parameters = [ 55 | Parameter(name: "client_id", value: clientID), 56 | Parameter(name: "client_secret", value: clientSecret), 57 | Parameter(name: "scope", value: scopes.map(toString).joined(separator: " ")), 58 | Parameter(name: "grant_type", value: "authorization_code"), 59 | Parameter(name: "redirect_uri", value: redirectURI), 60 | Parameter(name: "code", value: code) 61 | ] 62 | 63 | let method = HTTPMethod.post(.parameters(parameters)) 64 | return Request(path: "/oauth/token", method: method) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Mactodon/FeedRendering/PullToRefreshCell.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | 5 | protocol PullToRefreshCellDelegate: AnyObject { 6 | func startRefresh() 7 | } 8 | 9 | class PullToRefreshCell: NSCollectionViewItem, FeedViewCell { 10 | static let identifier = NSUserInterfaceItemIdentifier("PullToRefreshCell") 11 | 12 | weak var delegate: PullToRefreshCellDelegate? 13 | private var progressIndicator: NSProgressIndicator! 14 | 15 | var refreshing = false { 16 | didSet { 17 | progressIndicator.isIndeterminate = refreshing 18 | if refreshing { 19 | progressIndicator.startAnimation(nil) 20 | delegate?.startRefresh() 21 | } 22 | } 23 | } 24 | 25 | private let pullDistance = CGFloat(50) 26 | 27 | func willDisplay() { 28 | progressIndicator.startAnimation(nil) 29 | NotificationCenter.default.addObserver(self, selector: #selector(boundsDidChange(_:)), name: NSView.boundsDidChangeNotification, object: collectionView!.superview) 30 | } 31 | 32 | func didEndDisplaying() { 33 | progressIndicator.stopAnimation(nil) 34 | NotificationCenter.default.removeObserver(self) 35 | } 36 | 37 | deinit { 38 | NotificationCenter.default.removeObserver(self) 39 | } 40 | 41 | static func size(width: CGFloat, isReloading: Bool) -> CGSize { 42 | let height: CGFloat = isReloading ? 40 : 0 43 | return CGSize(width: width, height: height) 44 | } 45 | 46 | @objc func boundsDidChange(_ notification: Notification) { 47 | if refreshing { 48 | return 49 | } 50 | 51 | let scrollView = (notification.object as! NSView).superview as! NSScrollView 52 | 53 | let y = 0.0 - scrollView.documentVisibleRect.minY 54 | if y < 0 { 55 | return 56 | } 57 | 58 | refreshing = y > pullDistance 59 | 60 | progressIndicator.doubleValue = Double(y) 61 | } 62 | 63 | override func loadView() { 64 | view = NSView() 65 | view.wantsLayer = true 66 | view.layer!.masksToBounds = false 67 | } 68 | 69 | override func viewDidLayout() { 70 | super.viewDidLayout() 71 | self.layout(width: self.view.bounds.width) 72 | } 73 | 74 | override func viewDidLoad() { 75 | super.viewDidLoad() 76 | 77 | progressIndicator = NSProgressIndicator(frame: .zero) 78 | progressIndicator.isIndeterminate = false 79 | progressIndicator.isDisplayedWhenStopped = true 80 | progressIndicator.style = .bar 81 | progressIndicator.minValue = 15 82 | progressIndicator.maxValue = Double(pullDistance) 83 | view.addSubview(progressIndicator) 84 | } 85 | 86 | func layout(width: CGFloat) { 87 | let size = CGSize(width: round(width / 3.0), height: 12) 88 | let origin = CGPoint(x: round(width / 3.0), y: 10) 89 | progressIndicator.frame = CGRect(origin: origin, size: size) 90 | progressIndicator.sizeToFit() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Mactodon/Keychain.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | 5 | struct Keychain { 6 | enum KeychainError: Error { 7 | case error(status: OSStatus) 8 | } 9 | 10 | static func add(service: String, account: String? = nil, key: Data) throws { 11 | var query = Keychain.query(withService: service, account: account, accessGroup: nil) 12 | query[kSecValueData as String] = key 13 | let status = SecItemAdd(query as CFDictionary, nil) 14 | guard status == errSecSuccess else { 15 | throw KeychainError.error(status: status) 16 | } 17 | } 18 | 19 | static func get(service: String, account: String? = nil) throws -> Data? { 20 | var query = Keychain.query(withService: service, account: account, accessGroup: nil) 21 | query[kSecReturnData as String] = true 22 | var item: CFTypeRef? 23 | let status = SecItemCopyMatching(query as CFDictionary, &item) 24 | guard status == errSecSuccess || status == errSecItemNotFound else { 25 | throw KeychainError.error(status: status) 26 | } 27 | return item as? Data 28 | } 29 | 30 | static func delete(service: String, account: String? = nil) throws { 31 | let query = Keychain.query(withService: service, account: account, accessGroup: nil) 32 | let status = SecItemDelete(query as CFDictionary) 33 | guard status == errSecSuccess || status == errSecItemNotFound else { 34 | throw KeychainError.error(status: status) 35 | } 36 | } 37 | 38 | static func set(service: String, account: String? = nil, key: Data) throws { 39 | try delete(service: service, account: account) 40 | try add(service: service, account: account, key: key) 41 | } 42 | 43 | private static func query(withService service: String, account: String? = nil, accessGroup: String? = nil) -> [String : Any] { 44 | var query = [String : AnyObject]() 45 | query[kSecClass as String] = kSecClassGenericPassword 46 | query[kSecAttrService as String] = service as AnyObject? 47 | 48 | if let account = account { 49 | query[kSecAttrAccount as String] = account as AnyObject? 50 | } 51 | 52 | if let accessGroup = accessGroup { 53 | query[kSecAttrAccessGroup as String] = accessGroup as AnyObject? 54 | } 55 | 56 | return query 57 | } 58 | } 59 | 60 | extension Keychain { 61 | static func add(service: String, account: String? = nil, value: T) throws where T: Encodable { 62 | let data = try JSONEncoder().encode(value) 63 | try add(service: service, account: account, key: data) 64 | } 65 | 66 | static func set(service: String, account: String? = nil, value: T) throws where T: Encodable { 67 | try delete(service: service, account: account) 68 | try add(service: service, account: account, value: value) 69 | } 70 | 71 | static func get(service: String, account: String? = nil, type: T.Type) throws -> T? where T: Decodable { 72 | guard let data = try get(service: service, account: account) else { 73 | return nil 74 | } 75 | return try JSONDecoder().decode(type, from: data) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Models/Status.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Status.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Status: Codable { 12 | /// The ID of the status. 13 | public let id: String 14 | /// A Fediverse-unique resource ID. 15 | public let uri: String 16 | /// URL to the status page (can be remote). 17 | public let url: URL? 18 | /// The Account which posted the status. 19 | public let account: Account 20 | /// null or the ID of the status it replies to. 21 | public let inReplyToID: String? 22 | /// null or the ID of the account it replies to. 23 | public let inReplyToAccountID: String? 24 | /// Body of the status; this will contain HTML (remote HTML already sanitized). 25 | public let content: String 26 | /// The time the status was created. 27 | public let createdAt: Date 28 | /// An array of Emoji. 29 | public let emojis: [Emoji] 30 | /// The number of reblogs for the status. 31 | public let reblogsCount: Int 32 | /// The number of favourites for the status. 33 | public let favouritesCount: Int 34 | /// Whether the authenticated user has reblogged the status. 35 | public let reblogged: Bool? 36 | /// Whether the authenticated user has favourited the status. 37 | public let favourited: Bool? 38 | /// Whether media attachments should be hidden by default. 39 | public let sensitive: Bool? 40 | /// If not empty, warning text that should be displayed before the actual content. 41 | public let spoilerText: String 42 | /// The visibility of the status. 43 | public let visibility: Visibility 44 | /// An array of attachments. 45 | public let mediaAttachments: [Attachment] 46 | /// An array of mentions. 47 | public let mentions: [Mention] 48 | /// An array of tags. 49 | public let tags: [Tag] 50 | /// Application from which the status was posted. 51 | public let application: Application? 52 | /// The detected language for the status. 53 | public let language: String? 54 | /// The reblogged Status 55 | public let reblog: Status? 56 | /// Whether this is the pinned status for the account that posted it. 57 | public let pinned: Bool? 58 | 59 | private enum CodingKeys: String, CodingKey { 60 | case id 61 | case uri 62 | case url 63 | case account 64 | case inReplyToID = "in_reply_to_id" 65 | case inReplyToAccountID = "in_reply_to_account_id" 66 | case content 67 | case createdAt = "created_at" 68 | case emojis 69 | case reblogsCount = "reblogs_count" 70 | case favouritesCount = "favourites_count" 71 | case reblogged 72 | case favourited 73 | case sensitive 74 | case spoilerText = "spoiler_text" 75 | case visibility 76 | case mediaAttachments = "media_attachments" 77 | case mentions 78 | case tags 79 | case application 80 | case language 81 | case reblog 82 | case pinned 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Lists.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lists.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 1/2/18. 6 | // Copyright © 2018 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Lists` requests. 12 | public enum Lists { 13 | /// Retrieves lists. 14 | /// 15 | /// - Returns: Request for `[List]`. 16 | public static func all() -> Request<[List]> { 17 | return Request<[List]>(path: "/api/v1/lists") 18 | } 19 | 20 | /// Retrieves accounts in a list. 21 | /// 22 | /// - Parameter id: The list ID. 23 | /// - Returns: Request for `[Account]`. 24 | public static func accounts(id: String) -> Request<[Account]> { 25 | return Request<[Account]>(path: "/api/v1/lists/\(id)/accounts") 26 | } 27 | 28 | /// Retrieves a list. 29 | /// 30 | /// - Parameter id: The list ID. 31 | /// - Returns: Request for `List`. 32 | public static func list(id: String) -> Request { 33 | return Request(path: "/api/v1/lists/\(id)") 34 | } 35 | 36 | /// Creates a list. 37 | /// 38 | /// - Parameter title: The title of the list. 39 | /// - Returns: Request for `List`. 40 | public static func create(title: String) -> Request { 41 | let parameter = [Parameter(name: "title", value: title)] 42 | let method = HTTPMethod.post(.parameters(parameter)) 43 | 44 | return Request(path: "/api/v1/lists", method: method) 45 | } 46 | 47 | /// Updates the list title. 48 | /// 49 | /// - Parameters: 50 | /// - id: The list ID. 51 | /// - title: The title of the list. 52 | /// - Returns: Request for `List`. 53 | public static func update(id: String, title: String) -> Request { 54 | let parameter = [Parameter(name: "title", value: title)] 55 | let method = HTTPMethod.put(.parameters(parameter)) 56 | 57 | return Request(path: "/api/v1/lists/\(id)", method: method) 58 | } 59 | 60 | /// Deletes a list. 61 | /// 62 | /// - Parameter id: The list ID. 63 | /// - Returns: Request for `Empty`. 64 | public static func delete(id: String) -> Request { 65 | return Request(path: "/api/v1/lists/\(id)", method: .delete(.empty)) 66 | } 67 | 68 | /// Adds accounts to a list. 69 | /// 70 | /// - Parameters: 71 | /// - accountIDs: The account IDs to be added to the list. 72 | /// - id: The list ID> 73 | /// - Returns: Request for `Empty`. 74 | public static func add(accountIDs: [String], toList id: String) -> Request { 75 | let parameter = accountIDs.map(toArrayOfParameters(withName: "account_ids")) 76 | let method = HTTPMethod.post(.parameters(parameter)) 77 | 78 | return Request(path: "/api/v1/lists/\(id)/accounts", method: method) 79 | } 80 | 81 | /// Removes accounts from a list. 82 | /// 83 | /// - Parameters: 84 | /// - accountIDs: The account IDs to be removed from the list. 85 | /// - id: The list ID> 86 | /// - Returns: Request for `Empty`. 87 | public static func remove(accountIDs: [String], fromList id: String) -> Request { 88 | let parameter = accountIDs.map(toArrayOfParameters(withName: "account_ids")) 89 | let method = HTTPMethod.delete(.parameters(parameter)) 90 | 91 | return Request(path: "/api/v1/lists/\(id)/accounts", method: method) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Readme.md: -------------------------------------------------------------------------------- 1 | ![MastodonKit](https://cloud.githubusercontent.com/assets/19753339/26019845/f64df19a-3778-11e7-8482-e09e187f3923.png) 2 | 3 | [![Build Status](https://travis-ci.org/MastodonKit/MastodonKit.svg?branch=master)](https://travis-ci.org/MastodonKit/MastodonKit) 4 | [![Code Coverage](http://codecov.io/github/MastodonKit/MastodonKit/branch/master/graphs/badge.svg)](http://codecov.io/github/MastodonKit/MastodonKit) 5 | [![SwiftPM Compatible](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 6 | [![SwiftPM Version](https://img.shields.io/github/release/MastodonKit/MastodonKit.svg?colorB=brightgreen)](https://github.com/MastodonKit/MastodonKit/releases) 7 | [![CocoaPods](https://img.shields.io/cocoapods/v/MastodonKit.svg?colorB=brightgreen)](https://github.com/MastodonKit/MastodonKit) 8 | 9 | **MastodonKit** is a Swift Framework built using Swift Package Manager that wraps the Mastodon API. It covers all the endpoints and entities from [Mastodon's API](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md) and is designed to be simple to use. 10 | 11 | Visit [MastodonKit's Complete Documentation](https://mastodonkit.github.io/MastodonKit) for examples and descriptions of all the methods available. These are some of the examples of what you'll find there: 12 | 13 | * [How to Install](https://mastodonkit.github.io/MastodonKit/docs/how-to-install.html) 14 | * [How to Build and Test](https://mastodonkit.github.io/MastodonKit/docs/how-to-build-and-test.html) 15 | * [Initializing the Client](https://mastodonkit.github.io/MastodonKit/docs/initializing-the-client.html) 16 | * [Making Requests](https://mastodonkit.github.io/MastodonKit/docs/making-requests.html) 17 | * [Ranges and Limits](https://mastodonkit.github.io/MastodonKit/docs/ranges-and-limits.html) 18 | * [Uploading Media Attachments](https://mastodonkit.github.io/MastodonKit/docs/uploading-media-attachments.html) 19 | 20 | By the way, if you want to get in touch with me, [toot me](https://mastodon.technology/@ornithocoder). 21 | 22 | # Contributors 23 | 24 | * [Ornithologist Coder (@ornithocoder)](https://mastodon.technology/@ornithocoder) 25 | * [Paul Schifferer (@exsortis)](https://github.com/exsortis) 26 | * [Valerii Hiora (@vhbit)](https://github.com/vhbit) 27 | * [Calv Collins (@calv@mastodon.social)](https://github.com/calvcoll) 28 | * [Frank Rausch](https://github.com/frankrausch) 29 | * [Adam Lickel](https://github.com/lickel) 30 | * [Bei Li (@libei@mastodon.social)](https://github.com/kylinroc) 31 | * [Tony Arnold](https://github.com/tonyarnold) 32 | 33 | # License 34 | 35 | Copyright (c) 2017 Ornithologist Coder. All rights reserved. 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 42 | -------------------------------------------------------------------------------- /Mactodon/NotificationFeed/FollowingItem.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | import MastodonKit 5 | import Nuke 6 | 7 | class FollowingItem: NSCollectionViewItem, FeedViewCell { 8 | enum FollowingState { 9 | case Unknown 10 | case NotFollowing 11 | case FollowRequested 12 | case Following 13 | case UnfollowRequested 14 | } 15 | 16 | struct Model { 17 | let account: Account 18 | let followingState: FollowingState 19 | let follow: (_ account: Account) -> () 20 | let unfollow: (_ account: Account) -> () 21 | } 22 | 23 | static let identifier = NSUserInterfaceItemIdentifier("FollowingItem") 24 | var model: Model? 25 | 26 | private var actorAvatar: AvatarView! 27 | private var descriptionView: NSTextView! 28 | private var actionButton: NSButton! 29 | 30 | func willDisplay() { 31 | guard let model = model else { 32 | return 33 | } 34 | 35 | Nuke.loadImage(with: URL(string: model.account.avatar)!, into: actorAvatar) 36 | actorAvatar.clickURL = URL(string: model.account.url) 37 | 38 | descriptionView.set(html: "\(model.account.someDisplayName) followed you") 39 | 40 | switch model.followingState { 41 | case .Unknown: 42 | actionButton.isHidden = true 43 | case .NotFollowing: 44 | actionButton.isHidden = false 45 | actionButton.title = "Follow" 46 | actionButton.isEnabled = true 47 | case .FollowRequested: 48 | actionButton.isHidden = false 49 | actionButton.title = "Follow" 50 | actionButton.isEnabled = false 51 | case .Following: 52 | actionButton.isHidden = false 53 | actionButton.title = "Unfollow" 54 | actionButton.isEnabled = true 55 | case .UnfollowRequested: 56 | actionButton.isHidden = false 57 | actionButton.title = "Unfollow" 58 | actionButton.isEnabled = false 59 | } 60 | } 61 | 62 | func didEndDisplaying() { 63 | Nuke.cancelRequest(for: actorAvatar) 64 | } 65 | 66 | override func prepareForReuse() { 67 | super.prepareForReuse() 68 | model = nil 69 | } 70 | 71 | override func loadView() { 72 | view = FlippedView() 73 | } 74 | 75 | override func viewDidLoad() { 76 | super.viewDidLoad() 77 | 78 | actorAvatar = AvatarView(frame: .zero) 79 | view.addSubview(actorAvatar) 80 | 81 | descriptionView = NSTextView(frame: .zero) 82 | descriptionView.prepareAsLabel() 83 | view.addSubview(descriptionView) 84 | 85 | actionButton = NSButton(title: "Follow", target: self, action: #selector(clickedFollowButton(_:))) 86 | actionButton.bezelStyle = .roundRect 87 | actionButton.font = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .small)) 88 | view.addSubview(actionButton) 89 | } 90 | 91 | @IBAction func clickedFollowButton(_ sender: NSButton) { 92 | guard let model = model else { 93 | assert(false) 94 | return 95 | } 96 | switch model.followingState { 97 | case .Following: 98 | model.unfollow(model.account) 99 | case .NotFollowing: 100 | model.follow(model.account) 101 | default: 102 | assert(false) 103 | } 104 | } 105 | 106 | func layout(width: CGFloat) { 107 | CellLayout.layoutActorRow(hasActorRow: true, width: width, avatar: actorAvatar, description: descriptionView) 108 | 109 | actionButton.sizeToFit() 110 | var buttonFrame = view.frame.alignedRect(size: actionButton.frame.size, horizontalOffset: 1.0, verticalOffset: 0.5) 111 | buttonFrame.origin.x -= CellLayout.margin.right 112 | actionButton.frame = buttonFrame 113 | } 114 | 115 | override func viewDidLayout() { 116 | super.viewDidLayout() 117 | layout(width: view.bounds.width) 118 | } 119 | 120 | static func size(width: CGFloat) -> CGSize { 121 | return CGSize(width: width, 122 | height: CellLayout.margin.top + AvatarView.size(.small).height + CellLayout.margin.bottom) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Pods/Starscream/Sources/Starscream/SSLClientCertificate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSLClientCertificate.swift 3 | // Starscream 4 | // 5 | // Created by Tomasz Trela on 08/03/2018. 6 | // Copyright © 2018 Vluxe. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct SSLClientCertificateError: LocalizedError { 12 | public var errorDescription: String? 13 | 14 | init(errorDescription: String) { 15 | self.errorDescription = errorDescription 16 | } 17 | } 18 | 19 | public class SSLClientCertificate { 20 | internal let streamSSLCertificates: NSArray 21 | 22 | /** 23 | Convenience init. 24 | - parameter pkcs12Path: Path to pkcs12 file containing private key and X.509 ceritifacte (.p12) 25 | - parameter password: file password, see **kSecImportExportPassphrase** 26 | */ 27 | public convenience init(pkcs12Path: String, password: String) throws { 28 | let pkcs12Url = URL(fileURLWithPath: pkcs12Path) 29 | do { 30 | try self.init(pkcs12Url: pkcs12Url, password: password) 31 | } catch { 32 | throw error 33 | } 34 | } 35 | 36 | /** 37 | Designated init. For more information, see SSLSetCertificate() in Security/SecureTransport.h. 38 | - parameter identity: SecIdentityRef, see **kCFStreamSSLCertificates** 39 | - parameter identityCertificate: CFArray of SecCertificateRefs, see **kCFStreamSSLCertificates** 40 | */ 41 | public init(identity: SecIdentity, identityCertificate: SecCertificate) { 42 | self.streamSSLCertificates = NSArray(objects: identity, identityCertificate) 43 | } 44 | 45 | /** 46 | Convenience init. 47 | - parameter pkcs12Url: URL to pkcs12 file containing private key and X.509 ceritifacte (.p12) 48 | - parameter password: file password, see **kSecImportExportPassphrase** 49 | */ 50 | public convenience init(pkcs12Url: URL, password: String) throws { 51 | let importOptions = [kSecImportExportPassphrase as String : password] as CFDictionary 52 | do { 53 | try self.init(pkcs12Url: pkcs12Url, importOptions: importOptions) 54 | } catch { 55 | throw error 56 | } 57 | } 58 | 59 | /** 60 | Designated init. 61 | - parameter pkcs12Url: URL to pkcs12 file containing private key and X.509 ceritifacte (.p12) 62 | - parameter importOptions: A dictionary containing import options. A 63 | kSecImportExportPassphrase entry is required at minimum. Only password-based 64 | PKCS12 blobs are currently supported. See **SecImportExport.h** 65 | */ 66 | public init(pkcs12Url: URL, importOptions: CFDictionary) throws { 67 | do { 68 | let pkcs12Data = try Data(contentsOf: pkcs12Url) 69 | var rawIdentitiesAndCertificates: CFArray? 70 | let pkcs12CFData: CFData = pkcs12Data as CFData 71 | let importStatus = SecPKCS12Import(pkcs12CFData, importOptions, &rawIdentitiesAndCertificates) 72 | 73 | guard importStatus == errSecSuccess else { 74 | throw SSLClientCertificateError(errorDescription: "(Starscream) Error during 'SecPKCS12Import', see 'SecBase.h' - OSStatus: \(importStatus)") 75 | } 76 | guard let identitiyAndCertificate = (rawIdentitiesAndCertificates as? Array>)?.first else { 77 | throw SSLClientCertificateError(errorDescription: "(Starscream) Error - PKCS12 file is empty") 78 | } 79 | 80 | let identity = identitiyAndCertificate[kSecImportItemIdentity as String] as! SecIdentity 81 | var identityCertificate: SecCertificate? 82 | let copyStatus = SecIdentityCopyCertificate(identity, &identityCertificate) 83 | guard copyStatus == errSecSuccess else { 84 | throw SSLClientCertificateError(errorDescription: "(Starscream) Error during 'SecIdentityCopyCertificate', see 'SecBase.h' - OSStatus: \(copyStatus)") 85 | } 86 | self.streamSSLCertificates = NSArray(objects: identity, identityCertificate!) 87 | } catch { 88 | throw error 89 | } 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /Mactodon/MultiFeedViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | import MastodonKit 5 | 6 | class MultiFeedViewController: NSViewController { 7 | enum Feed: Int { 8 | case UserTimeline 9 | case LocalTimeline 10 | case FederatedTimeline 11 | case Notifications 12 | } 13 | 14 | let client: ValuePromise 15 | let streamingController: ValuePromise 16 | 17 | var feedViewControllers: [Feed: FeedViewController] = [:] 18 | var selectedFeed = Feed.UserTimeline { 19 | didSet { 20 | updateSelectedFeedViewController() 21 | } 22 | } 23 | 24 | init(client: ValuePromise, streamingController: ValuePromise) { 25 | self.client = client 26 | self.streamingController = streamingController 27 | super.init(nibName: nil, bundle: nil) 28 | } 29 | 30 | required init?(coder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | override func loadView() { 35 | view = NSView() 36 | } 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | updateSelectedFeedViewController() 42 | } 43 | 44 | func updateSelectedFeedViewController() { 45 | children.forEach { (vc) in 46 | vc.removeFromParent() 47 | vc.view.removeFromSuperview() 48 | } 49 | 50 | let selectedVC = selectedFeedViewController 51 | selectedVC.view.autoresizingMask = [.width, .height] 52 | selectedVC.view.frame = view.bounds 53 | addChild(selectedVC) 54 | view.addSubview(selectedVC.view) 55 | } 56 | 57 | func createSignal(_ callback: @escaping (_ streamingController: StreamingController) -> (Promise)) -> Promise { 58 | return Promise({ [weak self] (completion) in 59 | self?.streamingController.didChange.then { (streamingController) in 60 | if let streamingController = streamingController { 61 | callback(streamingController).then { 62 | completion($0) 63 | } 64 | } 65 | } 66 | }, multiCall: true) 67 | } 68 | 69 | func createViewController(feed: Feed) -> FeedViewController { 70 | switch feed { 71 | case .UserTimeline: 72 | let signal = createSignal { $0.userStream.statusSignal } 73 | let deleteSignal = createSignal { $0.userStream.deletedSignal } 74 | return FeedViewController(feedProvider: FeedProvider.user(client: client, newStatusSignal: signal, deleteStatusSignal: deleteSignal)) 75 | case .LocalTimeline: 76 | let signal = createSignal { $0.localStream.statusSignal } 77 | let deleteSignal = createSignal { $0.localStream.deletedSignal } 78 | return FeedViewController(feedProvider: FeedProvider.local(client: client, newStatusSignal: signal, deleteStatusSignal: deleteSignal)) 79 | case .FederatedTimeline: 80 | let signal = createSignal { $0.federatedStream.statusSignal } 81 | let deleteSignal = createSignal { $0.federatedStream.deletedSignal } 82 | return FeedViewController(feedProvider: FeedProvider.federated(client: client, newStatusSignal: signal, deleteStatusSignal: deleteSignal)) 83 | case .Notifications: 84 | let signal = Promise({ [weak self] (completion, _) in 85 | self?.streamingController.didChange.then { (streamingController) in 86 | streamingController?.userStream.notificationSignal.then { notification in 87 | completion(notification) 88 | } 89 | } 90 | }, multiCall: true) 91 | let feedProvider = FeedProvider.notifications(client: client, newNotificationSignal: signal) 92 | let cellProvider = FeedViewNotificationCellProvider(feedProvider: feedProvider, client: client) 93 | return FeedViewController(cellProvider: cellProvider) 94 | } 95 | } 96 | 97 | func cachedViewController(feed: Feed) -> FeedViewController { 98 | if let viewController = feedViewControllers[selectedFeed] { 99 | return viewController 100 | } else { 101 | let viewController = self.createViewController(feed: selectedFeed) 102 | feedViewControllers[selectedFeed] = viewController 103 | return viewController 104 | } 105 | } 106 | 107 | var selectedFeedViewController: FeedViewController { 108 | get { 109 | return cachedViewController(feed: selectedFeed) 110 | } 111 | } 112 | 113 | func refresh() { 114 | selectedFeedViewController.refresh() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Mactodon/NotificationFeed/FeedViewNotificationCellProvider.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | import MastodonKit 5 | 6 | class FeedViewNotificationCellProvider: FeedViewCellProvider { 7 | weak var delegate: FeedViewCellProviderDelegate? 8 | 9 | typealias Notification = MastodonKit.Notification 10 | private let notificationFeedProvider: FeedProvider 11 | private let client: ValuePromise 12 | var feedProvider: TypelessFeedProvider { 13 | get { 14 | return notificationFeedProvider 15 | } 16 | } 17 | 18 | init(feedProvider: FeedProvider, client: ValuePromise) { 19 | self.notificationFeedProvider = feedProvider 20 | self.client = client 21 | 22 | feedProvider.prepare = { [weak self] (items) in 23 | guard let self = self else { 24 | return 25 | } 26 | 27 | let ids = items.filter({ $0.type == .follow }).map({ $0.account.id }) 28 | self.client.value!.run(Accounts.relationships(ids: ids)).mainQueue.then { relationships in 29 | relationships.forEach({ (relationship) in 30 | self.update(following: relationship.following ? .Following : .NotFollowing, accountId: relationship.id) 31 | }) 32 | } 33 | } 34 | } 35 | 36 | func prepare(collectionView: NSCollectionView) { 37 | collectionView.register(TootItem.self, forItemWithIdentifier: TootItem.identifier) 38 | collectionView.register(FollowingItem.self, forItemWithIdentifier: FollowingItem.identifier) 39 | } 40 | 41 | var itemCount: Int { 42 | get { 43 | return notificationFeedProvider.items.count 44 | } 45 | } 46 | 47 | private var followings: [String: FollowingItem.FollowingState] = [:] 48 | func following(account: Account) -> FollowingItem.FollowingState { 49 | return followings[account.id] ?? .Unknown 50 | } 51 | 52 | func update(following: FollowingItem.FollowingState, accountId: String) { 53 | followings[accountId] = following 54 | reloadFollowNotificationCell(accountId: accountId) 55 | } 56 | 57 | private func indexForFollowNotification(followerId: String) -> Int? { 58 | return notificationFeedProvider.items.firstIndex(where: { (notification) -> Bool in 59 | return (notification.type == .follow) && (notification.account.id == followerId) 60 | }) 61 | } 62 | 63 | func reloadFollowNotificationCell(accountId: String) { 64 | guard let index = indexForFollowNotification(followerId: accountId) else { 65 | return 66 | } 67 | 68 | delegate?.updateCell(index: index) 69 | } 70 | 71 | func followingModel(account: Account) -> FollowingItem.Model { 72 | let following = self.following(account: account) 73 | return FollowingItem.Model(account: account, followingState: following, follow: { [weak self] (account: Account) -> () in 74 | self?.follow(account: account) 75 | }, unfollow: { [weak self] (account: Account) -> () in 76 | self?.unfollow(account: account) 77 | }) 78 | } 79 | 80 | func item(collectionView: NSCollectionView, indexPath: IndexPath, index: Int) -> NSCollectionViewItem { 81 | let notification = notificationFeedProvider.items[index] 82 | if let model = TootItemModel(notification: notification) { 83 | let view = collectionView.makeItem(withIdentifier: TootItem.identifier, for: indexPath) as! TootItem 84 | view.model = model 85 | return view 86 | } else { 87 | let view = collectionView.makeItem(withIdentifier: FollowingItem.identifier, for: indexPath) as! FollowingItem 88 | view.model = followingModel(account: notification.account) 89 | return view 90 | } 91 | } 92 | 93 | func itemSize(collectionView: NSCollectionView, indexPath: IndexPath, index: Int) -> CGSize { 94 | let notification = notificationFeedProvider.items[index] 95 | if let model = TootItemModel(notification: notification) { 96 | return TootItem.size(width: collectionView.bounds.width, toot: model) 97 | } else { 98 | return FollowingItem.size(width: collectionView.bounds.width) 99 | } 100 | } 101 | } 102 | 103 | extension FeedViewNotificationCellProvider { 104 | func follow(account: Account) { 105 | self.update(following: .FollowRequested, accountId: account.id) 106 | 107 | client.value!.run(Accounts.follow(id: account.id)).mainQueue.then { [weak self] in 108 | self?.update(following: .Following, accountId: account.id) 109 | }.fail { [weak self] (_) in 110 | self?.update(following: .NotFollowing, accountId: account.id) 111 | } 112 | } 113 | 114 | func unfollow(account: Account) { 115 | self.update(following: .UnfollowRequested, accountId: account.id) 116 | 117 | client.value!.run(Accounts.follow(id: account.id)).mainQueue.then { [weak self] in 118 | self?.update(following: .NotFollowing, accountId: account.id) 119 | }.fail { [weak self] (_) in 120 | self?.update(following: .Following, accountId: account.id) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Pods/Nuke/Sources/ImageTaskMetrics.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2018 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | 7 | public struct ImageTaskMetrics: CustomDebugStringConvertible { 8 | public let taskId: Int 9 | public internal(set) var wasCancelled: Bool = false 10 | public internal(set) var session: SessionMetrics? 11 | 12 | public let startDate: Date 13 | public internal(set) var processStartDate: Date? 14 | public internal(set) var processEndDate: Date? 15 | public internal(set) var endDate: Date? // failed or completed 16 | public var totalDuration: TimeInterval? { 17 | guard let endDate = endDate else { return nil } 18 | return endDate.timeIntervalSince(startDate) 19 | } 20 | 21 | /// Returns `true` is the task wasn't the one that initiated image loading. 22 | public internal(set) var wasSubscibedToExistingSession: Bool = false 23 | public internal(set) var isMemoryCacheHit: Bool = false 24 | 25 | init(taskId: Int, startDate: Date) { 26 | self.taskId = taskId; self.startDate = startDate 27 | } 28 | 29 | public var debugDescription: String { 30 | var printer = Printer() 31 | printer.section(title: "Task Information") { 32 | $0.value("Task ID", taskId) 33 | $0.timeline("Duration", startDate, endDate, isReversed: false) 34 | $0.timeline("Process", processStartDate, processEndDate) 35 | $0.value("Was Cancelled", wasCancelled) 36 | $0.value("Is Memory Cache Hit", isMemoryCacheHit) 37 | $0.value("Was Subscribed To Existing Image Loading Session", wasSubscibedToExistingSession) 38 | } 39 | printer.section(title: "Image Loading Session") { 40 | $0.string(session.map({ $0.debugDescription }) ?? "nil") 41 | } 42 | return printer.output() 43 | } 44 | 45 | // Download session metrics. One more more tasks can share the same 46 | // session metrics. 47 | public final class SessionMetrics: CustomDebugStringConvertible { 48 | /// - important: Data loading might start prior to `timeResumed` if the task gets 49 | /// coalesced with another task. 50 | public let sessionId: Int 51 | public internal(set) var wasCancelled: Bool = false 52 | 53 | // MARK: - Timeline 54 | 55 | public let startDate = Date() 56 | 57 | public internal(set) var checkDiskCacheStartDate: Date? 58 | public internal(set) var checkDiskCacheEndDate: Date? 59 | 60 | public internal(set) var loadDataStartDate: Date? 61 | public internal(set) var loadDataEndDate: Date? 62 | 63 | public internal(set) var decodeStartDate: Date? 64 | public internal(set) var decodeEndDate: Date? 65 | 66 | @available(*, deprecated, message: "Please use the same property on `ImageTaskMetrics` instead.") 67 | public internal(set) var processStartDate: Date? 68 | 69 | @available(*, deprecated, message: "Please use the same property on `ImageTaskMetrics` instead.") 70 | public internal(set) var processEndDate: Date? 71 | 72 | public internal(set) var endDate: Date? // failed or completed 73 | 74 | public var totalDuration: TimeInterval? { 75 | guard let endDate = endDate else { return nil } 76 | return endDate.timeIntervalSince(startDate) 77 | } 78 | 79 | // MARK: - Resumable Data 80 | 81 | public internal(set) var wasResumed: Bool? 82 | public internal(set) var resumedDataCount: Int? 83 | public internal(set) var serverConfirmedResume: Bool? 84 | 85 | public internal(set) var downloadedDataCount: Int? 86 | public var totalDownloadedDataCount: Int? { 87 | guard let downloaded = self.downloadedDataCount else { return nil } 88 | return downloaded + (resumedDataCount ?? 0) 89 | } 90 | 91 | init(sessionId: Int) { self.sessionId = sessionId } 92 | 93 | public var debugDescription: String { 94 | var printer = Printer() 95 | printer.section(title: "Session Information") { 96 | $0.value("Session ID", sessionId) 97 | $0.value("Total Duration", Printer.duration(totalDuration)) 98 | $0.value("Was Cancelled", wasCancelled) 99 | } 100 | printer.section(title: "Timeline") { 101 | $0.timeline("Total", startDate, endDate) 102 | $0.line(String(repeating: "-", count: 36)) 103 | $0.timeline("Check Disk Cache", checkDiskCacheStartDate, checkDiskCacheEndDate) 104 | $0.timeline("Load Data", loadDataStartDate, loadDataEndDate) 105 | $0.timeline("Decode", decodeStartDate, decodeEndDate) 106 | } 107 | printer.section(title: "Resumable Data") { 108 | $0.value("Was Resumed", wasResumed) 109 | $0.value("Resumable Data Count", resumedDataCount) 110 | $0.value("Server Confirmed Resume", serverConfirmedResume) 111 | } 112 | return printer.output() 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Mactodon/Promise.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | 5 | protocol UntypedPromise { 6 | typealias UntypedThenCall = () -> Void 7 | typealias ErrorCall = (_ error: Error) -> Void 8 | 9 | @discardableResult func then(_ completion: @escaping UntypedThenCall) -> Self 10 | @discardableResult func fail(_ failedCompletion: @escaping ErrorCall) -> Self 11 | func `throw`(error: Error) 12 | } 13 | 14 | class Promise : UntypedPromise { 15 | typealias ReturnType = T 16 | typealias CompletionCallback = (_ result: T) -> Void 17 | typealias ThenCall = (_ result: T) -> Void 18 | 19 | public private(set) var error: Error? 20 | public private(set) var result: T? 21 | 22 | var fulfilled: Bool { 23 | get { 24 | return result != nil 25 | } 26 | } 27 | 28 | var failed: Bool { 29 | get { 30 | return error != nil 31 | } 32 | } 33 | 34 | var thenCalls: [ThenCall] = [] 35 | var errorCalls: [ErrorCall] = [] 36 | 37 | let multiCall: Bool 38 | 39 | convenience init(multiCall: Bool = false) { 40 | self.init({ (_, _) in }, multiCall: multiCall) 41 | } 42 | 43 | convenience init(_ setup: @escaping (_ complete: @escaping CompletionCallback) throws -> Void, multiCall: Bool = false) { 44 | let fullSetup = { (_ complete: @escaping CompletionCallback, _ promise: Promise) throws -> Void in 45 | try setup(complete) 46 | } 47 | 48 | self.init(fullSetup, multiCall: multiCall) 49 | } 50 | 51 | init(_ setup: @escaping (_ complete: @escaping CompletionCallback, _ promise: Promise) throws -> Void, multiCall: Bool = false) { 52 | self.multiCall = multiCall 53 | do { 54 | try setup({ (result) in 55 | self.result = result 56 | 57 | self.handle(thens: self.thenCalls) 58 | }, self) 59 | } catch { 60 | self.error = error 61 | 62 | self.handle(fails: self.errorCalls) 63 | } 64 | } 65 | 66 | private func handle(thens: [ThenCall]) { 67 | guard let result = self.result else { 68 | return 69 | } 70 | 71 | for thenCall in thens { 72 | thenCall(result) 73 | } 74 | } 75 | 76 | private func handle(fails: [ErrorCall]) { 77 | guard let error = self.error else { 78 | return 79 | } 80 | 81 | for errorCall in fails { 82 | errorCall(error) 83 | } 84 | } 85 | 86 | func `throw`(error: Error) { 87 | self.error = error 88 | 89 | handle(fails: errorCalls) 90 | } 91 | 92 | func fulfill(_ result: T) { 93 | assert(!fulfilled || multiCall, "promise already fulfilled") 94 | self.result = result 95 | handle(thens: thenCalls) 96 | } 97 | 98 | @discardableResult func then(_ completion: @escaping ThenCall) -> Self { 99 | thenCalls.append(completion) 100 | handle(thens: [completion]) 101 | return self 102 | } 103 | 104 | @discardableResult func then(_ completion: @escaping UntypedPromise.UntypedThenCall) -> Self { 105 | self.then { (_) in 106 | completion() 107 | } 108 | 109 | return self 110 | } 111 | 112 | @discardableResult func fail(_ failedCompletion: @escaping ErrorCall) -> Self { 113 | errorCalls.append(failedCompletion) 114 | handle(fails: [failedCompletion]) 115 | return self 116 | } 117 | 118 | func map(_ mapping: @escaping (_ result: T) -> U) -> Promise { 119 | return Promise({ (completion) in 120 | self.then({ (result) in 121 | completion(mapping(result)) 122 | }) 123 | }) 124 | } 125 | 126 | func map(_ mapping: @escaping (_ result: T, _ completion: @escaping (U) -> ()) -> ()) -> Promise { 127 | return Promise({ (completion) in 128 | self.then { (result) in 129 | mapping(result, completion) 130 | } 131 | }) 132 | } 133 | } 134 | 135 | extension Promise { 136 | // combines cascading promises into one single promise that fires when the last promise fires 137 | func combine(_ callback: @escaping (_ result: ReturnType) -> (Promise)) -> Promise { 138 | return Promise({ [weak self] (finalCallback: @escaping (_: T) -> Void) in 139 | guard let self = self else { 140 | return 141 | } 142 | 143 | self.then { (result) in 144 | callback(result).then { 145 | finalCallback($0) 146 | } 147 | } 148 | }) 149 | } 150 | } 151 | 152 | 153 | func allDone(_ promiseContainer: T) -> Promise { 154 | return Promise({ (completion, allDonePromise) in 155 | let promises: [UntypedPromise] 156 | if let dict = promiseContainer as? [AnyHashable: Any] { 157 | promises = dict.values.compactMap { $0 as? UntypedPromise } 158 | } else { 159 | promises = Mirror(reflecting: promiseContainer).children.compactMap { $0.value as? UntypedPromise } 160 | } 161 | 162 | var remaining = promises.count 163 | for promise in promises { 164 | promise.then { 165 | remaining -= 1 166 | if remaining == 0 { 167 | completion(promiseContainer) 168 | } 169 | } 170 | 171 | promise.fail { (error) in 172 | allDonePromise.throw(error: error) 173 | } 174 | } 175 | }) 176 | } 177 | -------------------------------------------------------------------------------- /Mactodon/InstanceViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Atributika 4 | import Cocoa 5 | import MastodonKit 6 | 7 | class InstanceViewController: NSViewController { 8 | var clientApplication: ClientApplication? 9 | let client = ValuePromise(initialValue: nil) 10 | let currentUser = ValuePromise(initialValue: nil) 11 | let streamingController = ValuePromise(initialValue: nil) 12 | var tokenController: TokenController? 13 | var multiFeedViewController: MultiFeedViewController! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | client.didChange.then { [weak self] in 19 | self?.update() 20 | } 21 | 22 | client.didChange.then { [weak self] (client) in 23 | if let client = client { 24 | StreamingController.controller(client: client).then { [weak self] (controller) in 25 | self?.streamingController.value = controller 26 | } 27 | } else { 28 | self?.streamingController.value = nil 29 | } 30 | } 31 | 32 | currentUser.didChange.mainQueue.then { [weak self] (currentUser) in 33 | self?.view.window?.title = currentUser?.username ?? "Mactodon" 34 | } 35 | 36 | let multiFeedViewController = MultiFeedViewController(client: client, streamingController: streamingController) 37 | check(selectedFeed: multiFeedViewController.selectedFeed) 38 | multiFeedViewController.view.autoresizingMask = [.width, .height] 39 | multiFeedViewController.view.frame = view.bounds 40 | addChild(multiFeedViewController) 41 | view.addSubview(multiFeedViewController.view) 42 | self.multiFeedViewController = multiFeedViewController 43 | } 44 | 45 | override func viewDidAppear() { 46 | super.viewDidAppear() 47 | 48 | let settings = Settings.load() 49 | guard let account = settings.accounts.first else { 50 | displayLogin() 51 | return 52 | } 53 | 54 | tokenController = TokenController(delegate: self, 55 | scopes: [.follow, .read, .write], 56 | username: account.username, 57 | instance: account.instance, 58 | protocolHandler: Bundle.main.bundleIdentifier!) 59 | tokenController?.acquireAuthenticatedClient() 60 | } 61 | 62 | lazy var loginViewController: LoginViewController = { 63 | let vc = storyboard!.instantiateLoginViewController() 64 | vc.delegate = self 65 | return vc 66 | }() 67 | 68 | func displayLogin() { 69 | presentAsSheet(loginViewController) 70 | } 71 | 72 | func update() { 73 | guard let client = self.client.value else { 74 | return 75 | } 76 | 77 | client.run(Accounts.currentUser()).then { 78 | self.currentUser.value = $0 79 | } 80 | } 81 | 82 | @IBAction func refreshFeed(_ sender: AnyObject) { 83 | multiFeedViewController.refresh() 84 | } 85 | 86 | lazy var switchMenuItems: [MultiFeedViewController.Feed: NSMenuItem] = { 87 | let appDelegate = AppDelegate.Shared() 88 | return [ 89 | .UserTimeline: appDelegate.switchToUserTimeline, 90 | .LocalTimeline: appDelegate.switchToLocalTimeline, 91 | .FederatedTimeline: appDelegate.switchToFederatedTimeline, 92 | .Notifications: appDelegate.switchToNotifications 93 | ] 94 | }() 95 | 96 | func check(selectedFeed: MultiFeedViewController.Feed) { 97 | switchMenuItems.forEach { (element) in 98 | element.value.state = element.key == selectedFeed ? .on : .off 99 | } 100 | } 101 | 102 | @IBAction func switchToFeed(_ sender: NSMenuItem) { 103 | let feed = switchMenuItems.first { (_, value) -> Bool in 104 | return value == sender 105 | }!.key 106 | multiFeedViewController.selectedFeed = feed 107 | check(selectedFeed: feed) 108 | } 109 | } 110 | 111 | extension InstanceViewController: LoginViewControllerDelegate { 112 | func registered(baseURL: URL) { 113 | tokenController = TokenController(delegate: self, scopes: [.follow, .read, .write], instance: baseURL.host!, protocolHandler: Bundle.main.bundleIdentifier!) 114 | tokenController!.acquireAuthenticatedClient() 115 | } 116 | } 117 | 118 | extension InstanceViewController: TokenControllerDelegate { 119 | func loadClientApplication(instance: String) -> ClientApplication? { 120 | return try! Keychain.getClientApplication(instance: instance) 121 | } 122 | 123 | func loadLoginSettings(username: String, instance: String) -> LoginSettings? { 124 | return try! Keychain.getLoginSettings(forUser: username, instance: instance) 125 | } 126 | 127 | func store(clientApplication: ClientApplication, forInstance instance: String) { 128 | try! Keychain.set(clientApplication: clientApplication, instance: instance) 129 | } 130 | 131 | func store(loginSettings: LoginSettings, forUsername username: String, instance: String) { 132 | try! Keychain.set(loginSettings: loginSettings, forUser: username, instance: instance) 133 | 134 | var settings = Settings.load() 135 | settings.accounts = settings.accounts + [Settings.Account(username, instance)] 136 | settings.save() 137 | } 138 | 139 | func authenticatedClient(client: Client) { 140 | self.client.value = client 141 | } 142 | 143 | func clientName() -> String { 144 | return "Mactodon" 145 | } 146 | 147 | func open(url: URL) { 148 | NSWorkspace.shared.open(url) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Mactodon/Mastodon/TokenController.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | import MastodonKit 5 | 6 | protocol TokenControllerDelegate: AnyObject { 7 | func loadClientApplication(instance: String) -> ClientApplication? 8 | func loadLoginSettings(username: String, instance: String) -> LoginSettings? 9 | func store(clientApplication: ClientApplication, forInstance: String) 10 | func store(loginSettings: LoginSettings, forUsername: String, instance: String) 11 | func authenticatedClient(client: Client) 12 | func clientName() -> String 13 | func open(url: URL) 14 | } 15 | 16 | class TokenController { 17 | let scopes: [AccessScope] 18 | let instance: String 19 | let uuid = UUID().uuidString 20 | var username: String? 21 | let redirectUri: String 22 | let baseUrl: String 23 | lazy var anonymousClient: Client = { 24 | return Client(baseURL: baseUrl) 25 | }() 26 | var authenticatedClient: Client? 27 | 28 | fileprivate static var controllers: Set = [] 29 | 30 | weak var delegate: TokenControllerDelegate? 31 | 32 | var clientApplication: ClientApplication? 33 | var loginSettings: LoginSettings? 34 | 35 | convenience init(delegate: TokenControllerDelegate, scopes: [AccessScope], username: String, instance: String, protocolHandler: String) { 36 | self.init(delegate: delegate, scopes: scopes, instance: instance, protocolHandler: protocolHandler) 37 | self.username = username 38 | } 39 | 40 | init(delegate: TokenControllerDelegate, scopes: [AccessScope], instance: String, protocolHandler: String) { 41 | self.delegate = delegate 42 | self.scopes = scopes 43 | self.instance = instance 44 | self.redirectUri = "\(protocolHandler)://authenticated/?uuid=\(self.uuid)" 45 | self.baseUrl = "https://\(instance)/" 46 | } 47 | 48 | func loadStoredItems() { 49 | guard let delegate = delegate else { 50 | return 51 | } 52 | 53 | if clientApplication == nil { 54 | clientApplication = delegate.loadClientApplication(instance: instance) 55 | } 56 | 57 | if let username = username, loginSettings == nil { 58 | loginSettings = delegate.loadLoginSettings(username: username, instance: instance) 59 | } 60 | } 61 | 62 | func acquireAuthenticatedClient() { 63 | loadStoredItems() 64 | 65 | if let loginSettings = loginSettings { 66 | authenticatedClient = Client(baseURL: baseUrl, accessToken: loginSettings.accessToken) 67 | delegate?.authenticatedClient(client: authenticatedClient!) 68 | return 69 | } 70 | 71 | let applicationPromise: Promise 72 | if let clientApplication = clientApplication { 73 | applicationPromise = Promise({ (completion) in 74 | completion(clientApplication) 75 | }) 76 | } else { 77 | let clientName = delegate!.clientName() 78 | let request = Clients.register(clientName: clientName, redirectURI: redirectUri, scopes: scopes) 79 | applicationPromise = anonymousClient.run(request) 80 | applicationPromise.then { (clientApplication) in 81 | self.clientApplication = clientApplication 82 | self.delegate?.store(clientApplication: clientApplication, forInstance: self.instance) 83 | } 84 | } 85 | 86 | applicationPromise.then { (clientApplication) in 87 | // we now have an ClientApplication for sure, let's ask the client to open the browser to authentikate the user 88 | let url = URLComponents(string: self.baseUrl + "oauth/authorize", 89 | queryItems: ["scope": self.scopes.map({ $0.rawValue }).joined(separator: " "), 90 | "client_id": clientApplication.clientID, 91 | "redirect_uri": self.redirectUri, 92 | "response_type": "code" 93 | ])!.url! 94 | TokenController.controllers.insert(self) 95 | self.delegate?.open(url: url) 96 | } 97 | } 98 | 99 | static func handleCallback(url: URL) { 100 | let components = URLComponents(url: url, resolvingAgainstBaseURL: false)! 101 | let queryItems = components.queryDict 102 | let uuid = queryItems["uuid"]!! 103 | let code = queryItems["code"]!! 104 | 105 | let tokenController = TokenController.controllers.first(where: { $0.uuid == uuid })! 106 | TokenController.controllers.remove(tokenController) 107 | tokenController.generateAccessToken(authenticationCode: code) 108 | } 109 | 110 | func generateAccessToken(authenticationCode: String) { 111 | let app = self.clientApplication! 112 | let request = Login.oauth(clientID: app.clientID, clientSecret: app.clientSecret, scopes: scopes, redirectURI: redirectUri, code: authenticationCode) 113 | anonymousClient.run(request).then { (login) in 114 | let client = Client(baseURL: self.baseUrl, accessToken: login.accessToken) 115 | self.authenticatedClient = client 116 | self.delegate?.authenticatedClient(client: client) 117 | client.run(Accounts.currentUser()).then { (account) in 118 | self.delegate?.store(loginSettings: login, forUsername: account.username, instance: self.instance) 119 | } 120 | } 121 | } 122 | } 123 | 124 | extension TokenController: Hashable { 125 | func hash(into hasher: inout Hasher) { 126 | hasher.combine(uuid) 127 | } 128 | 129 | public static func == (lhs: TokenController, rhs: TokenController) -> Bool { 130 | return lhs.uuid == rhs.uuid 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Mactodon/FeedRendering/FeedProvider.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Cocoa 4 | import MastodonKit 5 | 6 | protocol FeedProviderDelegate: AnyObject { 7 | func feedProviderReady() 8 | func didSet(itemCount: Int) 9 | func didPrepend(itemCount: Int) 10 | func didAppend(itemCount: Int) 11 | func didDelete(index: Int) 12 | } 13 | 14 | protocol TypelessFeedProvider: AnyObject { 15 | var client: ValuePromise { get } 16 | var delegate: FeedProviderDelegate? { get set } 17 | var isLoading: Bool { get } 18 | var ready: Bool { get } 19 | func reload() 20 | func loadMore() 21 | } 22 | 23 | class FeedProvider: TypelessFeedProvider { 24 | typealias TimelineRequest = (_ range: RequestRange) -> Request<[T]> 25 | 26 | var items: [T] = [] 27 | 28 | weak var delegate: FeedProviderDelegate? { 29 | didSet { 30 | if client.value != nil { 31 | delegate?.feedProviderReady() 32 | } 33 | } 34 | } 35 | 36 | var isLoading: Bool { 37 | get { 38 | return _isLoading 39 | } 40 | } 41 | 42 | var prepare: ((_ items: [T]) -> ()) = {_ in } 43 | 44 | var ready: Bool { 45 | get { 46 | return client.value != nil 47 | } 48 | } 49 | 50 | let client: ValuePromise 51 | private let streamingClient: ValuePromise = ValuePromise(initialValue: nil) 52 | private let request: TimelineRequest 53 | private var _isLoading = false 54 | private var nextPage: RequestRange? 55 | private var previousPage: RequestRange? 56 | 57 | public init(client: ValuePromise, request: @escaping TimelineRequest, newItemSignal: Promise? = nil) { 58 | self.client = client 59 | self.request = request 60 | self.client.didChange.then { 61 | self.delegate?.feedProviderReady() 62 | } 63 | 64 | newItemSignal?.then { [weak self] (item) in 65 | guard let self = self else { 66 | return 67 | } 68 | 69 | self.prepare([item]) 70 | self.items = [item] + self.items 71 | self.delegate?.didPrepend(itemCount: 1) 72 | } 73 | } 74 | 75 | static func user(client: ValuePromise, newStatusSignal: Promise, deleteStatusSignal: Promise) -> FeedProvider { 76 | return StatusFeedProvider(client: client, request: { (range) -> Request<[Status]> in 77 | return Timelines.home(range: range) 78 | }, statusSignal: newStatusSignal, deleteSignal: deleteStatusSignal) 79 | } 80 | 81 | static func local(client: ValuePromise, newStatusSignal: Promise, deleteStatusSignal: Promise) -> FeedProvider { 82 | return StatusFeedProvider(client: client, request: { (range) -> Request<[Status]> in 83 | return Timelines.public(local: true, range: range) 84 | }, statusSignal: newStatusSignal, deleteSignal: deleteStatusSignal) 85 | } 86 | 87 | static func federated(client: ValuePromise, newStatusSignal: Promise, deleteStatusSignal: Promise) -> FeedProvider { 88 | return StatusFeedProvider(client: client, request: { (range) -> Request<[Status]> in 89 | return Timelines.public(local: false, range: range) 90 | }, statusSignal: newStatusSignal, deleteSignal: deleteStatusSignal) 91 | } 92 | 93 | static func notifications(client: ValuePromise, newNotificationSignal: Promise? = nil) -> FeedProvider { 94 | return FeedProvider(client: client, request: { (range) -> Request<[MastodonKit.Notification]> in 95 | return Notifications.all(range: range) 96 | }, newItemSignal: newNotificationSignal) 97 | } 98 | 99 | func reload() { 100 | if (isLoading) { 101 | return 102 | } 103 | 104 | _isLoading = true 105 | client.value?.runPaginated(request(.default)).mainQueue.then { [weak self] (result) in 106 | guard let self = self else { 107 | return 108 | } 109 | 110 | self._isLoading = false 111 | self.prepare(result.value) 112 | self.items = result.value 113 | self.delegate?.didSet(itemCount: result.value.count) 114 | self.previousPage = result.pagination?.previous 115 | self.nextPage = result.pagination?.next 116 | 117 | }.fail { [weak self] (_) in 118 | self?._isLoading = false 119 | } 120 | } 121 | 122 | func loadMore() { 123 | guard let nextPage = nextPage else { 124 | return 125 | } 126 | _isLoading = true 127 | client.value?.runPaginated(request(nextPage)).mainQueue.then { [weak self] (result) in 128 | guard let self = self else { 129 | return 130 | } 131 | 132 | self._isLoading = false 133 | self.prepare(result.value) 134 | self.items += result.value 135 | self.delegate?.didSet(itemCount: result.value.count) 136 | self.nextPage = result.pagination?.next 137 | }.fail({ [weak self] (_) in 138 | self?._isLoading = false 139 | }) 140 | } 141 | } 142 | 143 | class StatusFeedProvider: FeedProvider { 144 | init(client: ValuePromise, request: @escaping TimelineRequest, statusSignal: Promise, deleteSignal: Promise) { 145 | super.init(client: client, request: request, newItemSignal: statusSignal) 146 | 147 | deleteSignal.then { [weak self] (statusID) in 148 | guard let self = self else { 149 | return 150 | } 151 | 152 | let index = self.items.firstIndex(where: { $0.id == statusID }) 153 | if let index = index { 154 | self.items.remove(at: index) 155 | self.delegate?.didDelete(index: index) 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Mactodon/Mastodon/StreamingClient.swift: -------------------------------------------------------------------------------- 1 | // Copyright Max von Webel. All Rights Reserved. 2 | 3 | import Foundation 4 | import MastodonKit 5 | import Reachability 6 | import Starscream 7 | 8 | protocol StreamingClientDelegate: AnyObject { 9 | func connected(streamingClient: StreamingClient) 10 | func disconnected(streamingClient: StreamingClient, error: Error?) 11 | func received(status: Status, streamingClient: StreamingClient) 12 | func received(notification: MastodonKit.Notification, streamingClient: StreamingClient) 13 | func deleted(statusID: String, streamingClient: StreamingClient) 14 | func filtersChanged(streamingClient: StreamingClient) 15 | } 16 | 17 | class StreamingClient { 18 | let socket: WebSocket 19 | let reachability = Reachability()! 20 | weak var delegate: StreamingClientDelegate? 21 | 22 | let statusSignal = Promise(multiCall: true) 23 | let notificationSignal = Promise(multiCall: true) 24 | let deletedSignal = Promise(multiCall: true) 25 | 26 | var shouldBeConnected: Bool = false 27 | 28 | enum Timeline: String { 29 | case User = "user" 30 | case Local = "public:local" 31 | case Federated = "public" 32 | } 33 | 34 | init(instance: String, timeline: Timeline, accessToken: String?) { 35 | let url = URL(string: "wss://\(instance)/api/v1/streaming/?stream=\(timeline.rawValue)")! 36 | var request = URLRequest(url: url) 37 | request.httpMethod = "GET" 38 | if let accessToken = accessToken { 39 | request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "authorization") 40 | } 41 | self.socket = WebSocket(request: request) 42 | self.socket.delegate = self 43 | 44 | reachability.whenReachable = { [weak self] (_) in 45 | guard let self = self else { 46 | return 47 | } 48 | 49 | if self.shouldBeConnected { 50 | self.socket.connect() 51 | } 52 | } 53 | 54 | reachability.whenUnreachable = { [weak self] (_) in 55 | self?.socket.disconnect() 56 | } 57 | 58 | try? reachability.startNotifier() 59 | } 60 | 61 | func connect() { 62 | shouldBeConnected = true 63 | socket.connect() 64 | } 65 | 66 | func disconnect() { 67 | shouldBeConnected = false 68 | socket.disconnect() 69 | } 70 | } 71 | 72 | extension StreamingClient: WebSocketDelegate { 73 | func websocketDidConnect(socket: WebSocketClient) { 74 | delegate?.connected(streamingClient: self) 75 | } 76 | 77 | func websocketDidDisconnect(socket: WebSocketClient, error: Error?) { 78 | delegate?.disconnected(streamingClient: self, error: error) 79 | } 80 | 81 | func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { 82 | handleResponse(data: text.data(using: .utf8)!) 83 | } 84 | 85 | func websocketDidReceiveData(socket: WebSocketClient, data: Data) { 86 | assert(false) 87 | } 88 | 89 | private func handleResponse(data: Data) { 90 | let event = try! JSONDecoder().decode(Event.self, from: data) 91 | switch event { 92 | case .Update(let status): 93 | statusSignal.fulfill(status) 94 | delegate?.received(status: status, streamingClient: self) 95 | case .Notification(let notification): 96 | notificationSignal.fulfill(notification) 97 | delegate?.received(notification: notification, streamingClient: self) 98 | case .Delete(let statusID): 99 | deletedSignal.fulfill(statusID) 100 | delegate?.deleted(statusID: statusID, streamingClient: self) 101 | case .FiltersChanged: 102 | delegate?.filtersChanged(streamingClient: self) 103 | } 104 | } 105 | } 106 | 107 | 108 | // protected helper, copied from MastodonKit 109 | private extension DateFormatter { 110 | static let mastodonFormatter: DateFormatter = { 111 | let dateFormatter = DateFormatter() 112 | 113 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" 114 | dateFormatter.timeZone = TimeZone(abbreviation: "UTC") 115 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 116 | 117 | return dateFormatter 118 | }() 119 | } 120 | 121 | // protected helper, copied from MastodonKit 122 | private extension Decodable { 123 | static func decode(data: Data) throws -> Self { 124 | let decoder = JSONDecoder() 125 | decoder.dateDecodingStrategy = .formatted(.mastodonFormatter) 126 | return try decoder.decode(Self.self, from: data) 127 | } 128 | } 129 | 130 | extension StreamingClient { 131 | fileprivate enum Event: Decodable { 132 | case Update(status: Status) 133 | case Notification(notification: MastodonKit.Notification) 134 | case Delete(id: String) 135 | case FiltersChanged 136 | 137 | struct RawEvent: Decodable { 138 | enum Kind: String, Decodable { 139 | case Update = "update" 140 | case Notification = "notification" 141 | case Delete = "delete" 142 | case FiltersChanged = "filters_changed" 143 | } 144 | 145 | let event: Kind 146 | let payload: String? 147 | 148 | var payloadData: Data? { 149 | get { 150 | return payload?.data(using: .utf8) 151 | } 152 | } 153 | } 154 | 155 | init(from decoder: Decoder) throws { 156 | let rawEvent = try RawEvent(from: decoder) 157 | switch rawEvent.event { 158 | case .Update: 159 | let status = try Status.decode(data: rawEvent.payloadData!) 160 | self = .Update(status: status) 161 | case .Notification: 162 | let notification = try MastodonKit.Notification.decode(data: rawEvent.payloadData!) 163 | self = .Notification(notification: notification) 164 | case .Delete: 165 | let id = rawEvent.payload! 166 | self = .Delete(id: id) 167 | case .FiltersChanged: 168 | self = .FiltersChanged 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Pods/Starscream/Sources/Starscream/Compression.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Compression.swift 4 | // 5 | // Created by Joseph Ross on 7/16/14. 6 | // Copyright © 2017 Joseph Ross. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | ////////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | ////////////////////////////////////////////////////////////////////////////////////////////////// 23 | // 24 | // Compression implementation is implemented in conformance with RFC 7692 Compression Extensions 25 | // for WebSocket: https://tools.ietf.org/html/rfc7692 26 | // 27 | ////////////////////////////////////////////////////////////////////////////////////////////////// 28 | 29 | import Foundation 30 | import zlib 31 | 32 | class Decompressor { 33 | private var strm = z_stream() 34 | private var buffer = [UInt8](repeating: 0, count: 0x2000) 35 | private var inflateInitialized = false 36 | private let windowBits:Int 37 | 38 | init?(windowBits:Int) { 39 | self.windowBits = windowBits 40 | guard initInflate() else { return nil } 41 | } 42 | 43 | private func initInflate() -> Bool { 44 | if Z_OK == inflateInit2_(&strm, -CInt(windowBits), 45 | ZLIB_VERSION, CInt(MemoryLayout.size)) 46 | { 47 | inflateInitialized = true 48 | return true 49 | } 50 | return false 51 | } 52 | 53 | func reset() throws { 54 | teardownInflate() 55 | guard initInflate() else { throw WSError(type: .compressionError, message: "Error for decompressor on reset", code: 0) } 56 | } 57 | 58 | func decompress(_ data: Data, finish: Bool) throws -> Data { 59 | return try data.withUnsafeBytes { (bytes:UnsafePointer) -> Data in 60 | return try decompress(bytes: bytes, count: data.count, finish: finish) 61 | } 62 | } 63 | 64 | func decompress(bytes: UnsafePointer, count: Int, finish: Bool) throws -> Data { 65 | var decompressed = Data() 66 | try decompress(bytes: bytes, count: count, out: &decompressed) 67 | 68 | if finish { 69 | let tail:[UInt8] = [0x00, 0x00, 0xFF, 0xFF] 70 | try decompress(bytes: tail, count: tail.count, out: &decompressed) 71 | } 72 | 73 | return decompressed 74 | 75 | } 76 | 77 | private func decompress(bytes: UnsafePointer, count: Int, out:inout Data) throws { 78 | var res:CInt = 0 79 | strm.next_in = UnsafeMutablePointer(mutating: bytes) 80 | strm.avail_in = CUnsignedInt(count) 81 | 82 | repeat { 83 | strm.next_out = UnsafeMutablePointer(&buffer) 84 | strm.avail_out = CUnsignedInt(buffer.count) 85 | 86 | res = inflate(&strm, 0) 87 | 88 | let byteCount = buffer.count - Int(strm.avail_out) 89 | out.append(buffer, count: byteCount) 90 | } while res == Z_OK && strm.avail_out == 0 91 | 92 | guard (res == Z_OK && strm.avail_out > 0) 93 | || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) 94 | else { 95 | throw WSError(type: .compressionError, message: "Error on decompressing", code: 0) 96 | } 97 | } 98 | 99 | private func teardownInflate() { 100 | if inflateInitialized, Z_OK == inflateEnd(&strm) { 101 | inflateInitialized = false 102 | } 103 | } 104 | 105 | deinit { 106 | teardownInflate() 107 | } 108 | } 109 | 110 | class Compressor { 111 | private var strm = z_stream() 112 | private var buffer = [UInt8](repeating: 0, count: 0x2000) 113 | private var deflateInitialized = false 114 | private let windowBits:Int 115 | 116 | init?(windowBits: Int) { 117 | self.windowBits = windowBits 118 | guard initDeflate() else { return nil } 119 | } 120 | 121 | private func initDeflate() -> Bool { 122 | if Z_OK == deflateInit2_(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 123 | -CInt(windowBits), 8, Z_DEFAULT_STRATEGY, 124 | ZLIB_VERSION, CInt(MemoryLayout.size)) 125 | { 126 | deflateInitialized = true 127 | return true 128 | } 129 | return false 130 | } 131 | 132 | func reset() throws { 133 | teardownDeflate() 134 | guard initDeflate() else { throw WSError(type: .compressionError, message: "Error for compressor on reset", code: 0) } 135 | } 136 | 137 | func compress(_ data: Data) throws -> Data { 138 | var compressed = Data() 139 | var res:CInt = 0 140 | data.withUnsafeBytes { (ptr:UnsafePointer) -> Void in 141 | strm.next_in = UnsafeMutablePointer(mutating: ptr) 142 | strm.avail_in = CUnsignedInt(data.count) 143 | 144 | repeat { 145 | strm.next_out = UnsafeMutablePointer(&buffer) 146 | strm.avail_out = CUnsignedInt(buffer.count) 147 | 148 | res = deflate(&strm, Z_SYNC_FLUSH) 149 | 150 | let byteCount = buffer.count - Int(strm.avail_out) 151 | compressed.append(buffer, count: byteCount) 152 | } 153 | while res == Z_OK && strm.avail_out == 0 154 | 155 | } 156 | 157 | guard res == Z_OK && strm.avail_out > 0 158 | || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) 159 | else { 160 | throw WSError(type: .compressionError, message: "Error on compressing", code: 0) 161 | } 162 | 163 | compressed.removeLast(4) 164 | return compressed 165 | } 166 | 167 | private func teardownDeflate() { 168 | if deflateInitialized, Z_OK == deflateEnd(&strm) { 169 | deflateInitialized = false 170 | } 171 | } 172 | 173 | deinit { 174 | teardownDeflate() 175 | } 176 | } 177 | 178 | -------------------------------------------------------------------------------- /Pods/MastodonKit/Sources/MastodonKit/Requests/Statuses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Statuses.swift 3 | // MastodonKit 4 | // 5 | // Created by Ornithologist Coder on 4/9/17. 6 | // Copyright © 2017 MastodonKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Statuses` requests. 12 | public enum Statuses { 13 | /// Fetches a status. 14 | /// 15 | /// - Parameter id: The status id. 16 | /// - Returns: Request for `Status`. 17 | public static func status(id: String) -> Request { 18 | return Request(path: "/api/v1/statuses/\(id)") 19 | } 20 | 21 | /// Gets a status context. 22 | /// 23 | /// - Parameter id: The status id. 24 | /// - Returns: Request for `Context`. 25 | public static func context(id: String) -> Request { 26 | return Request(path: "/api/v1/statuses/\(id)/context") 27 | } 28 | 29 | /// Gets a card associated with a status. 30 | /// 31 | /// - Parameter id: The status id. 32 | /// - Returns: Request for `Card`. 33 | public static func card(id: String) -> Request { 34 | return Request(path: "/api/v1/statuses/\(id)/card") 35 | } 36 | 37 | /// Gets who reblogged a status. 38 | /// 39 | /// - Parameters: 40 | /// - id: The status id. 41 | /// - range: The bounds used when requesting data from Mastodon. 42 | /// - Returns: Request for `[Account]`. 43 | public static func rebloggedBy(id: String, range: RequestRange = .default) -> Request<[Account]> { 44 | let parameters = range.parameters(limit: between(1, and: 80, default: 40)) 45 | let method = HTTPMethod.get(.parameters(parameters)) 46 | 47 | return Request<[Account]>(path: "/api/v1/statuses/\(id)/reblogged_by", method: method) 48 | } 49 | 50 | /// Gets who favourited a status. 51 | /// 52 | /// - Parameters: 53 | /// - id: The status id. 54 | /// - range: The bounds used when requesting data from Mastodon. 55 | /// - Returns: Request for `[Account]`. 56 | public static func favouritedBy(id: String, range: RequestRange = .default) -> Request<[Account]> { 57 | let parameters = range.parameters(limit: between(1, and: 80, default: 40)) 58 | let method = HTTPMethod.get(.parameters(parameters)) 59 | 60 | return Request<[Account]>(path: "/api/v1/statuses/\(id)/favourited_by", method: method) 61 | } 62 | 63 | /// Posts a new status. 64 | /// 65 | /// - Parameters: 66 | /// - status: The text of the status. 67 | /// - replyTo: The local ID of the status you want to reply to. 68 | /// - mediaIDs: The array of media IDs to attach to the status (maximum 4). 69 | /// - sensitive: Marks the status as NSFW. 70 | /// - spoilerText: the text to be shown as a warning before the actual content. 71 | /// - visibility: The status' visibility. 72 | /// - Returns: Request for `Status`. 73 | public static func create(status: String, 74 | replyToID: String? = nil, 75 | mediaIDs: [String] = [], 76 | sensitive: Bool? = nil, 77 | spoilerText: String? = nil, 78 | visibility: Visibility = .public) -> Request { 79 | let parameters = [ 80 | Parameter(name: "status", value: status), 81 | Parameter(name: "in_reply_to_id", value: replyToID), 82 | Parameter(name: "sensitive", value: sensitive.flatMap(trueOrNil)), 83 | Parameter(name: "spoiler_text", value: spoilerText), 84 | Parameter(name: "visibility", value: visibility.rawValue) 85 | ] + mediaIDs.map(toArrayOfParameters(withName: "media_ids")) 86 | 87 | let method = HTTPMethod.post(.parameters(parameters)) 88 | return Request(path: "/api/v1/statuses", method: method) 89 | } 90 | 91 | /// Deletes a status. 92 | /// 93 | /// - Parameter id: The status id. 94 | /// - Returns: Request for `Empty`. 95 | public static func delete(id: String) -> Request { 96 | return Request(path: "/api/v1/statuses/\(id)", method: .delete(.empty)) 97 | } 98 | 99 | /// Reblogs a status. 100 | /// 101 | /// - Parameter id: The status id. 102 | /// - Returns: Request for `Status`. 103 | public static func reblog(id: String) -> Request { 104 | return Request(path: "/api/v1/statuses/\(id)/reblog", method: .post(.empty)) 105 | } 106 | 107 | /// Unreblogs a status. 108 | /// 109 | /// - Parameter id: The status id. 110 | /// - Returns: Request for `Status`. 111 | public static func unreblog(id: String) -> Request { 112 | return Request(path: "/api/v1/statuses/\(id)/unreblog", method: .post(.empty)) 113 | } 114 | 115 | /// Favourites a status. 116 | /// 117 | /// - Parameter id: The status id. 118 | /// - Returns: Request for `Status`. 119 | public static func favourite(id: String) -> Request { 120 | return Request(path: "/api/v1/statuses/\(id)/favourite", method: .post(.empty)) 121 | } 122 | 123 | /// Unfavourites a status. 124 | /// 125 | /// - Parameter id: The status id. 126 | /// - Returns: Request for `Status`. 127 | public static func unfavourite(id: String) -> Request { 128 | return Request(path: "/api/v1/statuses/\(id)/unfavourite", method: .post(.empty)) 129 | } 130 | 131 | /// Pins a status. 132 | /// 133 | /// - Parameter id: The status id. 134 | /// - Returns: Request for `Status`. 135 | public static func pin(id: String) -> Request { 136 | return Request(path: "/api/v1/statuses/\(id)/pin", method: .post(.empty)) 137 | } 138 | 139 | /// Unpins a status. 140 | /// 141 | /// - Parameter id: The status id. 142 | /// - Returns: Request for `Status`. 143 | public static func unpin(id: String) -> Request { 144 | return Request(path: "/api/v1/statuses/\(id)/unpin", method: .post(.empty)) 145 | } 146 | 147 | /// Mutes a status. 148 | /// 149 | /// - Parameter id: The status id. 150 | /// - Returns: Request for `Status`. 151 | public static func mute(id: String) -> Request { 152 | return Request(path: "/api/v1/statuses/\(id)/mute", method: .post(.empty)) 153 | } 154 | 155 | /// Unmutes a status. 156 | /// 157 | /// - Parameter id: The status id. 158 | /// - Returns: Request for `Status`. 159 | public static func unmute(id: String) -> Request { 160 | return Request(path: "/api/v1/statuses/\(id)/unmute", method: .post(.empty)) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Mactodon/Pods-Mactodon-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH+x} ]; then 7 | # If UNLOCALIZED_RESOURCES_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # resources to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 13 | 14 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 15 | > "$RESOURCES_TO_COPY" 16 | 17 | XCASSET_FILES=() 18 | 19 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 20 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 21 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 22 | 23 | case "${TARGETED_DEVICE_FAMILY:-}" in 24 | 1,2) 25 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 26 | ;; 27 | 1) 28 | TARGET_DEVICE_ARGS="--target-device iphone" 29 | ;; 30 | 2) 31 | TARGET_DEVICE_ARGS="--target-device ipad" 32 | ;; 33 | 3) 34 | TARGET_DEVICE_ARGS="--target-device tv" 35 | ;; 36 | 4) 37 | TARGET_DEVICE_ARGS="--target-device watch" 38 | ;; 39 | *) 40 | TARGET_DEVICE_ARGS="--target-device mac" 41 | ;; 42 | esac 43 | 44 | install_resource() 45 | { 46 | if [[ "$1" = /* ]] ; then 47 | RESOURCE_PATH="$1" 48 | else 49 | RESOURCE_PATH="${PODS_ROOT}/$1" 50 | fi 51 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 52 | cat << EOM 53 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 54 | EOM 55 | exit 1 56 | fi 57 | case $RESOURCE_PATH in 58 | *.storyboard) 59 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 60 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 61 | ;; 62 | *.xib) 63 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 64 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 65 | ;; 66 | *.framework) 67 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 68 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 69 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 70 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 71 | ;; 72 | *.xcdatamodel) 73 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true 74 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 75 | ;; 76 | *.xcdatamodeld) 77 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true 78 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 79 | ;; 80 | *.xcmappingmodel) 81 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true 82 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 83 | ;; 84 | *.xcassets) 85 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 86 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 87 | ;; 88 | *) 89 | echo "$RESOURCE_PATH" || true 90 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 91 | ;; 92 | esac 93 | } 94 | 95 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 97 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 98 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 99 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 100 | fi 101 | rm -f "$RESOURCES_TO_COPY" 102 | 103 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ] 104 | then 105 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 106 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 107 | while read line; do 108 | if [[ $line != "${PODS_ROOT}*" ]]; then 109 | XCASSET_FILES+=("$line") 110 | fi 111 | done <<<"$OTHER_XCASSETS" 112 | 113 | if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then 114 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 115 | else 116 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist" 117 | fi 118 | fi 119 | --------------------------------------------------------------------------------