Compare commits

..

34 Commits

Author SHA1 Message Date
Shadowfacts 80c79ded3b Bump build number and update changelog, fix building weburl 2022-02-16 22:11:24 -05:00
Shadowfacts 126b0ae90a Extend disk cache expiry times
The cache keys are URLs, and Mastodon changes the url if the a new image is uploaded for avatar/header
2022-02-06 14:36:01 -05:00
Shadowfacts d6a847bfcc Use background image preparation apis on iOS 15
Closes #128
2022-02-06 10:24:48 -05:00
Shadowfacts 9b33059089 Fix crash when ProfileHeaderView leaks 2022-02-06 10:20:06 -05:00
Shadowfacts 804fdb439d Fix offscreen row pruning removing all rows from profile statuses 2022-02-06 10:19:38 -05:00
Shadowfacts 6ba5f70615 Fix pinned statuses from foreign instances not showing on Mastodon 2022-02-03 23:16:31 -05:00
Shadowfacts 54c01be7ff Use WebURL for more lenient parsing of external URLs
Fixes #136
2022-02-03 23:11:29 -05:00
Shadowfacts 6e964ff601 Profile directory can have a little shadow, as a treat 2022-01-25 21:34:41 -05:00
Shadowfacts 73d33ae730 Fix pleroma not being detected 2022-01-25 21:34:41 -05:00
Shadowfacts 434d975767 Fix crash when ownInstanceLoaded callback is called multiple times 2022-01-25 21:34:41 -05:00
Shadowfacts 41a31c23b7 Allow posting local-only from Glitch instances
See #130
2022-01-24 22:49:51 -05:00
Shadowfacts 02461ad46c Support local only posts on Hometown
Closes #130
2022-01-23 23:45:46 -05:00
Shadowfacts 072e68e97b Add nodeinfo request and InstanceFeatures 2022-01-23 23:26:49 -05:00
Shadowfacts 6879acbe02 Add local-only post icon 2022-01-23 23:22:34 -05:00
Shadowfacts ace503ad3d Use username on compose screen when there is no display name 2022-01-23 11:06:23 -05:00
Shadowfacts e12a82b476 Show local only posts on hometown instances
#130
2022-01-23 10:58:36 -05:00
Shadowfacts 51cb7c3edf Store local only post data 2022-01-23 10:57:32 -05:00
Shadowfacts 2198e2bf3e Allow development against local instances with self-signed certificates 2022-01-23 10:56:36 -05:00
Shadowfacts 6138fc7748 Add select more photos option to asset picker 2022-01-23 10:55:07 -05:00
Shadowfacts dc1eb3d6f0 Remove old code 2022-01-21 11:13:47 -05:00
Shadowfacts fa1482a152 Fix crash when fetching attachment data fails 2022-01-21 11:10:03 -05:00
Shadowfacts e65ed3e773 Fix crash when ProfileHeaderView leaks 2022-01-21 11:09:55 -05:00
Shadowfacts eca7f31e82 Use stringsdict for favorites/reblogs count 2021-11-25 12:38:05 -05:00
Shadowfacts 2b22180191 Remove TimelineLikeTableViewController
Everything now uses DiffableTimelineLike
2021-11-25 12:29:35 -05:00
Shadowfacts 654b5d9c59 Convert ProfileStatusesViewController to DiffableTimelineLike 2021-11-25 12:27:59 -05:00
Shadowfacts 777d1f378c Fix hashtag history view background being opaque 2021-11-24 15:15:34 -05:00
Shadowfacts 3b132ab4dc Enable context menus and drag and drop for trending hashtags 2021-11-24 15:12:25 -05:00
Shadowfacts d1083116e0 Use a single disptach queue for attachment/card thumbnails 2021-11-24 15:02:35 -05:00
Shadowfacts 7b79cec0ed Remove old comments 2021-11-22 23:41:06 -05:00
Shadowfacts 50cbbb86fc Fix instance selector activity indicator background color 2021-11-22 23:23:52 -05:00
Shadowfacts 5a914ea5a3 Don't show Mute action when not applicable to status 2021-11-22 23:23:19 -05:00
Shadowfacts ca5ac8b826 Fix crash due to leaked ProfileHeaderView not having a
mastodonController
2021-11-22 21:38:00 -05:00
Shadowfacts 2b50609e5c Fix animating poll configuration button size change when selected option
changes
2021-11-20 11:37:09 -05:00
Shadowfacts 57cb0614a9 Fix keyboard getting dismissed when scrolling autocomplete suggestions
Presentation controller takes care of dismissing keyboard when swipe
down in main scroll view starts
2021-11-20 11:28:37 -05:00
65 changed files with 1689 additions and 846 deletions

View File

@ -1,5 +1,20 @@
# Changelog
## 2022.1 (24)
Features/Improvements:
- Local only posts (Glitch/Hometown)
- Show indicator for local only posts
- Add local only option to Compose screen
- Add extend selection button to asset picker when the limited library was used
- Improve profile directory UI
- Improve scrolling performance with large attachments on older devices
Bugfixes:
- Fix crash when closing Preferences
- Fix crash when posting attachment fails
- Fix scrolling through compose autocomplete suggestions dismissing keyboard
- Only show Mute action on your own posts
## 2021.1 (23)
Features/Improvements:
- Synchronize GIF playback through animations and in gallery

View File

@ -109,7 +109,9 @@ public class Client {
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
urlRequest.httpMethod = request.method.name
urlRequest.httpBody = request.body.data
urlRequest.setValue(request.body.mimeType, forHTTPHeaderField: "Content-Type")
if let mimeType = request.body.mimeType {
urlRequest.setValue(mimeType, forHTTPHeaderField: "Content-Type")
}
if let accessToken = accessToken {
urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
@ -150,6 +152,24 @@ public class Client {
}
}
public func nodeInfo(completion: @escaping Callback<NodeInfo>) {
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
run(wellKnown) { result in
switch result {
case let .failure(error):
completion(.failure(error))
case let .success(wellKnown, _):
if let url = wellKnown.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
let components = URLComponents(string: url.href),
components.host == self.baseURL.host {
let nodeInfo = Request<NodeInfo>(method: .get, path: components.path)
self.run(nodeInfo, completion: completion)
}
}
}
}
// MARK: - Self
public static func getSelfAccount() -> Request<Account> {
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
@ -315,7 +335,8 @@ public class Client {
language: String? = nil,
pollOptions: [String]? = nil,
pollExpiresIn: Int? = nil,
pollMultiple: Bool? = nil) -> Request<Status> {
pollMultiple: Bool? = nil,
localOnly: Bool? = nil /* hometown only, not glitch */) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([
"status" => text,
"content_type" => contentType.mimeType,
@ -326,6 +347,7 @@ public class Client {
"language" => language,
"poll[expires_in]" => pollExpiresIn,
"poll[multiple]" => pollMultiple,
"local_only" => localOnly,
] + "media_ids" => media?.map { $0.id } + "poll[options]" => pollOptions))
}

View File

@ -14,7 +14,6 @@ public class Attachment: Codable {
public let url: URL
public let remoteURL: URL?
public let previewURL: URL?
public let textURL: URL?
public let meta: Metadata?
public let description: String?
public let blurHash: String?
@ -33,7 +32,6 @@ public class Attachment: Codable {
self.url = try container.decode(URL.self, forKey: .url)
self.previewURL = try? container.decode(URL?.self, forKey: .previewURL)
self.remoteURL = try? container.decode(URL?.self, forKey: .remoteURL)
self.textURL = try? container.decode(URL?.self, forKey: .textURL)
self.meta = try? container.decode(Metadata?.self, forKey: .meta)
self.description = try? container.decode(String?.self, forKey: .description)
self.blurHash = try? container.decode(String?.self, forKey: .blurHash)
@ -45,7 +43,6 @@ public class Attachment: Codable {
case url
case remoteURL = "remote_url"
case previewURL = "preview_url"
case textURL = "text_url"
case meta
case description
case blurHash = "blurhash"

View File

@ -7,17 +7,18 @@
//
import Foundation
import WebURL
public class Card: Codable {
public let url: URL
public let url: WebURL
public let title: String
public let description: String
public let image: URL?
public let image: WebURL?
public let kind: Kind
public let authorName: String?
public let authorURL: URL?
public let authorURL: WebURL?
public let providerName: String?
public let providerURL: URL?
public let providerURL: WebURL?
public let html: String?
public let width: Int?
public let height: Int?
@ -26,15 +27,15 @@ public class Card: Codable {
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.url = try container.decode(URL.self, forKey: .url)
self.url = try container.decode(WebURL.self, forKey: .url)
self.title = try container.decode(String.self, forKey: .title)
self.description = try container.decode(String.self, forKey: .description)
self.kind = try container.decode(Kind.self, forKey: .kind)
self.image = try? container.decodeIfPresent(URL.self, forKey: .image)
self.image = try? container.decodeIfPresent(WebURL.self, forKey: .image)
self.authorName = try? container.decodeIfPresent(String.self, forKey: .authorName)
self.authorURL = try? container.decodeIfPresent(URL.self, forKey: .authorURL)
self.authorURL = try? container.decodeIfPresent(WebURL.self, forKey: .authorURL)
self.providerName = try? container.decodeIfPresent(String.self, forKey: .providerName)
self.providerURL = try? container.decodeIfPresent(URL.self, forKey: .providerURL)
self.providerURL = try? container.decodeIfPresent(WebURL.self, forKey: .providerURL)
self.html = try? container.decodeIfPresent(String.self, forKey: .html)
self.width = try? container.decodeIfPresent(Int.self, forKey: .width)
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)

View File

@ -7,29 +7,22 @@
//
import Foundation
import WebURL
public class Emoji: Codable {
public let shortcode: String
public let url: URL
public let staticURL: URL
// these shouldn't need to be WebURLs as they're not external resources,
// but some instances (pleroma?) has emoji urls that Foundation considers malformed so we use WebURL to be more lenient
public let url: WebURL
public let staticURL: WebURL
public let visibleInPicker: Bool
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.shortcode = try container.decode(String.self, forKey: .shortcode)
if let url = try? container.decode(URL.self, forKey: .url) {
self.url = url
} else {
let str = try container.decode(String.self, forKey: .url)
self.url = URL(string: str.replacingOccurrences(of: " ", with: "%20"))!
}
if let url = try? container.decode(URL.self, forKey: .staticURL) {
self.staticURL = url
} else {
let staticStr = try container.decode(String.self, forKey: .staticURL)
self.staticURL = URL(string: staticStr.replacingOccurrences(of: " ", with: "%20"))!
}
self.url = try container.decode(WebURL.self, forKey: .url)
self.staticURL = try container.decode(WebURL.self, forKey: .staticURL)
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
}

View File

@ -7,11 +7,13 @@
//
import Foundation
import WebURL
public class Mention: Codable {
public let url: URL
public let url: WebURL
public let username: String
public let acct: String
/// The instance-local ID of the user being mentioned.
public let id: String
private enum CodingKeys: String, CodingKey {

View File

@ -0,0 +1,19 @@
//
// NodeInfo.swift
// Pachyderm
//
// Created by Shadowfacts on 1/22/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
public struct NodeInfo: Decodable {
public let version: String
public let software: Software
public struct Software: Decodable {
public let name: String
public let version: String
}
}

View File

@ -38,6 +38,8 @@ public final class Status: /*StatusProtocol,*/ Decodable {
public let bookmarked: Bool?
public let card: Card?
public let poll: Poll?
// Hometown, Glitch only
public let localOnly: Bool?
public var applicationName: String? { application?.name }
@ -134,6 +136,7 @@ public final class Status: /*StatusProtocol,*/ Decodable {
case bookmarked
case card
case poll
case localOnly = "local_only"
}
}

View File

@ -0,0 +1,18 @@
//
// WellKnown.swift
// Pachyderm
//
// Created by Shadowfacts on 1/22/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
struct WellKnown: Decodable {
let links: [Link]
struct Link: Decodable {
let href: String
let rel: String
}
}

View File

@ -1,24 +0,0 @@
//
// InstanceType.swift
// Pachyderm
//
// Created by Shadowfacts on 9/11/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
public enum InstanceType {
case mastodon, pleroma
}
public extension Instance {
var instanceType: InstanceType {
let lowercased = version.lowercased()
if lowercased.contains("pleroma") {
return .pleroma
} else {
return .mastodon
}
}
}

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -90,7 +90,6 @@
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A542263634100095BD04 /* PollOptionCheckboxView.swift */; };
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */; };
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; };
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; };
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */; };
@ -117,6 +116,11 @@
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
D62E9981279C691F00C26176 /* NodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9980279C691F00C26176 /* NodeInfo.swift */; };
D62E9983279C69D400C26176 /* WellKnown.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9982279C69D400C26176 /* WellKnown.swift */; };
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9984279CA23900C26176 /* URLSession+Development.swift */; };
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */; };
D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9988279DB2D100C26176 /* InstanceFeatures.swift */; };
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
@ -154,7 +158,6 @@
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */; };
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */; };
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
@ -173,8 +176,8 @@
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626321360D2300C9CBA2 /* AvatarStyle.swift */; };
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */; };
D667383C23299340000A2373 /* InstanceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667383B23299340000A2373 /* InstanceType.swift */; };
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */; };
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; };
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */; };
@ -204,6 +207,7 @@
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; };
@ -335,6 +339,7 @@
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */; };
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
D6F1F9DF27B0613300CB7D88 /* WebURL in Frameworks */ = {isa = PBXBuildFile; productRef = D6F1F9DE27B0613300CB7D88 /* WebURL */; settings = {ATTRIBUTES = (Required, ); }; };
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; };
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
@ -496,7 +501,6 @@
D623A5402635FB3C0095BD04 /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionCheckboxView.swift; sourceTree = "<group>"; };
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = "<group>"; };
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; sourceTree = "<group>"; };
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = "<group>"; };
D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = "<group>"; };
D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AllPhotosTableViewCell.xib; sourceTree = "<group>"; };
@ -523,6 +527,11 @@
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
D62E9980279C691F00C26176 /* NodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfo.swift; sourceTree = "<group>"; };
D62E9982279C69D400C26176 /* WellKnown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellKnown.swift; sourceTree = "<group>"; };
D62E9984279CA23900C26176 /* URLSession+Development.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Development.swift"; sourceTree = "<group>"; };
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
D62E9988279DB2D100C26176 /* InstanceFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceFeatures.swift; sourceTree = "<group>"; };
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = "<group>"; };
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
@ -559,7 +568,6 @@
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeTableViewController.swift; sourceTree = "<group>"; };
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
@ -582,7 +590,6 @@
D663626321360D2300C9CBA2 /* AvatarStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStyle.swift; sourceTree = "<group>"; };
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Visibility+Helpers.swift"; sourceTree = "<group>"; };
D667383B23299340000A2373 /* InstanceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceType.swift; sourceTree = "<group>"; };
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimelineStatusTableViewCell.xib; sourceTree = "<group>"; };
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = "<group>"; };
@ -613,6 +620,7 @@
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
@ -760,6 +768,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D6F1F9DF27B0613300CB7D88 /* WebURL in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -779,6 +788,7 @@
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -903,6 +913,7 @@
D61099F02145686D00432DC2 /* List.swift */,
D6109A062145756700432DC2 /* LoginSettings.swift */,
D61099F22145688600432DC2 /* Mention.swift */,
D62E9980279C691F00C26176 /* NodeInfo.swift */,
D61099F4214568C300432DC2 /* Notification.swift */,
D623A53E2635F6910095BD04 /* Poll.swift */,
D61099F62145693500432DC2 /* PushSubscription.swift */,
@ -914,6 +925,7 @@
D61099FE21456A4C00432DC2 /* Status.swift */,
D6285B4E21EA695800FE4B39 /* StatusContentType.swift */,
D6109A10214607D500432DC2 /* Timeline.swift */,
D62E9982279C69D400C26176 /* WellKnown.swift */,
);
path = Model;
sourceTree = "<group>";
@ -976,11 +988,11 @@
children = (
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */,
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */,
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */,
D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */,
D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */,
D626493A23C1000300612E6E /* AlbumTableViewCell.swift */,
D626493B23C1000300612E6E /* AlbumTableViewCell.xib */,
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */,
);
path = "Asset Picker";
sourceTree = "<group>";
@ -1222,6 +1234,7 @@
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */,
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */,
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */,
);
path = Status;
sourceTree = "<group>";
@ -1322,6 +1335,7 @@
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
D62E9984279CA23900C26176 /* URLSession+Development.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1362,7 +1376,6 @@
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */,
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */,
D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */,
D667383B23299340000A2373 /* InstanceType.swift */,
D61AC1D2232E928600C54D2D /* InstanceSelector.swift */,
);
path = Utilities;
@ -1523,7 +1536,6 @@
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
@ -1587,6 +1599,7 @@
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
D62E9988279DB2D100C26176 /* InstanceFeatures.swift */,
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
D6AEBB3F2321640F00E5038B /* Activities */,
D6F1F84E2193B9BE00F5FE67 /* Caching */,
@ -1711,6 +1724,9 @@
dependencies = (
);
name = Pachyderm;
packageProductDependencies = (
D6F1F9DE27B0613300CB7D88 /* WebURL */,
);
productName = Pachyderm;
productReference = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */;
productType = "com.apple.product-type.framework";
@ -1744,6 +1760,7 @@
D6F953E52125197500CF0F2B /* Embed Frameworks */,
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
D6E3438F2659849800C4AA01 /* Embed App Extensions */,
D6F1F9E127B0677000CB7D88 /* ShellScript */,
);
buildRules = (
);
@ -1756,6 +1773,7 @@
D6B0539E23BD2BA300A066FA /* SheetController */,
D69CCBBE249E6EFD000AF167 /* CrashReporter */,
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
);
productName = Tusker;
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
@ -1873,6 +1891,7 @@
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */,
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
);
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
projectDirPath = "";
@ -1926,7 +1945,6 @@
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */,
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */,
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
@ -1988,6 +2006,24 @@
shellPath = /bin/sh;
shellScript = "#if [ \"${CONFIGURATION}\" == \"Debug\" ]; then\n# echo \"Embedding ${SCRIPT_INPUT_FILE_0}\"\n# cp -R $SCRIPT_INPUT_FILE_0 $SCRIPT_OUTPUT_FILE_0\n# codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_0\n# \n# echo \"Embedding ${SCRIPT_INPUT_FILE_1}\"\n# cp -R $SCRIPT_INPUT_FILE_1 $SCRIPT_OUTPUT_FILE_1\n# codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_1\n#else\n# echo \"Skipping embedding debug frameworks\"\n#fi\n";
};
D6F1F9E127B0677000CB7D88 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\n# the nested framework doens't get signed automatically for some reason\n# so we sign it ourselves\n#codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY \"$BUILT_PRODUCTS_DIR/Tusker.app/Frameworks/Pachyderm.framework/Frameworks/WebURL.framework\"\n\n# xcode wants to include the weburl framework twice for some reason, but the app store doesn't like nested frameworks\n# we already have a copy in the app's Frameworks/ dir, so we can delete the nested one\nif [ \"$(ls \"$BUILT_PRODUCTS_DIR/Tusker.app/Frameworks/Pachyderm.framework/Frameworks/\")\" -ne \"WebURL.framework\" ]; then\n exit 1\nfi\nrm -rf \"$BUILT_PRODUCTS_DIR/Tusker.app/Frameworks/Pachyderm.framework/Frameworks/\"\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -1996,7 +2032,7 @@
buildActionMask = 2147483647;
files = (
D61099E5214561AB00432DC2 /* Application.swift in Sources */,
D667383C23299340000A2373 /* InstanceType.swift in Sources */,
D62E9983279C69D400C26176 /* WellKnown.swift in Sources */,
D61099FF21456A4C00432DC2 /* Status.swift in Sources */,
D61099E32144C38900432DC2 /* Emoji.swift in Sources */,
D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */,
@ -2024,6 +2060,7 @@
D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */,
D6109A0921458C4A00432DC2 /* Empty.swift in Sources */,
D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */,
D62E9981279C691F00C26176 /* NodeInfo.swift in Sources */,
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */,
D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */,
D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */,
@ -2078,9 +2115,9 @@
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
@ -2100,6 +2137,7 @@
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
@ -2180,6 +2218,7 @@
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */,
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
@ -2256,6 +2295,7 @@
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
@ -2390,11 +2430,11 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW;
DEVELOPMENT_TEAM = V4WK9KR9U2;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -2406,6 +2446,8 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.Pachyderm;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
@ -2421,11 +2463,11 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW;
DEVELOPMENT_TEAM = V4WK9KR9U2;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -2437,6 +2479,8 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.Pachyderm;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
@ -2617,7 +2661,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2626,7 +2670,8 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2021.1;
MARKETING_VERSION = 2022.1;
OTHER_CODE_SIGN_FLAGS = "";
OTHER_LDFLAGS = "";
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
@ -2647,7 +2692,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2656,7 +2701,8 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2021.1;
MARKETING_VERSION = 2022.1;
OTHER_CODE_SIGN_FLAGS = "";
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -2756,7 +2802,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2766,7 +2812,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2021.1;
MARKETING_VERSION = 2022.1;
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -2783,7 +2829,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2793,7 +2839,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2021.1;
MARKETING_VERSION = 2022.1;
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -2880,6 +2926,14 @@
minimumVersion = 2.3.2;
};
};
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/karwa/swift-url";
requirement = {
branch = main;
kind = branch;
};
};
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/microsoft/plcrashreporter";
@ -2904,6 +2958,11 @@
package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup;
};
D6676CA427A8D0020052936B /* WebURLFoundationExtras */ = {
isa = XCSwiftPackageProductDependency;
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
productName = WebURLFoundationExtras;
};
D69CCBBE249E6EFD000AF167 /* CrashReporter */ = {
isa = XCSwiftPackageProductDependency;
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
@ -2914,6 +2973,11 @@
package = D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */;
productName = SheetController;
};
D6F1F9DE27B0613300CB7D88 /* WebURL */ = {
isa = XCSwiftPackageProductDependency;
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
productName = WebURL;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View File

@ -99,6 +99,11 @@
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "DEBUG_BLUR_HASH"
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction

View File

@ -2,7 +2,7 @@
"object": {
"pins": [
{
"package": "plcrashreporter",
"package": "PLCrashReporter",
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
"state": {
"branch": null,
@ -19,6 +19,24 @@
"version": null
}
},
{
"package": "swift-system",
"repositoryURL": "https://github.com/apple/swift-system.git",
"state": {
"branch": null,
"revision": "836bc4557b74fe6d2660218d56e3ce96aff76574",
"version": "1.1.1"
}
},
{
"package": "swift-url",
"repositoryURL": "https://github.com/karwa/swift-url",
"state": {
"branch": "main",
"revision": "9d06f9f89397de16c8942aa123c425568654fd6a",
"version": null
}
},
{
"package": "SwiftSoup",
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",

View File

@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}

View File

@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "link.broken 3.svg",
"idiom" : "universal"
}
]
}

View File

@ -0,0 +1,527 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--Generator: Apple Native CoreSVG 175-->
<svg
version="1.1"
width="3300"
height="2200"
id="svg5705"
sodipodi:docname="link.broken 3.svg"
inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5709" />
<sodipodi:namedview
id="namedview5707"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="0.46411918"
inkscape:cx="2761.1443"
inkscape:cy="1328.3226"
inkscape:window-width="2560"
inkscape:window-height="1387"
inkscape:window-x="1728"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg5705" />
<!--glyph: "uni100263.medium", point size: 100.0, font version: "17.1d1e1", template writer version: "65.1"-->
<g
id="Notes">
<rect
height="2200"
id="artboard"
style="fill:white;opacity:1"
width="3300"
x="0"
y="0" />
<line
style="fill:none;stroke:black;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="292"
y2="292"
id="line5518" />
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;"
transform="matrix(1 0 0 1 263 322)"
id="text5520">Weight/Scale Variations</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;"
transform="matrix(1 0 0 1 559.711 322)"
id="text5522">Ultralight</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;"
transform="matrix(1 0 0 1 856.422 322)"
id="text5524">Thin</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;"
transform="matrix(1 0 0 1 1153.13 322)"
id="text5526">Light</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;"
transform="matrix(1 0 0 1 1449.84 322)"
id="text5528">Regular</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;"
transform="matrix(1 0 0 1 1746.56 322)"
id="text5530">Medium</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;"
transform="matrix(1 0 0 1 2043.27 322)"
id="text5532">Semibold</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;"
transform="matrix(1 0 0 1 2339.98 322)"
id="text5534">Bold</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;"
transform="matrix(1 0 0 1 2636.69 322)"
id="text5536">Heavy</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;"
transform="matrix(1 0 0 1 2933.4 322)"
id="text5538">Black</text>
<line
style="fill:none;stroke:black;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="1903"
y2="1903"
id="line5540" />
<g
transform="matrix(1 0 0 1 263 1933)"
id="g5544">
<path
d="M9.24805 0.830078Q10.8691 0.830078 12.2949 0.214844Q13.7207-0.400391 14.8096-1.49414Q15.8984-2.58789 16.5186-4.01367Q17.1387-5.43945 17.1387-7.05078Q17.1387-8.66211 16.5186-10.0879Q15.8984-11.5137 14.8047-12.6074Q13.7109-13.7012 12.2852-14.3164Q10.8594-14.9316 9.23828-14.9316Q7.62695-14.9316 6.20117-14.3164Q4.77539-13.7012 3.69141-12.6074Q2.60742-11.5137 1.9873-10.0879Q1.36719-8.66211 1.36719-7.05078Q1.36719-5.43945 1.9873-4.01367Q2.60742-2.58789 3.69629-1.49414Q4.78516-0.400391 6.21094 0.214844Q7.63672 0.830078 9.24805 0.830078ZM9.24805-0.654297Q7.91992-0.654297 6.7627-1.14746Q5.60547-1.64062 4.73145-2.51953Q3.85742-3.39844 3.36426-4.56055Q2.87109-5.72266 2.87109-7.05078Q2.87109-8.37891 3.35938-9.54102Q3.84766-10.7031 4.72168-11.582Q5.5957-12.4609 6.75293-12.9541Q7.91016-13.4473 9.23828-13.4473Q10.5762-13.4473 11.7334-12.9541Q12.8906-12.4609 13.7695-11.582Q14.6484-10.7031 15.1465-9.54102Q15.6445-8.37891 15.6445-7.05078Q15.6445-5.72266 15.1514-4.56055Q14.6582-3.39844 13.7842-2.51953Q12.9102-1.64062 11.748-1.14746Q10.5859-0.654297 9.24805-0.654297ZM5.6543-7.05078Q5.6543-6.72852 5.85938-6.52832Q6.06445-6.32812 6.40625-6.32812L8.51562-6.32812L8.51562-4.20898Q8.51562-3.88672 8.71094-3.67676Q8.90625-3.4668 9.23828-3.4668Q9.56055-3.4668 9.77051-3.67676Q9.98047-3.88672 9.98047-4.20898L9.98047-6.32812L12.0898-6.32812Q12.4219-6.32812 12.627-6.52832Q12.832-6.72852 12.832-7.05078Q12.832-7.38281 12.627-7.58789Q12.4219-7.79297 12.0898-7.79297L9.98047-7.79297L9.98047-9.90234Q9.98047-10.2344 9.77051-10.4443Q9.56055-10.6543 9.23828-10.6543Q8.90625-10.6543 8.71094-10.4443Q8.51562-10.2344 8.51562-9.90234L8.51562-7.79297L6.40625-7.79297Q6.06445-7.79297 5.85938-7.58789Q5.6543-7.38281 5.6543-7.05078Z"
id="path5542" />
</g>
<g
transform="matrix(1 0 0 1 281.867 1933)"
id="g5548">
<path
d="M11.709 2.91016Q13.75 2.91016 15.5518 2.12891Q17.3535 1.34766 18.7305-0.0292969Q20.1074-1.40625 20.8887-3.20801Q21.6699-5.00977 21.6699-7.05078Q21.6699-9.0918 20.8887-10.8936Q20.1074-12.6953 18.7305-14.0723Q17.3535-15.4492 15.5469-16.2305Q13.7402-17.0117 11.6992-17.0117Q9.6582-17.0117 7.85645-16.2305Q6.05469-15.4492 4.68262-14.0723Q3.31055-12.6953 2.5293-10.8936Q1.74805-9.0918 1.74805-7.05078Q1.74805-5.00977 2.5293-3.20801Q3.31055-1.40625 4.6875-0.0292969Q6.06445 1.34766 7.86621 2.12891Q9.66797 2.91016 11.709 2.91016ZM11.709 1.25Q9.98047 1.25 8.47656 0.605469Q6.97266-0.0390625 5.83496-1.17676Q4.69727-2.31445 4.05762-3.81836Q3.41797-5.32227 3.41797-7.05078Q3.41797-8.7793 4.05762-10.2832Q4.69727-11.7871 5.83008-12.9297Q6.96289-14.0723 8.4668-14.7119Q9.9707-15.3516 11.6992-15.3516Q13.4277-15.3516 14.9316-14.7119Q16.4355-14.0723 17.5781-12.9297Q18.7207-11.7871 19.3652-10.2832Q20.0098-8.7793 20.0098-7.05078Q20.0098-5.32227 19.3701-3.81836Q18.7305-2.31445 17.5928-1.17676Q16.4551-0.0390625 14.9463 0.605469Q13.4375 1.25 11.709 1.25ZM7.17773-7.05078Q7.17773-6.69922 7.41211-6.47461Q7.64648-6.25 8.01758-6.25L10.8887-6.25L10.8887-3.36914Q10.8887-2.99805 11.1133-2.76855Q11.3379-2.53906 11.6895-2.53906Q12.0605-2.53906 12.29-2.76855Q12.5195-2.99805 12.5195-3.36914L12.5195-6.25L15.4004-6.25Q15.7715-6.25 16.001-6.47461Q16.2305-6.69922 16.2305-7.05078Q16.2305-7.42188 16.001-7.65137Q15.7715-7.88086 15.4004-7.88086L12.5195-7.88086L12.5195-10.752Q12.5195-11.1328 12.29-11.3623Q12.0605-11.5918 11.6895-11.5918Q11.3379-11.5918 11.1133-11.3574Q10.8887-11.123 10.8887-10.752L10.8887-7.88086L8.01758-7.88086Q7.63672-7.88086 7.40723-7.65137Q7.17773-7.42188 7.17773-7.05078Z"
id="path5546" />
</g>
<g
transform="matrix(1 0 0 1 305.646 1933)"
id="g5552">
<path
d="M14.9707 5.66406Q17.0605 5.66406 18.96 5.01465Q20.8594 4.36523 22.4512 3.19336Q24.043 2.02148 25.2197 0.429688Q26.3965-1.16211 27.0459-3.06641Q27.6953-4.9707 27.6953-7.05078Q27.6953-9.14062 27.0459-11.04Q26.3965-12.9395 25.2197-14.5312Q24.043-16.123 22.4512-17.2998Q20.8594-18.4766 18.9551-19.126Q17.0508-19.7754 14.9609-19.7754Q12.8711-19.7754 10.9717-19.126Q9.07227-18.4766 7.48535-17.2998Q5.89844-16.123 4.72168-14.5312Q3.54492-12.9395 2.90039-11.04Q2.25586-9.14062 2.25586-7.05078Q2.25586-4.9707 2.90527-3.06641Q3.55469-1.16211 4.72656 0.429688Q5.89844 2.02148 7.49023 3.19336Q9.08203 4.36523 10.9814 5.01465Q12.8809 5.66406 14.9707 5.66406ZM14.9707 3.84766Q13.1641 3.84766 11.5283 3.2959Q9.89258 2.74414 8.53516 1.74805Q7.17773 0.751953 6.17676-0.610352Q5.17578-1.97266 4.62891-3.6084Q4.08203-5.24414 4.08203-7.05078Q4.08203-8.86719 4.62891-10.5029Q5.17578-12.1387 6.17188-13.501Q7.16797-14.8633 8.52539-15.8594Q9.88281-16.8555 11.5186-17.4023Q13.1543-17.9492 14.9609-17.9492Q16.7773-17.9492 18.4131-17.4023Q20.0488-16.8555 21.4111-15.8594Q22.7734-14.8633 23.7695-13.501Q24.7656-12.1387 25.3174-10.5029Q25.8691-8.86719 25.8691-7.05078Q25.8789-5.24414 25.332-3.6084Q24.7852-1.97266 23.7842-0.610352Q22.7832 0.751953 21.4209 1.74805Q20.0586 2.74414 18.4229 3.2959Q16.7871 3.84766 14.9707 3.84766ZM9.19922-7.05078Q9.19922-6.66992 9.45801-6.4209Q9.7168-6.17188 10.1172-6.17188L14.0625-6.17188L14.0625-2.2168Q14.0625-1.81641 14.3115-1.55762Q14.5605-1.29883 14.9512-1.29883Q15.3516-1.29883 15.6104-1.55273Q15.8691-1.80664 15.8691-2.2168L15.8691-6.17188L19.8242-6.17188Q20.2246-6.17188 20.4785-6.4209Q20.7324-6.66992 20.7324-7.05078Q20.7324-7.46094 20.4785-7.71484Q20.2246-7.96875 19.8242-7.96875L15.8691-7.96875L15.8691-11.9141Q15.8691-12.3242 15.6104-12.583Q15.3516-12.8418 14.9512-12.8418Q14.5605-12.8418 14.3115-12.583Q14.0625-12.3242 14.0625-11.9141L14.0625-7.96875L10.1172-7.96875Q9.70703-7.96875 9.45312-7.71484Q9.19922-7.46094 9.19922-7.05078Z"
id="path5550" />
</g>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;"
transform="matrix(1 0 0 1 263 1953)"
id="text5554">Design Variations</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1971)"
id="text5556">Symbols are supported in up to nine weights and three scales.</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1989)"
id="text5558">For optimal layout with text and other symbols, vertically align</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 2007)"
id="text5560">symbols with the adjacent text.</text>
<line
style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
x1="776"
x2="776"
y1="1919"
y2="1933"
id="line5562" />
<g
transform="matrix(1 0 0 1 776 1933)"
id="g5566">
<path
d="M3.31055 0.15625Q3.70117 0.15625 3.91602-0.00976562Q4.13086-0.175781 4.26758-0.585938L5.52734-4.0332L11.2891-4.0332L12.5488-0.585938Q12.6855-0.175781 12.9004-0.00976562Q13.1152 0.15625 13.4961 0.15625Q13.8867 0.15625 14.1162-0.0585938Q14.3457-0.273438 14.3457-0.644531Q14.3457-0.869141 14.2383-1.17188L9.6582-13.3691Q9.48242-13.8184 9.17969-14.043Q8.87695-14.2676 8.4082-14.2676Q7.5-14.2676 7.17773-13.3789L2.59766-1.16211Q2.49023-0.859375 2.49023-0.634766Q2.49023-0.263672 2.70996-0.0537109Q2.92969 0.15625 3.31055 0.15625ZM6.00586-5.51758L8.37891-12.0898L8.42773-12.0898L10.8008-5.51758Z"
id="path5564" />
</g>
<line
style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
x1="793.197"
x2="793.197"
y1="1919"
y2="1933"
id="line5568" />
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;"
transform="matrix(1 0 0 1 776 1953)"
id="text5570">Margins</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 776 1971)"
id="text5572">Leading and trailing margins on the left and right side of each symbol</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 776 1989)"
id="text5574">can be adjusted by modifying the x-location of the margin guidelines.</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 776 2007)"
id="text5576">Modifications are automatically applied proportionally to all</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 776 2025)"
id="text5578">scales and weights.</text>
<g
transform="matrix(1 0 0 1 1289 1933)"
id="g5582">
<path
d="M2.8418 1.86523L4.54102 3.57422Q5.18555 4.22852 5.90332 4.17969Q6.62109 4.13086 7.31445 3.35938L18.0078-8.42773L17.041-9.4043L6.42578 2.27539Q6.16211 2.57812 5.89355 2.61719Q5.625 2.65625 5.27344 2.30469L4.10156 1.14258Q3.75 0.791016 3.79395 0.522461Q3.83789 0.253906 4.14062-0.0195312L15.6152-10.8203L14.6387-11.7871L3.04688-0.898438Q2.30469-0.214844 2.25098 0.498047Q2.19727 1.21094 2.8418 1.86523ZM9.25781-16.3281Q8.94531-16.0254 8.90625-15.6348Q8.86719-15.2441 9.04297-14.9512Q9.21875-14.6777 9.55566-14.541Q9.89258-14.4043 10.3809-14.5215Q11.4746-14.7754 12.5977-14.7314Q13.7207-14.6875 14.7949-13.9844L14.209-12.5293Q13.9551-11.9043 14.0674-11.4404Q14.1797-10.9766 14.5801-10.5664L16.875-8.25195Q17.2363-7.88086 17.5781-7.82227Q17.9199-7.76367 18.3398-7.8418L19.4043-8.03711L20.0684-7.36328L20.0293-6.80664Q20-6.43555 20.1221-6.12305Q20.2441-5.81055 20.6055-5.44922L21.3672-4.70703Q21.7285-4.3457 22.1533-4.33105Q22.5781-4.31641 22.9297-4.66797L25.8398-7.58789Q26.1914-7.93945 26.1816-8.35449Q26.1719-8.76953 25.8105-9.13086L25.0391-9.89258Q24.6875-10.2539 24.3799-10.3857Q24.0723-10.5176 23.7109-10.4883L23.1348-10.4395L22.4902-11.0742L22.7344-12.1973Q22.832-12.6172 22.6953-12.9834Q22.5586-13.3496 22.1191-13.7891L19.9219-15.9766Q18.6719-17.2168 17.2607-17.8369Q15.8496-18.457 14.4189-18.4814Q12.9883-18.5059 11.665-17.959Q10.3418-17.4121 9.25781-16.3281ZM10.752-15.957Q11.6602-16.6211 12.7002-16.9043Q13.7402-17.1875 14.8047-17.085Q15.8691-16.9824 16.8701-16.5137Q17.8711-16.0449 18.7012-15.2051L21.1328-12.793Q21.3086-12.6172 21.3525-12.4512Q21.3965-12.2852 21.3379-12.0312L21.0156-10.5469L22.5195-9.0625L23.5059-9.12109Q23.6914-9.13086 23.7891-9.09668Q23.8867-9.0625 24.0332-8.91602L24.6094-8.33984L22.168-5.89844L21.5918-6.47461Q21.4453-6.62109 21.4062-6.71875Q21.3672-6.81641 21.377-7.01172L21.4453-7.98828L19.9512-9.47266L18.4277-9.21875Q18.1836-9.16992 18.042-9.2041Q17.9004-9.23828 17.7148-9.41406L15.7129-11.416Q15.5176-11.5918 15.4932-11.7529Q15.4688-11.9141 15.5859-12.1875L16.4648-14.2773Q15.293-15.3711 13.8281-15.791Q12.3633-16.2109 10.8398-15.7617Q10.7227-15.7324 10.6885-15.8057Q10.6543-15.8789 10.752-15.957Z"
id="path5580" />
</g>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;"
transform="matrix(1 0 0 1 1289 1953)"
id="text5584">Exporting</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 1289 1971)"
id="text5586">Symbols should be outlined when exporting to ensure the</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 1289 1989)"
id="text5588">design is preserved when submitting to Xcode.</text>
<text
id="template-version"
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;"
transform="matrix(1 0 0 1 3036 1933)">Template v.3.0</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;"
transform="matrix(1 0 0 1 3036 1951)"
id="text5591">Requires Xcode 13 or greater</text>
<text
id="descriptive-name"
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;"
transform="matrix(1 0 0 1 3036 1969)">Generated from link</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;"
transform="matrix(1 0 0 1 3036 1987)"
id="text5594">Typeset at 100 points</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 726)"
id="text5596">Small</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1156)"
id="text5598">Medium</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1586)"
id="text5600">Large</text>
</g>
<g
id="Guides">
<g
id="H-reference"
style="fill:#27AAE1;stroke:none;"
transform="matrix(1 0 0 1 339 696)">
<path
d="M0.993347 0L3.6377 0L29.3282-67.1326L30.0301-67.1326L30.0301-70.459L28.1227-70.459ZM11.6882-24.4797L46.9818-24.4797L46.2311-26.7288L12.4382-26.7288ZM55.1193 0L57.7637 0L30.6381-70.459L29.4327-70.459L29.4327-67.1326Z"
id="path5603" />
</g>
<line
id="Baseline-S"
style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="696"
y2="696" />
<line
id="Capline-S"
style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="625.541"
y2="625.541" />
<g
id="g5610"
style="fill:#27AAE1;stroke:none;"
transform="matrix(1 0 0 1 339 1126)">
<path
d="M0.993347 0L3.6377 0L29.3282-67.1326L30.0301-67.1326L30.0301-70.459L28.1227-70.459ZM11.6882-24.4797L46.9818-24.4797L46.2311-26.7288L12.4382-26.7288ZM55.1193 0L57.7637 0L30.6381-70.459L29.4327-70.459L29.4327-67.1326Z"
id="path5608" />
</g>
<line
id="Baseline-M"
style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="1126"
y2="1126" />
<line
id="Capline-M"
style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="1055.54"
y2="1055.54" />
<g
id="g5616"
style="fill:#27AAE1;stroke:none;"
transform="matrix(1 0 0 1 339 1556)">
<path
d="M0.993347 0L3.6377 0L29.3282-67.1326L30.0301-67.1326L30.0301-70.459L28.1227-70.459ZM11.6882-24.4797L46.9818-24.4797L46.2311-26.7288L12.4382-26.7288ZM55.1193 0L57.7637 0L30.6381-70.459L29.4327-70.459L29.4327-67.1326Z"
id="path5614" />
</g>
<line
id="Baseline-L"
style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="1556"
y2="1556" />
<line
id="Capline-L"
style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="1485.54"
y2="1485.54" />
<line
id="left-margin-Regular-M"
style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
x1="1386.86"
x2="1386.86"
y1="1030.79"
y2="1150.12" />
<line
id="right-margin-Regular-M"
style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
x1="1512.83"
x2="1512.83"
y1="1030.79"
y2="1150.12" />
</g>
<g
id="Symbols">
<g
id="Black-L"
transform="matrix(1 0 0 1 2847.89 1556)">
<path
d="M 93.47189,-11.283754 72.4455,9.81883 C 69.234833,12.98981 65.7329,15.108233 61.9397,16.1741 58.146433,17.239967 54.3499,17.231033 50.5501,16.1473 46.7503,15.0635 43.265333,12.93694 40.0952,9.76762 36.886133,6.54384 34.736067,3.0320613 33.645,-0.767716 32.553867,-4.567492 32.5315,-8.3737533 33.5779,-12.1865 c 1.0464,-3.812667 3.174967,-7.297933 6.3857,-10.4558 l 21.097708,-20.193405 c 1.798266,-1.798266 -6.50896,0.04511 -9.613447,-3.124452 -2.977888,-3.040312 -0.681815,-11.172348 -2.546608,-9.447414 L 27.0471,-34.5354 c -5.616867,5.525067 -9.349867,11.667033 -11.199,18.4259 -1.849133,6.7588467 -1.830067,13.5180933 0.0572,20.27774 1.887267,6.75964 5.6276,12.929193 11.221,18.50866 5.580267,5.580333 11.750033,9.307667 18.5093,11.182 6.759267,1.874333 13.508567,1.883667 20.2479,0.028 6.7394,-1.855667 12.8911,-5.5655 18.4551,-11.1295 L 106.38969,0.60928382 C 108.55301,-1.6133644 99.80736,0.96679441 96.457782,-1.9468587 93.162967,-4.8128774 95.296133,-13.207618 93.47189,-11.283754 Z M 77.419846,-59.660518 98.5821,-81.0324 c 3.15793,-3.209933 6.6369,-5.3381 10.4369,-6.3845 3.8,-1.0464 7.6,-1.037467 11.4,0.0268 3.8,1.064267 7.304,3.200567 10.512,6.4089 3.21,3.222933 5.35067,6.7345 6.422,10.5347 1.07133,3.8002 1.08033,7.5969 0.027,11.3901 -1.05333,3.793267 -3.159,7.2883 -6.317,10.4851 l -20.19391,19.710529 c -1.67456,1.722407 6.41859,0.04 9.5293,3.214024 3.08031,3.143 1.16211,11.159102 2.8203,9.548285 L 143.981,-36.6782 c 5.60267,-5.525067 9.322,-11.667033 11.158,-18.4259 1.83667,-6.758867 1.81767,-13.5181 -0.057,-20.2777 -1.874,-6.759667 -5.601,-12.929267 -11.181,-18.5088 -5.61933,-5.632933 -11.799,-9.3734 -18.539,-11.2214 -6.74,-1.848 -13.47967,-1.844 -20.219,0.012 -6.7394,1.85533 -12.890733,5.565 -18.454,11.129 l -22.185654,22.417382 c -1.960018,2.299329 6.899115,-0.650503 9.802694,2.197837 3.285076,3.222579 0.920078,12.00044 3.113806,9.695263 z"
id="path5623"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscssccsccsscscccsc" />
</g>
<g
id="Heavy-L"
transform="matrix(1 0 0 1 2552.28 1556)">
<path
d="M 92.940506,-10.441306 72.061,10.3934 C 68.729,13.692 65.087533,15.8939 61.1366,16.9991 57.1856,18.1043 53.2305,18.1006 49.2713,16.988 45.312167,15.8754 41.6852,13.6736 38.3904,10.3826 35.065867,7.0341333 32.841333,3.3803223 31.7168,-0.578833 30.592267,-4.5379843 30.575167,-8.5005067 31.6655,-12.4664 c 1.090333,-3.965867 3.3015,-7.589833 6.6335,-10.8719 l 15.680191,-15.73912 c 2.548115,-2.69494 -6.838919,-1.187794 -8.525525,-2.948406 -1.697409,-1.771889 0.444996,-9.67038 -2.235255,-7.340372 L 26.7382,-33.8896 c -5.4716,5.392 -9.110733,11.381833 -10.9174,17.9695 -1.806667,6.5876867 -1.793667,13.17725 0.039,19.76869 1.832667,6.591407 5.483733,12.611777 10.9532,18.06111 5.453067,5.453067 11.474367,9.0926 18.0639,10.9186 6.5896,1.825933 13.1708,1.8315 19.7436,0.0167 6.572733,-1.814867 12.569967,-5.433167 17.9917,-10.8549 L 104.50701,0.10982646 C 106.61066,-2.0493963 97.41764,-0.0070237 95.480259,-1.7138328 93.443232,-3.508428 95.187966,-12.550828 92.940506,-10.441306 Z M 74.930678,-59.9886 96.7681,-81.6804 c 3.28193,-3.328333 6.90357,-5.537667 10.8649,-6.628 3.96067,-1.090333 7.92067,-1.086633 11.88,0.0111 3.95933,1.097733 7.59933,3.307 10.92,6.6278 3.328,3.344667 5.546,6.997533 6.654,10.9586 1.108,3.961067 1.11167,7.917067 0.011,11.868 -1.1,3.951 -3.291,7.582433 -6.573,10.8943 l -14.6441,14.980051 c -2.34992,2.35828 6.31958,1.544004 8.03088,3.145715 1.71064,1.668977 0.47033,10.394594 2.60826,7.997254 L 142.091,-37.3974 c 5.45133,-5.391933 9.077,-11.381767 10.877,-17.9695 1.8,-6.587667 1.787,-13.1772 -0.039,-19.7686 -1.826,-6.5914 -5.46367,-12.6118 -10.913,-18.0612 -5.48267,-5.502867 -11.51233,-9.15497 -18.089,-10.9563 -6.57667,-1.80067 -13.15133,-1.79367 -19.724,0.021 -6.572733,1.81467 -12.568133,5.432967 -17.9862,10.8549 l -22.846922,22.7373 c -1.949069,1.831959 7.845854,0.132192 9.65871,1.765003 1.869807,1.684103 0.09258,10.666787 1.90209,8.786197 z"
id="path5626"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscsccccccssccsccsc" />
</g>
<g
id="Bold-L"
transform="matrix(1 0 0 1 2256.99 1556)">
<path
d="M 92.098427,-9.3334055 71.5399,11.1005 C 68.0543,14.557833 64.2362,16.864667 60.0856,18.021 55.935,19.1774 51.780267,19.181367 47.6214,18.0329 43.462467,16.884433 39.658767,14.5904 36.2103,11.1508 32.742367,7.6485933 30.425633,3.8180657 29.2601,-0.340783 28.094567,-4.4996277 28.0851,-8.6592667 29.2317,-12.8197 c 1.1466,-4.1604 3.4627,-7.960933 6.9483,-11.4016 l 17.583685,-15.902805 c 3.612696,-4.404625 -4.932631,-1.324342 -6.405621,-2.913311 -1.425759,-1.53802 1.469624,-9.672437 -2.731733,-5.612788 L 26.3392,-33.0681 c -5.282467,5.218133 -8.800133,11.011033 -10.553,17.3787 -1.752867,6.3675933 -1.747533,12.73965 0.016,19.11617 1.763533,6.3764867 5.298233,12.204997 10.6041,17.48553 5.289333,5.289333 11.120067,8.815033 17.4922,10.5771 6.372067,1.762 12.737033,1.762467 19.0949,0.0014 6.357867,-1.761133 12.1556,-5.2605 17.3932,-10.4981 l 21.56563,-21.47960554 c 2.35096,-2.59731306 -6.275451,-0.3224295 -7.872161,-2.03664706 -1.504227,-1.614929 0.14803,-9.0537524 -1.981642,-6.8098529 z M 73.877873,-61.847651 94.4497,-82.4722 c 3.440867,-3.476867 7.24297,-5.7886 11.4063,-6.9352 4.16333,-1.1466 8.32433,-1.150533 12.483,-0.0118 4.15867,1.138667 7.96767,3.4376 11.427,6.8968 3.47667,3.493333 5.79067,7.321667 6.942,11.485 1.152,4.163267 1.14833,8.3202 -0.011,12.4708 -1.15933,4.1506 -3.45967,7.956067 -6.901,11.4164 l -15.99907,16.856363 c -2.33598,2.5194 5.66711,0.340412 7.0183,1.76911 1.42356,1.505209 -0.15666,8.440128 2.29087,5.725408 L 139.651,-38.3037 c 5.25667,-5.218 8.76067,-11.010833 10.512,-17.3785 1.75133,-6.367733 1.746,-12.7398 -0.016,-19.1162 -1.762,-6.376467 -5.28333,-12.205 -10.564,-17.4856 -5.30867,-5.334 -11.14633,-8.871 -17.513,-10.611 -6.36667,-1.73933 -12.729,-1.72833 -19.087,0.033 -6.357867,1.76067 -12.1512,5.259833 -17.38,10.4975 l -21.565927,21.670049 c -2.428644,2.351377 5.932305,0.510192 7.59687,1.982982 1.598944,1.41473 0.06236,9.269414 2.24393,6.863818 z"
id="path5629"
sodipodi:nodetypes="ccsscssccsccsscssccscccssccsccsccsscccccsc" />
</g>
<g
id="Semibold-L"
transform="matrix(1 0 0 1 1961.26 1556)">
<path
d="M 92.214,-9.4267 71.1806,11.5879 c -3.591467,3.5668 -7.5313,5.946033 -11.8195,7.1377 -4.2882,1.1916 -8.5806,1.2008 -12.8772,0.0276 C 42.187233,17.580067 38.2617,15.2225 34.7073,11.6805 31.1405,8.0722333 28.7602,4.1198427 27.5664,-0.176672 26.3726,-4.473184 26.368433,-8.7686933 27.5539,-13.0632 28.7393,-17.357733 31.127733,-21.28 34.7192,-24.83 l 15.314851,-16.343989 c 2.838864,-2.59362 -4.466565,-0.05255 -6.075051,-1.660025 -1.399264,-1.398388 1.495701,-8.136603 -1.127367,-5.826789 L 26.0641,-32.5017 c -5.152,5.0982 -8.5859,10.755267 -10.3017,16.9712 -1.7158,6.21596 -1.715767,12.4381167 10e-5,18.66647 1.715867,6.2283533 5.170333,11.924563 10.3634,17.08863 5.176533,5.176533 10.875867,8.6238 17.098,10.3418 6.222133,1.717933 12.438067,1.714867 18.6478,-0.0092 6.209733,-1.724133 11.869933,-5.1415 16.9806,-10.2521 L 100.888,-1.75529 C 103.05654,-4.0331058 95.742992,-1.822136 94.091859,-3.2138078 92.447237,-4.5999917 93.748539,-11.36137 92.214,-9.4267 Z M 71.7994,-62.0032 92.8515,-83.0181 c 3.550267,-3.5792 7.47677,-5.9615 11.7795,-7.1469 4.30267,-1.185467 8.60233,-1.1947 12.899,-0.0277 4.29667,1.167 8.222,3.527767 11.776,7.0823 3.57933,3.595867 5.95967,7.545167 7.141,11.8479 1.18133,4.302667 1.17233,8.598133 -0.027,12.8864 -1.2,4.2882 -3.575,8.2136 -7.125,11.7762 l -15.01109,15.962802 c -2.08976,2.498398 3.67755,1.115567 5.20876,2.51702 1.24489,1.591962 -0.15999,7.470051 1.6891,5.160238 L 137.968,-38.9285 c 5.12267,-5.098 8.54333,-10.755033 10.262,-16.9711 1.718,-6.216067 1.71767,-12.4382 -10e-4,-18.6664 -1.718,-6.228267 -5.159,-11.924533 -10.323,-17.0888 -5.18867,-5.217733 -10.894,-8.67513 -17.116,-10.3722 -6.222,-1.698 -12.438,-1.68467 -18.648,0.04 -6.209733,1.72407 -11.8638,5.1413 -16.9622,10.2517 l -22.0354,22.0604 c -1.780932,2.011389 4.635029,0.306466 6.1543,1.55272 1.762717,1.445952 0.538361,8.573669 2.5007,6.11898 z"
id="path5632"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscscccccccccccccsc" />
</g>
<g
id="Medium-L"
transform="matrix(1 0 0 1 1665.29 1556)">
<path
d="M 92.29543,-9.4189296 70.9082,11.9574 c -3.671733,3.649867 -7.703867,6.083967 -12.0964,7.3023 -4.3926,1.2184 -8.7894,1.231633 -13.1904,0.0397 C 41.220333,18.107467 37.202433,15.701667 33.5677,12.082 29.925967,8.3934067 27.497467,4.3486609 26.2822,-0.0522372 25.067,-4.4531324 25.066833,-8.8516867 26.2817,-13.2479 c 1.2148,-4.3962 3.658067,-8.410767 7.3298,-12.0437 l 15.465507,-15.842412 c 1.822207,-1.576209 -3.161713,-1.263594 -4.403316,-2.466165 -1.138825,-1.103024 -0.389294,-5.468814 -1.921183,-4.013494 L 25.8556,-32.0723 c -5.0532,5.007333 -8.423633,10.561467 -10.1113,16.6624 -1.687667,6.1009133 -1.691633,12.2093733 -0.0119,18.32538 1.679733,6.1160133 5.073367,11.71192 10.1809,16.78772 5.090933,5.090933 10.690633,8.4787 16.7991,10.1633 6.108467,1.684533 12.2114,1.6788 18.3088,-0.0172 6.0974,-1.696067 11.6533,-5.051267 16.6677,-10.0656 l 22.38513,-22.4222496 c 0.6772,-0.6813457 -4.980518,-0.4643687 -6.133158,-1.551962 -0.987752,-0.9320103 -0.929836,-5.9164214 -1.645442,-5.228418 z M 70.8011,-62.6265 91.6397,-83.432 c 3.633133,-3.656867 7.6539,-6.0927 12.0623,-7.3075 4.40867,-1.214867 8.81333,-1.2281 13.214,-0.0397 4.40067,1.1884 8.41467,3.596067 12.042,7.223 3.65667,3.673533 6.087,7.714533 7.291,12.123 1.20467,4.4084 1.19167,8.8089 -0.039,13.2015 -1.23133,4.392533 -3.66333,8.4089 -7.296,12.0491 l -15.40426,16.144115 c -1.23062,1.23955 3.33378,0.767254 4.28594,1.827503 0.9437,1.050825 0.52732,5.670742 1.70728,4.552149 L 136.693,-39.4022 c 5.02067,-5.007133 8.37733,-10.5612 10.07,-16.6622 1.69267,-6.101067 1.69667,-12.209533 0.012,-18.3254 -1.68467,-6.115867 -5.065,-11.711867 -10.141,-16.788 -5.09733,-5.1294 -10.70233,-8.5268 -16.815,-10.1922 -6.11267,-1.66533 -12.21767,-1.65 -18.315,0.046 -6.0972,1.6964 -11.645567,5.051667 -16.6451,10.0658 l -21.8138,21.8509 c -0.97075,0.905999 4.91377,0.732295 5.894833,1.57109 0.967877,0.827523 0.83686,6.385001 1.861167,5.20971 z"
id="path5635"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscccccsccsscssccsc" />
</g>
<g
id="Regular-L"
transform="matrix(1 0 0 1 1369.55 1556)">
<path
d="M 91.354327,-8.4289092 70.5528,12.4395 c -3.7764,3.758133 -7.928933,6.2638 -12.4576,7.517 -4.528667,1.2532 -9.0616,1.271633 -13.5988,0.0553 -4.5372,-1.216333 -8.675567,-3.685 -12.4151,-7.406 -3.7396,-3.79344 -6.230967,-7.9586833 -7.4741,-12.49573 -1.243133,-4.5370533 -1.2381,-9.0699767 0.0151,-13.59877 1.2532,-4.528867 3.768,-8.663833 7.5444,-12.4049 l 16.524842,-16.834001 c 1.816544,-1.609535 -2.910853,-0.677615 -3.881878,-1.922544 -0.865169,-1.109212 -0.183332,-4.966975 -1.745424,-3.495381 L 25.5835,-31.5122 c -4.924133,4.8888 -8.211733,10.308633 -9.8628,16.2595 -1.651,5.9509133 -1.6602,11.9111067 -0.0276,17.88058 1.6326,5.96948 4.946933,11.434587 9.943,16.39532 4.979267,4.979267 10.449,8.2894 16.4092,9.9304 5.9602,1.641 11.915733,1.631767 17.8666,-0.0277 5.950867,-1.6594 11.370733,-4.9335 16.2596,-9.8223 L 97.6961,-2.47408 c 1.650507,-1.6169859 -3.260874,-0.9085722 -4.508544,-2.1246356 -1.090394,-1.0627728 -0.283887,-5.3064669 -1.833229,-3.8301936 z M 69.4989,-63.4396 90.059,-83.9718 c 3.7414,-3.758133 7.8854,-6.2638 12.432,-7.517 4.546,-1.253267 9.08767,-1.2717 13.625,-0.0553 4.53667,1.216333 8.66567,3.685133 12.387,7.4064 3.758,3.774867 6.254,7.935467 7.488,12.4818 1.234,4.546333 1.21567,9.083867 -0.055,13.6126 -1.27133,4.528667 -3.77733,8.663667 -7.518,12.405 l -17.37326,17.641583 c -1.49795,1.40754 2.9904,0.719828 4.20494,1.855213 1.20706,1.128383 0.18766,5.640292 1.86629,3.899372 L 135.029,-40.0201 c 4.90707,-4.870067 8.16267,-10.3083 9.822,-16.2593 1.65933,-5.951067 1.66867,-11.911267 0.028,-17.8806 -1.64133,-5.969267 -4.94233,-11.434433 -9.903,-16.3955 -4.97933,-5.014333 -10.45367,-8.333167 -16.423,-9.9565 -5.96933,-1.62333 -11.92933,-1.60533 -17.88,0.054 -5.9512,1.659533 -11.3621,4.933567 -16.2327,9.8221 l -21.5246,21.5777 c -1.035002,0.915112 3.528321,0.728208 4.505423,1.580822 1.0651,0.9294 0.966178,5.264338 2.077777,4.037778 z"
id="path5638"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscscccscssccssccsc" />
</g>
<g
id="Light-L"
transform="matrix(1 0 0 1 1074.21 1556)">
<path
d="M 92.392769,-9.971625 69.9038,12.8974 c -3.9036,3.872733 -8.2024,6.461133 -12.8964,7.7652 -4.694067,1.304067 -9.386067,1.335233 -14.076,0.0935 C 38.241467,19.5143 33.975867,16.988433 30.1346,13.1785 26.293267,9.2830867 23.7383,4.99054 22.4697,0.30086 21.2011,-4.38882 21.218867,-9.0808067 22.523,-13.7751 c 1.304067,-4.694267 3.9079,-8.981833 7.8115,-12.8627 l 18.759627,-18.713327 -4.1913,-4.631671 L 25.2019,-30.8061 c -4.733467,4.710733 -7.903433,9.939733 -9.5099,15.687 -1.606467,5.7472667 -1.622033,11.51024 -0.0467,17.28892 1.575333,5.7786533 4.771967,11.065613 9.5899,15.86088 4.826733,4.826667 10.121567,8.0255 15.8845,9.5965 5.763,1.571 11.518133,1.555433 17.2654,-0.0467 5.747267,-1.602133 10.976267,-4.758567 15.687,-9.4693 L 97.101046,-5.3327821 Z M 65.695001,-61.860025 87.9978,-84.449 c 3.8814,-3.872667 8.1748,-6.461067 12.8802,-7.7652 4.70533,-1.304067 9.403,-1.3352 14.093,-0.0934 4.68933,1.2418 8.93933,3.7679 12.75,7.5783 3.872,3.864 6.43467,8.1487 7.688,12.8541 1.25267,4.7054 1.22133,9.4051 -0.094,14.0991 -1.31533,4.694067 -3.91333,8.9818 -7.794,12.8632 l -18.33254,18.521983 4.33464,4.441328 L 132.7,-40.7454 c 4.71067,-4.710267 7.867,-9.939167 9.469,-15.6867 1.60267,-5.747533 1.61833,-11.5105 0.047,-17.2889 -1.57133,-5.7784 -4.75467,-11.0655 -9.55,-15.8613 -4.82667,-4.848867 -10.129,-8.053233 -15.907,-9.6131 -5.77867,-1.55973 -11.5416,-1.53847 -17.2888,0.0638 -5.747267,1.602333 -10.960833,4.758633 -15.6407,9.4689 l -22.838502,23.253489 z"
id="path5641"
sodipodi:nodetypes="ccsscssccccsscsscccccssccscccccscssccc" />
</g>
<g
id="Thin-L"
transform="matrix(1 0 0 1 779.299 1556)">
<path
d="M 92.04761,-9.2286526 69.0455,13.503 c -4.071867,4.024267 -8.564167,6.7221 -13.4769,8.0935 -4.912667,1.371333 -9.815,1.4193 -14.707,0.1439 C 35.969667,20.464933 31.535767,17.863467 27.5599,13.936 23.584033,9.90568 20.944967,5.444745 19.6427,0.553195 18.340433,-4.338355 18.375,-9.2406867 19.7464,-14.1538 c 1.3714,-4.913133 4.093,-9.4026 8.1648,-13.4684 L 47.928751,-47.041148 45.487427,-49.792381 24.6973,-29.8723 c -4.481333,4.475267 -7.495733,9.451867 -9.0432,14.9298 -1.547533,5.4779867 -1.571533,10.9801533 -0.072,16.5065 1.499533,5.5263467 4.540533,10.577747 9.123,15.1542 4.6248,4.6248 9.6883,7.676367 15.1905,9.1547 5.502133,1.4784 10.9922,1.454433 16.4702,-0.0719 5.477933,-1.526333 10.454533,-4.527133 14.9298,-9.0024 L 94.728223,-6.6430734 Z M 62.326869,-62.164141 85.2717,-85.08 c 4.0666,-4.024267 8.557767,-6.7221 13.4735,-8.0935 4.91587,-1.371333 9.81947,-1.4193 14.7108,-0.1439 4.89133,1.275467 9.30133,3.877333 13.23,7.8056 4.02333,3.981933 6.67433,8.430767 7.953,13.3465 1.278,4.915733 1.23,9.829967 -0.144,14.7427 -1.37467,4.912733 -4.09467,9.4024 -8.16,13.469 l -19.47336,19.34835 2.77065,2.955295 L 129.621,-41.7047 c 4.46718,-4.481941 7.47533,-9.450867 9.002,-14.9292 1.52667,-5.4784 1.55067,-10.980567 0.072,-16.5065 -1.47867,-5.525933 -4.50633,-10.577533 -9.083,-15.1548 -4.62467,-4.63 -9.7,-7.6829 -15.226,-9.1587 -5.526,-1.475733 -11.02797,-1.450233 -16.5059,0.0765 -5.478,1.526733 -10.430833,4.527333 -14.8585,9.0018 l -23.353739,23.54093 z"
id="path5644"
sodipodi:nodetypes="ccsscssccccsscsscccccssccccccssscssccc" />
</g>
<g
id="Ultralight-L"
transform="matrix(1 0 0 1 483.513 1556)">
<path
d="M 91.312783,-9.7003848 68.6058,13.8132 c -4.158,4.101867 -8.749367,6.855733 -13.7741,8.2616 -5.024733,1.4058 -10.0348,1.462367 -15.0302,0.1697 C 34.8061,20.951833 30.286,18.311633 26.2412,14.3239 22.196333,10.22454 19.514167,5.6773867 18.1947,0.68244 16.875233,-4.3125067 16.918433,-9.3225867 18.3243,-14.3478 19.730167,-19.373 22.512067,-23.965833 26.67,-28.1263 L 46.058918,-47.229022 44.516986,-49.070125 24.4388,-29.394 c -4.352133,4.3546 -7.286867,9.201933 -8.8042,14.542 -1.517333,5.3400333 -1.545633,10.7086033 -0.0849,16.10571 1.460733,5.3971 4.422033,10.32783 8.8839,14.79219 4.5214,4.5214 9.4664,7.497567 14.835,8.9285 5.368533,1.430933 10.722833,1.402633 16.0629,-0.0849 5.34,-1.487533 10.187333,-4.4086 14.542,-8.7632 L 93.040529,-8.1648274 Z M 61.968983,-63.666052 83.8755,-85.4032 c 4.1614,-4.101867 8.753833,-6.855733 13.7773,-8.2616 5.02347,-1.405867 10.03253,-1.462467 15.0272,-0.1698 4.99533,1.292667 9.48733,3.933367 13.476,7.9221 4.10067,4.042267 6.79667,8.575133 8.088,13.5986 1.292,5.023533 1.23567,10.047667 -0.169,15.0724 -1.40467,5.024733 -4.18733,9.6178 -8.348,13.7792 l -19.23769,18.899327 1.69449,1.793492 19.8602,-19.426619 c 4.35333,-4.353667 7.274,-9.200733 8.762,-14.5412 1.488,-5.340533 1.51633,-10.7091 0.085,-16.1057 -1.43133,-5.396667 -4.379,-10.327633 -8.843,-14.7929 -4.52133,-4.518 -9.48033,-7.493333 -14.877,-8.926 -5.39667,-1.432667 -10.76513,-1.405 -16.1054,0.083 -5.34,1.488067 -10.159267,4.408933 -14.4578,8.7626 l -22.274882,22.473178 z"
id="path5647"
sodipodi:nodetypes="ccsscssccccsscsscccccssccsccccsscssccc" />
</g>
<g
id="Black-M"
transform="matrix(1 0 0 1 2866.39 1126)">
<path
d="M 71.582249,-15.781785 56.3691,-0.76047 C 53.9687,1.60207 51.3557,3.1827667 48.5301,3.98162 45.704433,4.7804733 42.877833,4.7707667 40.0503,3.9525 37.222767,3.13424 34.627633,1.5436507 32.2649,-0.819268 29.8641,-3.2235293 28.2537,-5.8394367 27.4337,-8.66699 c -0.82,-2.82754 -0.8401,-5.663643 -0.0603,-8.50831 0.779867,-2.844667 2.37,-5.446333 4.7704,-7.805 l 13.459567,-13.459968 c 2.002911,-1.698631 -5.474293,-0.02748 -8.060101,-2.619954 -2.366491,-2.372589 0.306177,-9.529573 -1.74508,-7.673951 L 21.1682,-35.1511 c -4.450533,4.370733 -7.407333,9.234067 -8.8704,14.59 -1.463067,5.355867 -1.443933,10.7116467 0.0574,16.06734 1.501267,5.35569333 4.4598,10.2396133 8.8756,14.65176 4.411933,4.411933 9.2958,7.36 14.6516,8.8442 5.3558,1.4842 10.702133,1.4938 16.039,0.0288 5.336867,-1.464933 10.2097,-4.401833 14.6185,-8.8107 L 82.557549,-5.6110354 c 1.649204,-1.7904175 -5.984975,0.2931727 -8.72614,-2.180718 -2.734773,-2.4681206 -0.572665,-9.4975846 -2.24916,-7.9900316 z M 62.644646,-55.066651 77.6554,-70.2227 c 2.358733,-2.4006 4.951833,-3.9908 7.7793,-4.7706 2.8274,-0.779867 5.6549,-0.770167 8.4825,0.0291 2.827533,0.7992 5.4418,2.3993 7.8428,4.8003 2.40067,2.404467 4.00133,5.0204 4.802,7.8478 0.80133,2.827467 0.81133,5.654033 0.03,8.4797 -0.782,2.8256 -2.35233,5.4368 -4.711,7.8336 l -14.065754,14.133187 c -1.417042,1.417042 5.664118,0.05204 8.057108,2.569378 2.610017,2.745649 0.677057,9.267166 2.152367,7.791859 L 112.856,-35.832 c 4.44733,-4.3708 7.394,-9.234167 8.84,-14.5901 1.446,-5.355867 1.42667,-10.711633 -0.058,-16.0673 -1.484,-5.355733 -4.432,-10.239667 -8.844,-14.6518 -4.45,-4.453667 -9.3434,-7.412167 -14.6802,-8.8755 -5.336667,-1.463333 -10.673433,-1.4625 -16.0103,0.0025 -5.336867,1.465 -10.209833,4.4019 -14.6189,8.8107 l -15.815554,15.966049 c -1.783889,1.65009 6.100529,-0.779019 8.747371,1.843179 2.703105,2.677937 0.209749,10.369633 2.228229,8.327621 z"
id="path5650"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscscccsccsccssccsc" />
</g>
<g
id="Heavy-M"
transform="matrix(1 0 0 1 2570.63 1126)">
<path
d="M 72.085262,-16.039019 56.0531,-0.240984 C 53.545833,2.2357187 50.8099,3.89038 47.8453,4.723 44.880767,5.55562 41.913867,5.5498633 38.9446,4.70573 35.975267,3.8616033 33.253033,2.202717 30.7779,-0.270929 28.273633,-2.787663 26.596333,-5.5306667 25.746,-8.49994 c -0.8504,-2.969307 -0.866567,-5.943493 -0.0485,-8.92256 0.818133,-2.979133 2.480833,-5.702333 4.9881,-8.1696 l 12.706258,-12.323072 c 2.918703,-2.694705 -5.359807,-1.268299 -6.667916,-2.679704 -1.426312,-1.538944 0.382642,-8.542861 -2.677394,-5.863808 L 20.9015,-34.5849 c -4.3262,4.257067 -7.202133,8.989267 -8.6278,14.1966 -1.4256,5.207333 -1.4118,10.41544 0.0414,15.62432 1.4532,5.20886 4.335333,9.9633567 8.6464,14.26349 4.301667,4.30166 9.056567,7.17419 14.2647,8.61759 5.208067,1.443333 10.4085,1.449833 15.6013,0.0195 5.1928,-1.430333 9.9323,-4.288593 14.2185,-8.57478 L 81.871662,-7.0462591 c 2.348131,-2.404027 -6.221377,-0.5454399 -7.667758,-1.956787 -1.455831,-1.4205689 0.579305,-9.6390639 -2.118642,-7.0359729 z M 59.752073,-54.442751 76.0723,-70.8121 c 2.467267,-2.5058 5.1859,-4.167733 8.1559,-4.9858 2.970067,-0.818133 5.939733,-0.8124 8.909,0.0172 2.969267,0.8296 5.7052,2.495767 8.2078,4.9985 2.506,2.5152 4.17633,5.257833 5.011,8.2279 0.83533,2.970067 0.84133,5.937367 0.018,8.9019 -0.824,2.9646 -2.46933,5.695067 -4.936,8.1914 l -11.276134,11.942186 c -2.438399,2.688976 5.381419,1.172608 6.857259,2.679703 1.388525,1.417931 0.188951,9.399856 2.676194,6.530359 L 111.224,-36.4682 c 4.31533,-4.257067 7.18067,-8.989267 8.596,-14.1966 1.416,-5.2074 1.40233,-10.4155 -0.041,-15.6243 -1.44333,-5.208867 -4.315,-9.963367 -8.615,-14.2635 -4.33067,-4.341667 -9.09323,-7.2242 -14.2877,-8.6476 -5.194333,-1.4234 -10.3879,-1.4199 -15.5807,0.0105 -5.1928,1.430333 -9.931533,4.2886 -14.2162,8.5748 l -17.111327,17.179349 c -2.790395,2.629771 6.926022,0.272862 8.18991,1.72687 1.262934,1.452911 -0.60083,9.80658 1.59409,7.26593 z"
id="path5653"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscccccscccscssccsc" />
</g>
<g
id="Bold-M"
transform="matrix(1 0 0 1 2274.98 1126)">
<path
d="M 71.715049,-15.456206 55.6996,0.340135 c -2.626867,2.60441 -5.500267,4.3418117 -8.6202,5.212205 -3.12,0.8703933 -6.2439,0.8690567 -9.3717,-0.00401 -3.127867,-0.8730667 -5.992267,-2.6083543 -8.5932,-5.205863 -2.62,-2.642558 -4.372167,-5.5277403 -5.2565,-8.655547 -0.884333,-3.127813 -0.896067,-6.256487 -0.0352,-9.38602 0.860867,-3.129533 2.604733,-5.988633 5.2316,-8.5773 l 13.024534,-13.876428 c 2.385556,-2.367243 -3.559736,-1.610177 -4.737149,-3.10767 -1.196217,-1.52141 -0.303275,-6.950345 -2.46619,-5.168661 L 20.6031,-33.9515 c -4.187067,4.129867 -6.9725,8.7154 -8.3563,13.7566 -1.3838,5.041133 -1.376,10.08401 0.0234,15.12863 1.3994,5.04460667 4.1961,9.6543367 8.3901,13.82919 4.178267,4.17832 8.788867,6.966313 13.8318,8.36398 5.042867,1.397733 10.0801,1.4008 15.1117,0.0092 5.031667,-1.391667 9.598399,-4.185663 13.7709,-8.31088 L 80.171449,-7.7811855 c 1.219646,-1.396454 -5.428484,0.021171 -6.843665,-1.412786 -1.487347,-1.5070805 -0.129909,-7.7257425 -1.612735,-6.2622345 z M 58.566316,-55.38943 74.3012,-71.4715 c 2.5888,-2.623467 5.447967,-4.365633 8.5775,-5.2265 3.129533,-0.860867 6.2582,-0.859533 9.386,0.004 3.1278,0.863533 5.9999,2.6036 8.6163,5.2202 2.62333,2.639133 4.37167,5.523467 5.245,8.653 0.87333,3.129533 0.87467,6.2543 0.004,9.3743 -0.87067,3.119933 -2.60033,5.9838 -5.189,8.5916 l -11.400267,12.257561 c -2.61235,3.037424 4.33931,1.292419 5.781986,2.726884 1.424012,1.415908 0.304077,7.331131 1.79704,5.644668 L 109.398,-37.1798 c 4.168,-4.129867 6.943,-8.7154 8.325,-13.7566 1.382,-5.0412 1.374,-10.0841 -0.024,-15.1287 -1.39733,-5.044533 -4.18333,-9.654267 -8.358,-13.8292 -4.19733,-4.2164 -8.81367,-7.013933 -13.849,-8.3926 -5.035067,-1.378667 -10.068433,-1.372167 -15.1001,0.0195 -5.031667,1.391667 -9.620267,4.161933 -13.7658,8.3108 l -16.511084,16.89207 c -2.282476,2.418069 4.658463,0.902512 6.188121,2.341407 1.528514,1.437818 0.247515,7.76955 2.263179,5.333693 z"
id="path5656"
sodipodi:nodetypes="ccsscssccsccsscssscscccsscssccsccsccssccsc" />
</g>
<g
id="Semibold-M"
transform="matrix(1 0 0 1 1979 1126)">
<path
d="M 71.8638,-15.6488 55.4559,0.740755 C 52.746633,3.4332117 49.778433,5.2276533 46.5513,6.12408 43.324167,7.0205133 40.092033,7.0222233 36.8549,6.12921 33.617767,5.2361967 30.655367,3.4482397 27.9677,0.765339 25.2679,-1.9639537 23.464133,-4.9471533 22.5564,-8.18426 c -0.907733,-3.237093 -0.9164,-6.47224 -0.026,-9.70544 0.8904,-3.233267 2.690233,-6.1861 5.3995,-8.8585 l 10.76706,-11.164051 c 1.909339,-1.887602 -3.496826,-0.786645 -4.493237,-2.004731 -0.970689,-1.18664 -0.328262,-6.186029 -1.689087,-4.79244 L 20.3975,-33.5149 c -4.0912,4.042267 -6.814267,8.5267 -8.1692,13.4533 -1.354933,4.9266 -1.351267,9.85558 0.011,14.78694 1.362333,4.93137333 4.1001,9.4412967 8.2133,13.52977 4.093267,4.09326 8.6044,6.82299 13.5334,8.18919 4.928933,1.366267 9.8537,1.366933 14.7743,0.002 4.920533,-1.364933 9.407967,-4.07456 13.4623,-8.12888 L 79.4033,-8.88225 c 0.651705,-0.7880006 -4.807739,-0.4157026 -5.852951,-1.510807 -1.098979,-1.151437 -0.909192,-6.03465 -1.686549,-5.255743 z m -15.1985,-39.8876 16.415,-16.3897 c 2.672467,-2.704533 5.628467,-4.502 8.868,-5.3924 3.239467,-0.8904 6.477767,-0.8921 9.7149,-0.0051 3.237067,0.886933 6.203,2.677967 8.8978,5.3731 2.70467,2.724467 4.50667,5.706467 5.406,8.946 0.89933,3.239467 0.89767,6.472767 -0.005,9.6999 -0.90267,3.227133 -2.69033,6.183 -5.363,8.8676 l -10.423101,10.692629 c -1.038759,1.281614 3.503901,0.827831 4.624401,1.937398 1.026121,1.016109 0.262064,6.320693 1.348828,4.859772 L 108.139,-37.6704 c 4.066,-4.0422 6.77867,-8.5266 8.138,-13.4532 1.35867,-4.926667 1.355,-9.855667 -0.011,-14.787 -1.36667,-4.931333 -4.094,-9.441267 -8.182,-13.5298 -4.10533,-4.130067 -8.6208,-6.869 -13.5464,-8.2168 -4.925267,-1.347867 -9.848167,-1.339333 -14.7687,0.0256 -4.920533,1.365 -9.405633,4.074633 -13.4553,8.1289 l -17.1807,17.1996 c -1.219345,1.278774 4.105962,0.70035 5.451089,1.73662 1.323225,1.019398 0.851376,6.457002 2.081311,5.03008 z"
id="path5659"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscssccscccccssscsc" />
</g>
<g
id="Medium-M"
transform="matrix(1 0 0 1 1682.85 1126)">
<path
d="M 71.807759,-15.347096 55.2711,1.04452 C 52.499367,3.8037333 49.4593,5.6414267 46.1509,6.5576 42.842567,7.47378 39.5284,7.4778 36.2084,6.56966 32.888333,5.6615267 29.8516,3.8336333 27.0982,1.08598 c -2.760267,-2.7950667 -4.603167,-5.8525867 -5.5287,-9.17256 -0.925467,-3.319947 -0.931833,-6.635853 -0.0191,-9.94772 0.9128,-3.311867 2.755067,-6.335733 5.5268,-9.0716 l 11.141406,-11.200237 c 1.847475,-1.652137 -2.835277,-0.973842 -3.858815,-2.257645 -0.975518,-1.223574 -0.0691,-5.634664 -1.760255,-4.08054 L 20.2415,-33.1838 c -4.018467,3.9758 -6.694233,8.383567 -8.0273,13.2233 -1.333067,4.839733 -1.3325,9.682343 0.0017,14.52783 1.3342,4.84550667 4.027267,9.2797567 8.0792,13.30275 4.0288,4.028747 8.4645,6.71432 13.3071,8.05672 4.842667,1.342333 9.682133,1.341167 14.5184,-0.0035 4.836333,-1.344667 9.2458,-4.008307 13.2284,-7.99092 l 16.9665,-16.9997 c 1.73766,-1.598346 -3.617048,-0.7862579 -4.62794,-2.034565 -0.90721,-1.120273 -0.424858,-5.673611 -1.879801,-4.245211 z M 55.609241,-55.609677 72.1545,-72.2708 c 2.736,-2.766 5.765433,-4.6054 9.0883,-5.5182 3.322867,-0.912733 6.6443,-0.916733 9.9643,-0.012 3.319933,0.904667 6.357233,2.7343 9.1119,5.4889 2.766,2.789333 4.60867,5.845433 5.528,9.1683 0.91933,3.322867 0.91533,6.638467 -0.012,9.9468 -0.92733,3.308333 -2.759,6.333933 -5.495,9.0768 l -10.729015,11.536797 c -1.583747,1.517405 3.0472,0.886926 4.123942,1.988316 1.193329,1.220646 0.407095,5.769261 1.621192,4.4172 L 107.184,-38.0424 c 3.98933,-3.975667 6.65467,-8.3834 7.996,-13.2232 1.34133,-4.8398 1.341,-9.682433 -0.001,-14.5279 -1.34267,-4.845467 -4.02533,-9.279733 -8.048,-13.3028 -4.036,-4.064533 -8.475,-6.759033 -13.317,-8.0835 -4.842,-1.324533 -9.681167,-1.314433 -14.5175,0.0303 -4.836333,1.344733 -9.242967,4.008367 -13.2199,7.9909 l -16.9664,16.9997 c -1.307646,1.426393 3.454529,1.077349 4.556022,2.261708 1.168828,1.256759 0.838434,5.496305 1.943019,4.287515 z"
id="path5662"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscssccsccsccssccsc" />
</g>
<g
id="Regular-M"
transform="matrix(1 0 0 1 1386.86 1126)">
<path
d="M 71.339886,-14.726054 55.0301,1.44074 C 52.176833,4.2870267 49.043033,6.1811333 45.6287,7.12306 42.214367,8.0649933 38.793133,8.0720267 35.365,7.14416 31.936867,6.2162933 28.8032,4.3363067 25.964,1.5042 c -2.8392,-2.8808467 -4.7331,-6.0353033 -5.6817,-9.46337 -0.948667,-3.428087 -0.952033,-6.84933 -0.0101,-10.26373 0.941933,-3.414333 2.839533,-6.530867 5.6928,-9.3496 l 12.668965,-13.466447 c 1.302623,-1.264407 -2.73998,-1.050228 -3.444119,-1.782403 -0.742844,-0.772421 -0.486293,-4.296347 -1.503199,-3.354136 L 20.0381,-32.7519 c -3.923667,3.889 -6.537733,8.196733 -7.8422,12.9232 -1.304533,4.7264 -1.308067,9.456373 -0.0106,14.18992 1.297467,4.73352667 3.932267,9.06907 7.9044,13.00663 3.944667,3.944633 8.282,6.572583 13.012,7.88385 4.73,1.3112 9.4582,1.307667 14.1846,-0.0106 4.7264,-1.318267 9.034133,-3.921913 12.9232,-7.81094 l 16.687,-16.73887 c 1.277273,-1.324884 -2.970423,-0.729268 -3.943326,-1.668199 -1.025997,-0.99017 -0.344591,-4.899056 -1.613288,-3.749145 z M 54.293338,-56.505735 70.947,-72.7203 c 2.818867,-2.846333 5.9441,-4.740433 9.3757,-5.6823 3.4316,-0.941933 6.861433,-0.948967 10.2895,-0.0211 3.428067,0.927867 6.558333,2.8079 9.3908,5.6401 2.846,2.873733 4.74167,6.026433 5.687,9.4581 0.94467,3.4316 0.93733,6.854567 -0.022,10.2689 -0.95867,3.414333 -2.84733,6.5309 -5.666,9.3497 l -11.918215,11.816717 c -0.972963,1.230674 2.912238,1.13081 3.640815,1.950733 0.645001,0.725867 0.1514,4.902233 1.093907,3.960022 L 105.939,-38.5276 c 3.88933,-3.889 6.493,-8.196733 7.811,-12.9232 1.31867,-4.726467 1.32233,-9.456433 0.011,-14.1899 -1.31133,-4.733467 -3.936,-9.069033 -7.874,-13.0067 -3.9444,-3.979133 -8.283333,-6.6157 -13.0168,-7.9097 -4.733467,-1.294 -9.463433,-1.281833 -14.1899,0.0365 -4.7264,1.318333 -9.030633,3.921967 -12.9127,7.8109 l -16.687,16.7388 c -1.170502,1.005367 2.75972,0.66995 3.597184,1.472776 0.961667,0.921893 0.579928,5.059098 1.615554,3.992389 z"
id="path5665"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscccccscccscssccsc" />
</g>
<g
id="Light-M"
transform="matrix(1 0 0 1 1091.37 1126)">
<path
d="M 70.612228,-14.244996 54.4482,1.85127 C 51.480933,4.8002833 48.216,6.76856 44.6534,7.7561 41.0908,8.7436467 37.526967,8.7620833 33.9619,7.81141 30.396833,6.8607367 27.1491,4.9294833 24.2187,2.01765 c -2.9304,-2.97227333 -4.881333,-6.2408633 -5.8528,-9.80577 -0.971467,-3.56492 -0.9634,-7.128747 0.0242,-10.69148 0.987533,-3.562733 2.964933,-6.8161 5.9322,-9.7601 L 38.873465,-43.30212 34.893136,-47.276604 19.696,-32.1189 c -3.752667,3.7294 -6.2613,7.866033 -7.5259,12.4099 -1.264533,4.543867 -1.273733,9.09705 -0.0276,13.65955 1.246133,4.5624693 3.775433,8.738316 7.5879,12.52754 3.8078,3.807807 7.9883,6.335943 12.5415,7.58441 4.553133,1.248467 9.101633,1.239267 13.6455,-0.0276 4.543933,-1.266933 8.6806,-3.765103 12.41,-7.49451 L 74.795104,-10.029636 Z M 52.570513,-56.916741 69.0991,-73.1481 c 2.944333,-2.949 6.2036,-4.917267 9.7778,-5.9048 3.5742,-0.987533 7.143767,-1.005967 10.7087,-0.0553 3.564867,0.950667 6.8034,2.882067 9.7156,5.7942 2.94853,2.953667 4.90413,6.2176 5.8668,9.7918 0.962,3.5742 0.94333,7.1426 -0.056,10.7052 -0.99867,3.562533 -2.97017,6.816 -5.9145,9.7604 l -14.725661,15.46591 3.750434,3.907054 L 103.852,-39.1779 c 3.72867,-3.729133 6.22667,-7.8657 7.494,-12.4097 1.26733,-4.544067 1.27667,-9.097233 0.028,-13.6595 -1.24867,-4.562333 -3.76767,-8.738267 -7.557,-12.5278 -3.80787,-3.8308 -7.992967,-6.364667 -12.5553,-7.6016 -4.562333,-1.237 -9.115433,-1.221967 -13.6593,0.0451 -4.543867,1.267 -8.671367,3.765067 -12.3825,7.4942 l -16.6698,16.7046 z"
id="path5668"
sodipodi:nodetypes="ccsscssccccsscsscccccsscccccccsscssccc" />
</g>
<g
id="Thin-M"
transform="matrix(1 0 0 1 796.28 1126)">
<path
d="M 70.194651,-14.120951 54.035681,2.0609444 C 50.917614,5.1458244 47.122167,7.5454767 43.3635,8.59335 39.604833,9.64125 35.852433,9.6747667 32.1063,8.6939 28.3601,7.7130667 24.961467,5.7140067 21.9104,2.69672 18.859333,-0.39646 16.833,-3.8159967 15.8314,-7.56189 c -1.001667,-3.745873 -0.978533,-7.498277 0.0694,-11.25721 1.047867,-3.758933 3.130833,-7.1933 6.2489,-10.3031 L 37.616446,-44.158259 35.307594,-46.460019 19.2435,-31.2816 c -3.526533,3.5182 -5.895667,7.428533 -7.1074,11.731 -1.211733,4.3024 -1.2285,8.621733 -0.0503,12.958 1.1782,4.33624 3.567967,8.3008733 7.1693,11.8939 3.626867,3.6268 7.599967,6.022933 11.9193,7.1884 4.319333,1.165467 8.6302,1.1487 12.9326,-0.0503 4.302467,-1.198933 8.212833,-3.55753 11.7311,-7.07579 L 72.842081,-11.533854 Z M 50.80251,-57.102627 66.655,-73.7139 c 3.1104,-3.084867 6.547,-5.151233 10.3098,-6.1991 3.7628,-1.047867 7.517133,-1.081367 11.263,-0.1005 3.745867,0.9808 7.127733,2.980133 10.1456,5.998 3.0844,3.0594 5.11893,6.470467 6.1036,10.2332 0.98533,3.7628 0.952,7.523533 -0.1,11.2822 -1.052,3.758667 -3.13297,7.193167 -6.2429,10.3035 l -15.940664,15.249407 2.538756,2.434012 L 101.091,-40.038 c 3.518,-3.517667 5.87633,-7.427867 7.075,-11.7306 1.19933,-4.302733 1.21633,-8.622067 0.051,-12.958 -1.166,-4.336 -3.54533,-8.300767 -7.138,-11.8943 -3.627133,-3.6346 -7.608667,-6.0327 -11.9446,-7.1943 -4.336,-1.161533 -8.6552,-1.1427 -12.9576,0.0565 -4.302467,1.199267 -8.291422,3.700565 -11.776422,7.218232 l -16.266114,16.802333 z"
id="path5671"
sodipodi:nodetypes="ccsscssccccsscsscccccssccscccccccssccc" />
</g>
<g
id="Ultralight-M"
transform="matrix(1 0 0 1 500.398 1126)">
<path
d="M 70.8723,-15.581051 53.2845,2.67232 C 50.0891,5.8267933 46.561867,7.9434167 42.7028,9.02219 38.843733,10.10093 34.994733,10.14217 31.1558,9.14591 27.316867,8.1496233 23.840967,6.1158333 20.7281,3.04454 17.615233,-0.11056667 15.550267,-3.6074167 14.5332,-7.44601 c -1.017067,-3.838593 -0.986233,-7.68759 0.0925,-11.54699 1.0788,-3.8594 3.215867,-7.386467 6.4112,-10.5812 L 36.648425,-46.034501 35.24043,-47.477106 19.0118,-30.8528 c -3.4108,3.410067 -5.7085,7.2045 -6.8931,11.3833 -1.184667,4.1788 -1.2053,8.37837 -0.0619,12.59871 1.1434,4.2203607 3.4617,8.0768107 6.9549,11.56935 3.534133,3.5341267 7.400967,5.86264 11.6005,6.98554 4.1996,1.122933 8.388767,1.102333 12.5675,-0.0618 4.1788,-1.164187 7.973233,-3.4513333 11.3833,-6.86144 L 72.436181,-14.017886 Z M 48.181398,-57.344211 65.4032,-74.0037 c 3.1954,-3.154467 6.7228,-5.271067 10.5822,-6.3498 3.8594,-1.0788 7.7084,-1.120033 11.547,-0.1237 3.8386,0.996267 7.293867,3.0304 10.3658,6.1024 3.15387,3.113533 5.22913,6.599967 6.2258,10.4593 0.99667,3.8594 0.95533,7.718633 -0.124,11.5777 -1.07933,3.859067 -3.2162,7.3863 -6.4106,10.5817 L 82.123029,-25.862232 83.575313,-24.18747 99.6769,-40.4785 c 3.4094,-3.4094 5.69643,-7.203667 6.8611,-11.3828 1.16467,-4.179133 1.18533,-8.3787 0.062,-12.5987 -1.12333,-4.22 -3.43133,-8.076633 -6.924,-11.5699 -3.534133,-3.534067 -7.4112,-5.862567 -11.6312,-6.9855 -4.220067,-1.123 -8.419467,-1.102233 -12.5982,0.0623 -4.1788,1.164533 -7.9528,3.4515 -11.322,6.8609 l -17.539905,17.25514 z"
id="path5674"
sodipodi:nodetypes="ccsscssccccsscsscccccsscssccccsscssccc" />
</g>
<g
id="Black-S"
transform="matrix(1 0 0 1 2880.67 696)">
<path
d="M 55.484427,-19.499317 44.2383,-8.34961 c -1.855467,1.82292 -3.8737,3.0436233 -6.0547,3.66211 -2.181,0.6184867 -4.362,0.61035 -6.543,-0.02441 -2.181,-0.6347667 -4.182933,-1.86361 -6.0058,-3.68653 -1.855467,-1.85544 -3.1006,-3.87366 -3.7354,-6.05466 -0.634733,-2.181 -0.651,-4.370133 -0.0488,-6.5674 0.6022,-2.197267 1.831033,-4.207367 3.6865,-6.0303 l 9.951714,-10.713489 c 2.213391,-2.103234 -5.346929,-0.192043 -6.76298,-1.857482 -1.475218,-1.735005 0.189224,-8.233861 -1.643283,-6.395178 l -9.051438,8.666984 c -3.5156,3.450467 -7.23193,9.338865 -8.387563,13.570665 -1.1555933,4.2318 -1.1393167,8.463567 0.04883,12.6953 1.188147,4.23178 3.523753,8.089207 7.00682,11.572281 3.483067,3.4830727 7.3405,5.8105457 11.5723,6.982419 4.231733,1.17188 8.455367,1.18002 12.6709,0.02442 4.215467,-1.1556 8.064733,-3.474937 11.5478,-6.958011 l 11.690484,-11.594083 c 1.689433,-1.769048 -4.567365,0.431951 -6.807164,-2.047527 -2.26878,-2.51156 -0.07873,-8.203681 -1.889093,-6.394816 z M 49.365332,-51.064618 61.2305,-62.5 c 1.822867,-1.855467 3.8248,-3.0843 6.0058,-3.6865 2.181,-0.6022 4.362,-0.594067 6.543,0.0244 2.181,0.618467 4.199233,1.855433 6.0547,3.7109 1.855467,1.855467 3.092433,3.8737 3.7109,6.0547 0.618533,2.181 0.626667,4.362 0.0244,6.543 -0.6022,2.181 -1.814767,4.199233 -3.6377,6.0547 l -10.618267,10.475434 c -1.737216,1.817571 5.552176,-0.0064 7.382021,2.000315 1.697091,1.861111 -0.522529,8.614748 1.262297,6.918898 L 88.8184,-35.5469 c 3.5156,-3.450533 5.843067,-7.291667 6.9824,-11.5234 1.139333,-4.2318 1.123067,-8.463567 -0.0488,-12.6953 -1.171933,-4.2318 -3.499433,-8.089233 -6.9825,-11.5723 -3.5156,-3.5156 -7.381133,-5.8512 -11.5966,-7.0068 -4.215533,-1.1556 -8.431033,-1.1556 -12.6465,0 -4.215533,1.1556 -8.064833,3.474933 -11.5479,6.958 l -12.499868,12.070182 c -1.413356,1.332672 5.668792,-0.03638 7.378352,1.610863 1.669204,1.608356 0.217626,8.116128 1.508348,6.641037 z"
id="path5677"
sodipodi:nodetypes="ccsssssccsccsssssccscccsssssccsccsssssccsc" />
</g>
<g
id="Heavy-S"
transform="matrix(1 0 0 1 2584.61 696)">
<path
d="M 56.165663,-20.178427 44.0424,-7.95778 c -1.932333,1.9074333 -4.038933,3.1819167 -6.3198,3.82345 -2.280867,0.64154 -4.563667,0.6353233 -6.8484,-0.01865 -2.284667,-0.6539733 -4.380733,-1.9346733 -6.2882,-3.8421 -1.932267,-1.9400133 -3.2273,-4.052353 -3.8851,-6.33702 -0.657867,-2.284733 -0.672233,-4.573733 -0.0431,-6.867 0.629133,-2.293333 1.909833,-4.389867 3.8421,-6.2896 l 10.38717,-10.185176 c 2.070536,-1.986668 -4.845719,-0.802257 -5.84601,-1.922843 -1.107156,-1.240304 0.664984,-7.618809 -1.311155,-5.579372 L 16.466,-34.8994 c -3.431133,3.373733 -5.711043,7.1246 -6.83973,11.2526 -1.1287067,4.128067 -1.1162733,8.256133 0.0373,12.3842 1.153553,4.12804 3.437297,7.8951824 6.85123,11.3014271 3.406267,3.4062419 7.173433,5.6818562 11.3015,6.8268429 4.128,1.1449867 8.249833,1.1512033 12.3655,0.01865 C 44.2974,5.7517733 48.054467,3.4862184 51.453,0.0876552 L 64.199563,-12.767727 c 1.714155,-1.478878 -5.410389,-0.399761 -6.529252,-1.272121 -1.205771,-0.940119 0.320699,-7.864272 -1.504648,-6.138579 z M 48.124001,-50.863537 60.1126,-62.9495 c 1.899733,-1.932267 3.991967,-3.212967 6.2767,-3.8421 2.284667,-0.629067 4.569367,-0.622833 6.8541,0.0187 2.284733,0.641533 4.393233,1.928433 6.3255,3.8607 1.932333,1.94 3.221167,4.052367 3.8665,6.3371 0.6454,2.284733 0.651633,4.567533 0.0187,6.8484 -0.632933,2.280867 -1.899267,4.3836 -3.799,6.3082 l -9.377193,9.713752 c -1.935871,2.188731 5.667852,0.556869 6.653893,1.518853 1.039739,1.014372 -0.395553,7.416879 1.378587,5.242713 L 87.689,-36.0079 c 3.4234,-3.373667 5.695167,-7.124533 6.8153,-11.2526 1.120133,-4.128 1.1077,-8.256033 -0.0373,-12.3841 -1.145,-4.128067 -3.420633,-7.895233 -6.8269,-11.3015 -3.431067,-3.4388 -7.204433,-5.722533 -11.3201,-6.8512 -4.1156,-1.128733 -8.2312,-1.126833 -12.3468,0.0057 -4.1156,1.1326 -7.8727,3.398167 -11.2713,6.7967 l -12.611799,12.720763 c -1.10988,1.341349 5.262659,0.171163 6.571091,1.376748 1.349799,1.243701 0.03774,7.596698 1.462809,6.033852 z"
id="path5680"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscssccsccsscssccsc" />
</g>
<g
id="Bold-S"
transform="matrix(1 0 0 1 2288.64 696)">
<path
d="M 56.958,-20.6299 43.8232,-7.51953 c -2.0182,2.0019533 -4.2236,3.33659 -6.6162,4.00391 -2.392533,0.6673133 -4.789167,0.6632433 -7.1899,-0.01221 -2.400733,-0.67546 -4.602067,-2.0141667 -6.604,-4.01612 -2.018267,-2.0345 -3.369167,-4.252117 -4.0527,-6.65285 -0.6836,-2.400733 -0.695833,-4.801433 -0.0367,-7.2021 0.6592,-2.400733 1.9979,-4.593933 4.0161,-6.5796 l 7.57176,-8.245079 c 1.828409,-1.8392 -3.260839,-1.186673 -4.280173,-2.351449 -0.988622,-1.129681 0.634883,-6.047494 -1.219397,-4.548171 L 16.2598,-34.4482 c -3.3366,3.287733 -5.554223,6.937633 -6.65287,10.9497 -1.0986333,4.012 -1.0904933,8.024033 0.02442,12.0361 1.1149,4.01204 3.34065,7.6782177 6.67725,10.998533 3.320333,3.3203113 6.9865,5.5379203 10.9985,6.652827 4.012067,1.1149133 8.020067,1.1189833 12.024,0.01221 4.003867,-1.1067667 7.657833,-3.3121697 10.9619,-6.616209 L 64.0381,-14.1602 c 0.809147,-0.929714 -4.655862,-0.03059 -5.676353,-1.063202 C 57.181145,-16.418033 57.931804,-21.775108 56.958,-20.6299 Z M 45.7275,-50.3418 58.8623,-63.4521 c 1.985667,-2.018267 4.178867,-3.357 6.5796,-4.0162 2.400733,-0.659133 4.801433,-0.655067 7.2021,0.0122 2.400733,0.667333 4.610233,2.010133 6.6285,4.0284 2.0182,2.034467 3.365033,4.252067 4.0405,6.6528 0.675467,2.400733 0.679533,4.797367 0.0122,7.1899 -0.667333,2.3926 -1.993833,4.589867 -3.9795,6.5918 l -7.369765,8.447075 c -1.693746,1.973863 3.013976,1.167494 4.010946,2.216784 0.969194,1.020056 0.57709,5.8455 1.960047,4.076849 L 86.4258,-36.5234 c 3.320267,-3.2878 5.529733,-6.9377 6.6284,-10.9497 1.0986,-4.012067 1.090467,-8.024133 -0.0244,-12.0362 -1.114933,-4.012 -3.332533,-7.678167 -6.6528,-10.9985 -3.3366,-3.352867 -7.006867,-5.578633 -11.0108,-6.6773 -4.003867,-1.0986 -8.007767,-1.094533 -12.0117,0.0122 -4.003933,1.1068 -7.6579,3.312233 -10.9619,6.6163 l -13.7451,13.7451 c -0.726409,0.781899 4.714432,0.06985 5.813847,1.355768 1.255841,1.468877 0.340928,6.113023 1.266153,5.113932 z"
id="path5683"
sodipodi:nodetypes="ccsscssccsccsscssscscccsscssccsccsscssscsc" />
</g>
<g
id="Semibold-S"
transform="matrix(1 0 0 1 1992.43 696)">
<path
d="M 54.959936,-18.489635 43.6722,-7.21738 c -2.077467,2.06712 -4.351,3.4432267 -6.8206,4.12832 -2.4696,0.6850867 -4.944767,0.6824967 -7.4255,-0.00777 -2.480667,-0.6902667 -4.754567,-2.06896 -6.8217,-4.13608 -2.077467,-2.09966 -3.4669,-4.389857 -4.1683,-6.87059 -0.701333,-2.480667 -0.712033,-4.9584 -0.0321,-7.4332 0.679867,-2.474733 2.058533,-4.734567 4.136,-6.7795 l 10.127967,-9.835536 c 1.410153,-1.262569 -2.80757,-0.822263 -3.81634,-1.802168 -0.904017,-0.87815 -0.221755,-5.292901 -1.4666,-4.064354 L 16.1176,-34.1372 c -3.271467,3.228533 -5.446133,6.808833 -6.524,10.7409 -1.0778933,3.932067 -1.0727167,7.864133 0.01553,11.7962 1.088247,3.9320333 3.274003,7.5285837 6.55727,10.789651 3.261067,3.261066 6.857633,5.4386857 10.7897,6.532859 3.932067,1.0941733 7.861533,1.0967633 11.7884,0.00777 3.926933,-1.089 7.509833,-3.2529337 10.7487,-6.491801 l 11.889136,-11.907014 c 1.033107,-1.071802 -3.737525,-0.797899 -4.665465,-1.578005 -0.932053,-0.783562 -0.640852,-5.351989 -1.756935,-4.242995 z M 46.039246,-52.122473 58.0003,-63.7987 c 2.044933,-2.077467 4.307733,-3.456167 6.7884,-4.1361 2.480733,-0.679933 4.961433,-0.677367 7.4421,0.0077 2.480733,0.685133 4.759833,2.066433 6.8373,4.1439 2.077467,2.099667 3.4643,4.389867 4.1605,6.8706 0.6962,2.480667 0.698767,4.9558 0.0077,7.4254 -0.691,2.4696 -3.944259,5.809343 -5.989192,7.86461 l -7.830516,8.06857 c -1.172032,0.976905 3.673254,0.741758 4.435381,1.611725 0.706698,0.806695 0.1265,5.292902 1.276056,4.111965 L 85.5549,-36.8789 c 3.2492,-3.228533 5.4157,-6.808833 6.4995,-10.7409 1.083867,-3.932067 1.0787,-7.864133 -0.0155,-11.7962 -1.094133,-3.932067 -3.271733,-7.528633 -6.5328,-10.7897 -3.271467,-3.2936 -6.870633,-5.479333 -10.7975,-6.5572 -3.926867,-1.077933 -7.853767,-1.0724 -11.7807,0.0166 -3.926867,1.089 -7.509733,3.252933 -10.7486,6.4918 l -12.657776,12.596692 c -0.957303,1.02482 3.748794,0.480309 4.721538,1.440915 0.885842,0.874788 0.974161,5.110381 1.796184,4.09442 z"
id="path5686"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscssccsccsscssccsc" />
</g>
<g
id="Medium-S"
transform="matrix(1 0 0 1 1696.1 696)">
<path
d="M 56.571765,-19.898443 43.5576,-6.98828 c -2.1224,2.1165333 -4.4476,3.5240867 -6.9756,4.22266 -2.528,0.6985667 -5.062667,0.6971 -7.604,-0.0044 -2.541333,-0.7015 -4.870267,-2.1105167 -6.9868,-4.22705 -2.1224,-2.1490867 -3.541,-4.494297 -4.2558,-7.03563 -0.714867,-2.541333 -0.7245,-5.077467 -0.0289,-7.6084 0.695667,-2.530933 2.1047,-4.841333 4.2271,-6.9312 l 8.379358,-8.609244 c 1.250721,-1.151942 -2.608825,-1.002525 -3.372529,-1.79007 -0.808763,-0.834012 -0.427866,-4.738658 -1.594599,-3.620575 L 16.0098,-33.9014 c -3.222,3.1836 -5.3641,6.711133 -6.4263,10.5826 -1.06218,3.8714 -1.05925,7.7428 0.00879,11.6142 1.068007,3.8714267 3.223443,7.4152133 6.46631,10.63136 3.216133,3.2161467 6.7599,5.3634433 10.6313,6.44189 3.871467,1.0784533 7.741433,1.07992 11.6099,0.0044 3.868467,-1.07552 7.397433,-3.2080067 10.5869,-6.39746 L 62.21,-14.3789 c 0.534983,-0.555077 -3.414379,-0.730689 -4.207325,-1.479979 -0.838826,-0.792643 -0.830331,-4.626314 -1.43091,-4.039564 z M 44.6182,-51.3418 57.3467,-64.0615 c 2.0898,-2.1224 4.4054,-3.531433 6.9468,-4.2271 2.541333,-0.6956 5.082667,-0.694133 7.624,0.0044 2.541333,0.6986 4.8732,2.1091 6.9956,4.2315 2.1224,2.149067 3.539567,4.494267 4.2515,7.0356 0.711867,2.541333 0.7133,5.076 0.0043,7.604 -0.708933,2.528 -2.108333,4.839867 -4.1982,6.9356 l -8.280173,8.613448 c -0.935834,1.007471 2.824041,1.213408 3.65423,2.071531 0.719118,0.743314 0.09459,4.595826 1.166102,3.572964 L 84.8945,-37.1484 c 3.195333,-3.1836 5.3293,-6.711133 6.4019,-10.5826 1.0726,-3.8714 1.069667,-7.7428 -0.0088,-11.6142 -1.078467,-3.8714 -3.225767,-7.4152 -6.4419,-10.6314 -3.222,-3.248667 -6.767233,-5.4041 -10.6357,-6.4663 -3.868533,-1.062133 -7.737033,-1.055467 -11.6055,0.02 -3.868467,1.075533 -7.397433,3.208033 -10.5869,6.3975 l -13.3233,13.3545 c -0.836385,1.014603 3.173193,0.931064 4.11495,1.687147 0.795264,0.638472 0.947372,4.738856 1.80895,3.641953 z"
id="path5689"
sodipodi:nodetypes="ccsscssccsccsscssccscccsscssccsccsscssccsc" />
</g>
<g
id="Regular-S"
transform="matrix(1 0 0 1 1399.89 696)">
<path
d="m 55.9082,-19.1895 -12.5,12.50005 c -2.181,2.1809867 -4.573567,3.6295533 -7.1777,4.3457 -2.6042,0.7161467 -5.2165,0.7161467 -7.8369,0 -2.620467,-0.7161467 -5.0212,-2.1647133 -7.2022,-4.3457 -2.181,-2.2135667 -3.6377,-4.63055 -4.3701,-7.25095 -0.7324,-2.620467 -0.740533,-5.232767 -0.0244,-7.8369 0.716133,-2.6042 2.1647,-4.9805 4.3457,-7.1289 l 9.790063,-10.497047 c 1.064763,-1.028342 -2.174195,-1.220632 -2.754199,-1.851124 -0.52353,-0.569103 -0.701388,-4.022144 -1.615964,-3.032729 l -10.6934,10.6933 c -3.157533,3.125067 -5.25713,6.583733 -6.29879,10.376 -1.0416667,3.792333 -1.0416667,7.584667 0,11.377 1.04166,3.7923067 3.157557,7.2672333 6.34769,10.42478 3.157533,3.1575533 6.632467,5.2653033 10.4248,6.32325 3.792267,1.05794 7.584567,1.05794 11.3769,0 3.792333,-1.0579467 7.251,-3.14942 10.376,-6.27442 L 61.1816,-14.502 c 1.039973,-0.92073 -2.800765,-0.797134 -3.583014,-1.511079 -0.761308,-0.694833 -0.787043,-4.082194 -1.690386,-3.176421 z m -11.9141,-32.7148 12.5,-12.5 c 2.148467,-2.181 4.532933,-3.629567 7.1534,-4.3457 2.6204,-0.716133 5.240833,-0.716133 7.8613,0 2.620467,0.716133 5.021167,2.1647 7.2021,4.3457 2.181,2.213533 3.637733,4.630533 4.3702,7.251 0.7324,2.6204 0.7324,5.2327 0,7.8369 -0.732467,2.604133 -2.18392,4.96947 -4.3213,7.1289 l -9.790063,9.891061 c -0.862834,0.960944 2.612221,1.20775 3.326419,1.817457 0.570442,0.486984 0.634123,3.921146 1.413969,3.20106 L 84.0332,-37.5 c 3.140324,-3.1096 5.216467,-6.583667 6.2744,-10.376 1.057933,-3.792333 1.057933,-7.584633 0,-11.3769 -1.057933,-3.792333 -3.165667,-7.267267 -6.3232,-10.4248 -3.1576,-3.190133 -6.632533,-5.306033 -10.4248,-6.3477 -3.792333,-1.041667 -7.584667,-1.033533 -11.377,0.0244 -3.792333,1.057933 -7.251,3.1494 -10.376,6.2744 l -13.0859,13.1348 c -0.955603,1.026581 2.33955,1.22023 3.207079,1.920075 0.902307,0.727901 1.024101,3.880642 2.066321,2.767425 z"
id="path5692"
sodipodi:nodetypes="cssscssccsccsscssccscccsscssscscssscssccsc" />
</g>
<g
id="Light-S"
transform="matrix(1 0 0 1 1104.26 696)">
<path
d="M 55.632527,-19.199791 42.8935,-6.32629 c -2.281867,2.27186 -4.790467,3.78604 -7.5258,4.54254 -2.735267,0.7564953 -5.4737,0.766582 -8.2153,0.03026 -2.7416,-0.73632 -5.243233,-2.23024 -7.5049,-4.48176 -2.261733,-2.2944133 -3.7689,-4.812363 -4.5215,-7.55385 -0.7526,-2.741533 -0.750633,-5.479967 0.0059,-8.2153 0.756467,-2.7354 2.275633,-5.232767 4.5575,-7.4921 L 30.84147,-41.03756 27.183,-44.6655 15.5665,-33.0338 c -3.006333,2.9838 -5.01265,6.2911 -6.01895,9.9219 -1.0063133,3.630867 -1.0113567,7.2668 -0.01513,10.9078 0.996253,3.641 3.018813,6.9746633 6.06768,10.00099 3.036467,3.03649333 6.372667,5.05595 10.0086,6.05837 3.635933,1.00242 7.2693,0.9973767 10.9001,-0.01513 3.630867,-1.0125067 6.938167,-3.01064667 9.9219,-5.99442 L 59.5014,-15.2588 Z M 42.105073,-51.774345 54.8594,-64.7827 c 2.259467,-2.271867 4.7625,-3.786067 7.5091,-4.5426 2.7466,-0.756467 5.490633,-0.766533 8.2321,-0.0302 2.741533,0.736333 5.238133,2.230333 7.4898,4.482 2.271733,2.284267 3.781367,4.799667 4.5289,7.5462 0.7476,2.7466 0.737533,5.487567 -0.0302,8.2229 -0.7678,2.735267 -2.281333,5.232667 -4.5406,7.4922 l -11.002406,11.54076 3.508506,3.62794 11.632,-11.6318 c 2.9836,-2.9836 4.9817,-6.290833 5.9943,-9.9217 1.0126,-3.630933 1.017667,-7.266867 0.0152,-10.9078 -1.002533,-3.640867 -3.016967,-6.974567 -6.0433,-10.0011 -3.036467,-3.058867 -6.375167,-5.083933 -10.0161,-6.0752 -3.640933,-0.9912 -7.2768,-0.9805 -10.9076,0.0321 -3.630867,1.0126 -6.933167,3.0107 -9.9069,5.9943 l -13.0707,13.1045 z"
id="path5695"
sodipodi:nodetypes="ccsscssccccsscsscccccsscsscccssscssccc" />
</g>
<g
id="Thin-S"
transform="matrix(1 0 0 1 808.985 696)">
<path
d="M 54.880454,-18.441738 42.2127,-5.84599 c -2.415267,2.3920533 -5.0773,3.9930133 -7.9861,4.80288 -2.908733,0.809864 -5.814,0.83329333 -8.7158,0.070288 -2.9018,-0.7630053 -5.5369,-2.316908 -7.9053,-4.661708 -2.3684,-2.4013733 -3.942267,-5.052863 -4.7216,-7.95447 -0.779267,-2.9016 -0.763967,-5.806867 0.0459,-8.7158 0.809867,-2.908933 2.422433,-5.5664 4.8377,-7.9724 L 28.2264,-40.7012 25.908825,-42.928292 15.1663,-32.2931 c -2.806333,2.797 -4.689283,5.9041 -5.64885,9.3213 -0.9595667,3.417267 -0.9712833,6.846333 -0.03515,10.2872 0.936133,3.4408867 2.835233,6.5877133 5.6973,9.44048 2.8764,2.8764 6.029133,4.77909 9.4582,5.70807 3.429067,0.92898 6.852233,0.9172667 10.2695,-0.03514 3.417267,-0.9524133 6.524367,-2.82711333 9.3213,-5.6241 L 57.2791,-16.2598 Z M 39.92273,-52.663057 52.6974,-65.2832 c 2.406333,-2.392067 5.0662,-3.993033 7.9796,-4.8029 2.9134,-0.809867 5.8209,-0.8333 8.7225,-0.0703 2.9016,0.763 5.525,2.3171 7.8702,4.6623 2.391667,2.377733 3.971333,5.023333 4.739,7.9368 0.767667,2.9134 0.744233,5.824467 -0.0703,8.7332 -0.814533,2.908733 -2.424767,5.5663 -4.8307,7.9727 l -10.424,10.4234 2.139243,2.08426 L 79.7444,-38.8361 c 2.7966,-2.7966 4.6712,-5.9036 5.6238,-9.321 0.9526,-3.417467 0.9643,-6.846533 0.0351,-10.2872 -0.929133,-3.440667 -2.8201,-6.5876 -5.6729,-9.4408 -2.8764,-2.885333 -6.034933,-4.790233 -9.4756,-5.7147 -3.440667,-0.924533 -6.869633,-0.9105 -10.2869,0.0421 -3.4172,0.9526 -6.5127,2.8272 -9.2865,5.6238 l -13.0505,13.0645 z"
id="path5698"
sodipodi:nodetypes="ccsscssccccsscsscccccsscssccccsscssccc" />
</g>
<g
id="Ultralight-S"
transform="matrix(1 0 0 1 513.007 696)">
<path
d="M 57.437797,-21.812649 41.864,-5.59998 c -2.4836,2.4536133 -5.2242,4.099021 -8.2218,4.936223 -2.9976,0.83719867 -5.988333,0.86746167 -8.9722,0.090789 -2.983867,-0.7766747 -5.687333,-2.3612987 -8.1104,-4.753872 -2.423067,-2.45616 -4.0311,-5.176047 -4.8241,-8.15966 -0.792933,-2.9836 -0.7708,-5.9743 0.0664,-8.9721 0.8372,-2.997867 2.4976,-5.737333 4.9812,-8.2184 L 29.73337,-44.070426 28.4241,-45.4224 14.9612,-31.9138 c -2.7038,2.701333 -4.523523,5.705933 -5.45917,9.0138 -0.93562,3.307867 -0.9507533,6.630967 -0.0454,9.9693 0.90538,3.33838 2.74127,6.38951 5.50767,9.15339 2.7944,2.79439067 5.853133,4.63727067 9.1762,5.52864 3.323133,0.8913667 6.638633,0.8762333 9.9465,-0.0454 3.307867,-0.92162933 6.312433,-2.7331027 9.0137,-5.43442 L 58.800508,-20.341088 Z M 38.832007,-52.827307 51.59,-65.5396 c 2.481533,-2.4536 5.221733,-4.099 8.2206,-4.9362 2.998867,-0.8372 5.9901,-0.867467 8.9737,-0.0908 2.9836,0.776667 5.671967,2.361567 8.0651,4.7547 2.453067,2.4256 4.068567,5.137833 4.8465,8.1367 0.778,2.998867 0.747733,5.9971 -0.0908,8.9947 -0.838467,2.9976 -2.498233,5.7372 -4.9793,8.2188 l -11.137709,11.271673 1.044641,1.183545 L 78.4935,-39.2258 c 2.7008,-2.7008 4.512133,-5.705233 5.434,-9.0133 0.921933,-3.308133 0.937067,-6.631267 0.0454,-9.9694 -0.8916,-3.338067 -2.719333,-6.3893 -5.4832,-9.1537 -2.7944,-2.796467 -5.860667,-4.639867 -9.1988,-5.5302 -3.338067,-0.890333 -6.661033,-0.874567 -9.9689,0.0473 -3.307867,0.921933 -6.297467,2.7333 -8.9688,5.4341 l -13.0401,13.0439 z"
id="path5701"
sodipodi:nodetypes="ccsscssccccsscsscccccsscssccccsscssccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -10,10 +10,10 @@ import UIKit
class ImageCache {
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24), desiredSize: CGSize(width: 50, height: 50))
static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24 * 7), desiredSize: CGSize(width: 50, height: 50))
static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
#if DEBUG
private static let disableCaching = ProcessInfo.processInfo.environment.keys.contains("DISABLE_IMAGE_CACHE")
@ -22,6 +22,7 @@ class ImageCache {
#endif
private let cache: ImageDataCache
private let desiredPixelSize: CGSize?
private var groups = MultiThreadDictionary<URL, RequestGroup>(name: "ImageCache request groups")
@ -30,28 +31,47 @@ class ImageCache {
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
self.desiredPixelSize = pixelSize
self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize)
}
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
let key = url.absoluteString
if !ImageCache.disableCaching,
let entry = try? cache.get(key, loadOriginal: loadOriginal) {
if let completion = completion {
backgroundQueue.async {
completion(entry.data, entry.image)
let wrappedCompletion: ((Data?, UIImage?) -> Void)?
if let completion = completion {
wrappedCompletion = { (data, image) in
if #available(iOS 15.0, *) {
if !loadOriginal,
let size = self.desiredPixelSize {
image?.prepareThumbnail(of: size, completionHandler: {
completion(data, $0)
})
} else {
image?.prepareForDisplay {
completion(data, $0)
}
}
} else {
self.backgroundQueue.async {
completion(data, image)
}
}
}
} else {
wrappedCompletion = nil
}
if !ImageCache.disableCaching,
let entry = try? cache.get(key, loadOriginal: loadOriginal) {
wrappedCompletion?(entry.data, entry.image)
return nil
} else {
if let group = groups[url] {
if let completion = completion {
return group.addCallback(completion)
}
return nil
return group.addCallback(wrappedCompletion)
} else {
let group = createGroup(url: url)
let request = group.addCallback(completion)
let request = group.addCallback(wrappedCompletion)
group.run()
return request
}
@ -122,21 +142,15 @@ class ImageCache {
task!.resume()
}
private func updatePriority() {
task?.priority = max(1.0, URLSessionTask.defaultPriority + 0.1 * Float(requests.filter { !$0.cancelled }.count))
}
func addCallback(_ completion: ((Data?, UIImage?) -> Void)?) -> Request {
let request = Request(callback: completion)
requests.append(request)
updatePriority()
return request
}
func cancelWithoutCallback() {
if let request = requests.first(where: { $0.callback == nil && !$0.cancelled }) {
request.cancel()
updatePriority()
}
}
@ -145,8 +159,6 @@ class ImageCache {
if remaining <= 0 {
task?.cancel()
complete(with: nil)
} else {
updatePriority()
}
}

View File

@ -44,6 +44,8 @@ class MastodonController: ObservableObject {
@Published private(set) var account: Account!
@Published private(set) var instance: Instance!
@Published private(set) var nodeInfo: NodeInfo!
@Published private(set) var instanceFeatures = InstanceFeatures()
private(set) var customEmojis: [Emoji]?
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
@ -56,7 +58,7 @@ class MastodonController: ObservableObject {
init(instanceURL: URL, transient: Bool = false) {
self.instanceURL = instanceURL
self.accountInfo = nil
self.client = Client(baseURL: instanceURL)
self.client = Client(baseURL: instanceURL, session: .appDefault)
self.transient = transient
}
@ -161,6 +163,7 @@ class MastodonController: ObservableObject {
DispatchQueue.main.async {
self.ownInstanceRequest = nil
self.instance = instance
self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo)
for completion in self.pendingOwnInstanceRequestCallbacks {
completion(instance)
@ -169,6 +172,21 @@ class MastodonController: ObservableObject {
}
}
}
client.nodeInfo { result in
switch result {
case let .failure(error):
print("Unable to get node info: \(error)")
case let .success(nodeInfo, _):
DispatchQueue.main.async {
self.nodeInfo = nodeInfo
if let instance = self.instance {
self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo)
}
}
}
}
}
}
}

View File

@ -45,6 +45,7 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
@NSManaged private var pollData: Data?
@NSManaged public var account: AccountMO
@NSManaged public var reblog: StatusMO?
@NSManaged public var localOnly: Bool
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
public var attachments: [Attachment]
@ -134,6 +135,7 @@ extension StatusMO {
self.url = status.url
self.visibility = status.visibility
self.poll = status.poll
self.localOnly = status.localOnly ?? false
if let existing = container.account(for: status.account.id, in: context) {
existing.updateFrom(apiAccount: status.account, container: container)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21C52" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="AccountMO" syncable="YES">
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" attributeType="URI"/>
@ -43,7 +43,7 @@
<entity name="Status" representedClassName="StatusMO" syncable="YES">
<attribute name="applicationName" optional="YES" attributeType="String"/>
<attribute name="attachmentsData" attributeType="Binary"/>
<attribute name="bookmarkedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="bookmarkedInternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="cardData" optional="YES" attributeType="Binary"/>
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
@ -54,6 +54,7 @@
<attribute name="id" attributeType="String"/>
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
<attribute name="localOnly" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mentionsData" attributeType="Binary"/>
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="pinnedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
@ -77,6 +78,6 @@
<elements>
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="329"/>
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
<element name="Status" positionX="-63" positionY="-18" width="128" height="434"/>
<element name="Status" positionX="-63" positionY="-18" width="128" height="449"/>
</elements>
</model>

View File

@ -0,0 +1,28 @@
//
// URLSession+Development.swift
// Tusker
//
// Created by Shadowfacts on 1/22/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
extension URLSession {
#if targetEnvironment(simulator) && DEBUG
static let appDefault = URLSession(configuration: .default, delegate: Delegate(), delegateQueue: nil)
#else
static let appDefault = URLSession.shared
#endif
}
#if targetEnvironment(simulator) && DEBUG
private class Delegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// allows testing with self-signed certificates in development
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
}
#endif

View File

@ -0,0 +1,64 @@
//
// InstanceFeatures.swift
// Tusker
//
// Created by Shadowfacts on 1/23/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
struct InstanceFeatures {
private(set) var instanceType = InstanceType.mastodon
private(set) var maxStatusChars = 500
var localOnlyPosts: Bool {
instanceType == .hometown || instanceType == .glitch
}
var mastodonAttachmentRestrictions: Bool {
instanceType.isMastodon
}
var pollsAndAttachments: Bool {
instanceType == .pleroma
}
var boostToOriginalAudience: Bool {
instanceType == .pleroma
}
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
let ver = instance.version.lowercased()
if ver.contains("glitch") {
instanceType = .glitch
} else if nodeInfo?.software.name == "hometown" {
instanceType = .hometown
} else if ver.contains("pleroma") {
instanceType = .pleroma
} else {
instanceType = .mastodon
}
maxStatusChars = instance.maxStatusCharacters ?? 500
}
}
extension InstanceFeatures {
enum InstanceType: Equatable {
case mastodon // vanilla
case pleroma
case hometown
case glitch
var isMastodon: Bool {
switch self {
case .mastodon, .hometown, .glitch:
return true
default:
return false
}
}
}
}

View File

@ -32,19 +32,6 @@ class LocalData: ObservableObject {
}
} else {
defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")!
tryMigrateOldDefaults()
}
}
// TODO: remove me before public beta
private func tryMigrateOldDefaults() {
let old = UserDefaults()
if let accounts = old.array(forKey: accountsKey) as? [[String: String]],
let mostRecentAccount = old.string(forKey: mostRecentAccountKey) {
defaults.setValue(accounts, forKey: accountsKey)
defaults.setValue(mostRecentAccount, forKey: mostRecentAccountKey)
old.removeObject(forKey: accountsKey)
old.removeObject(forKey: mostRecentAccountKey)
}
}

View File

@ -48,13 +48,13 @@ enum CompositionAttachmentData {
}
}
func getData(completion: @escaping (_ data: Data, _ mimeType: String) -> Void) {
func getData(completion: @escaping (Result<(Data, String), Error>) -> Void) {
switch self {
case let .image(image):
// Export as JPEG instead of PNG, otherweise photos straight from the camera are too large
// for Mastodon in its default configuration (max of 10MB).
// The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future.
completion(image.jpegData(compressionQuality: 0.8)!, "image/jpeg")
completion(.success((image.jpegData(compressionQuality: 0.8)!, "image/jpeg")))
case let .asset(asset):
if asset.mediaType == .image {
let options = PHImageRequestOptions()
@ -63,7 +63,10 @@ enum CompositionAttachmentData {
options.resizeMode = .none
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in
guard var data = data, let dataUTI = dataUTI else { fatalError() }
guard var data = data, let dataUTI = dataUTI else {
completion(.failure(.missingData))
return
}
let mimeType: String
if dataUTI == "public.heic" {
@ -77,7 +80,7 @@ enum CompositionAttachmentData {
mimeType = UTType(dataUTI)!.preferredMIMEType!
}
completion(data, mimeType)
completion(.success((data, mimeType)))
}
} else if asset.mediaType == .video {
let options = PHVideoRequestOptions()
@ -100,20 +103,23 @@ enum CompositionAttachmentData {
case let .drawing(drawing):
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
completion(image.pngData()!, "image/png")
completion(.success((image.pngData()!, "image/png")))
}
}
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) {
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, String), Error>) -> Void) {
session.outputFileType = .mp4
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
session.exportAsynchronously {
guard session.status == .completed else { fatalError("video export failed: \(String(describing: session.error))") }
guard session.status == .completed else {
completion(.failure(.export(session.error!)))
return
}
do {
let data = try Data(contentsOf: session.outputURL!)
completion(data, "video/mp4")
completion(.success((data, "video/mp4")))
} catch {
fatalError("Unable to load video: \(error)")
completion(.failure(.export(error)))
}
}
}
@ -121,6 +127,11 @@ enum CompositionAttachmentData {
enum AttachmentType {
case image, video
}
enum Error: Swift.Error {
case missingData
case export(Swift.Error)
}
}
extension PHAsset {

View File

@ -21,6 +21,7 @@ class Draft: Codable, ObservableObject {
@Published var inReplyToID: String?
@Published var visibility: Status.Visibility
@Published var poll: Poll?
@Published var localOnly: Bool
var initialText: String
@ -31,12 +32,6 @@ class Draft: Codable, ObservableObject {
poll?.hasContent == true
}
var textForPosting: String {
// when using dictation, iOS sometimes leaves a U+FFFC OBJECT REPLACEMENT CHARACTER behind in the text,
// which we want to strip out before actually posting the status
text.replacingOccurrences(of: "\u{fffc}", with: "")
}
init(accountID: String) {
self.id = UUID()
self.lastModified = Date()
@ -49,6 +44,7 @@ class Draft: Codable, ObservableObject {
self.inReplyToID = nil
self.visibility = Preferences.shared.defaultPostVisibility
self.poll = nil
self.localOnly = false
self.initialText = ""
}
@ -61,24 +57,13 @@ class Draft: Codable, ObservableObject {
self.accountID = try container.decode(String.self, forKey: .accountID)
self.text = try container.decode(String.self, forKey: .text)
if let enabled = try? container.decode(Bool.self, forKey: .contentWarningEnabled) {
self.contentWarningEnabled = enabled
self.contentWarning = try container.decode(String.self, forKey: .contentWarning)
} else {
// todo: temporary until migration away from old drafts manager is complete
let cw = try container.decode(String?.self, forKey: .contentWarning)
if let cw = cw {
self.contentWarningEnabled = !cw.isEmpty
self.contentWarning = cw
} else {
self.contentWarningEnabled = false
self.contentWarning = ""
}
}
self.contentWarningEnabled = try container.decode(Bool.self, forKey: .contentWarningEnabled)
self.contentWarning = try container.decode(String.self, forKey: .contentWarning)
self.attachments = try container.decode([CompositionAttachment].self, forKey: .attachments)
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility)
self.poll = try container.decode(Poll.self, forKey: .poll)
self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly) ?? false
self.initialText = try container.decode(String.self, forKey: .initialText)
}
@ -97,9 +82,23 @@ class Draft: Codable, ObservableObject {
try container.encode(inReplyToID, forKey: .inReplyToID)
try container.encode(visibility, forKey: .visibility)
try container.encode(poll, forKey: .poll)
try container.encode(localOnly, forKey: .localOnly)
try container.encode(initialText, forKey: .initialText)
}
func textForPosting(on instance: InstanceFeatures) -> String {
var text = self.text
// when using dictation, iOS sometimes leaves a U+FFFC OBJECT REPLACEMENT CHARACTER behind in the text,
// which we want to strip out before actually posting the status
text = text.replacingOccurrences(of: "\u{fffc}", with: "")
if localOnly && instance.instanceType == .glitch {
text += " 👁"
}
return text
}
}
extension Draft: Equatable {
@ -121,6 +120,7 @@ extension Draft {
case inReplyToID
case visibility
case poll
case localOnly
case initialText
}

View File

@ -18,16 +18,13 @@ protocol AssetCollectionViewControllerDelegate: AnyObject {
func captureFromCamera()
}
class AssetCollectionViewController: UICollectionViewController {
class AssetCollectionViewController: UIViewController, UICollectionViewDelegate {
weak var delegate: AssetCollectionViewControllerDelegate?
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var flowLayout: UICollectionViewFlowLayout {
return collectionViewLayout as! UICollectionViewFlowLayout
}
private var availableWidth: CGFloat!
private var thumbnailSize: CGSize!
private let imageManager = PHCachingImageManager()
@ -41,7 +38,7 @@ class AssetCollectionViewController: UICollectionViewController {
}
init() {
super.init(collectionViewLayout: UICollectionViewFlowLayout())
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
@ -51,6 +48,17 @@ class AssetCollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalWidth(1/3))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1/3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3)
group.interItemSpacing = .fixed(4)
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.delegate = self
view.addSubview(collectionView)
// use the safe area layout guide instead of letting it automatically use the safe area insets
// because otherwise, when presented in a popover with the arrow on the left or right side,
// the collection view content will be cut off by the width of the arrow because the popover
@ -73,16 +81,24 @@ class AssetCollectionViewController: UICollectionViewController {
collectionView.allowsSelection = true
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
let scale = UIScreen.main.scale
let cellSize = flowLayout.itemSize
thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
let controlCell = UICollectionView.CellRegistration<AssetPickerControlCollectionViewCell, Item> { cell, indexPath, itemIdentifier in
switch itemIdentifier {
case .showCamera:
cell.imageView.image = UIImage(systemName: "camera")
cell.label.text = "Take a Photo"
case .changeLimitedSelection:
cell.imageView.image = UIImage(systemName: "photo.on.rectangle.angled")
cell.label.text = "Select More Photos"
case .asset(_):
break
}
}
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
switch item {
case .showCamera:
return collectionView.dequeueReusableCell(withReuseIdentifier: cameraReuseIdentifier, for: indexPath)
case .showCamera, .changeLimitedSelection:
return collectionView.dequeueConfiguredReusableCell(using: controlCell, for: indexPath, item: item)
case let .asset(asset):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell
@ -107,30 +123,23 @@ class AssetCollectionViewController: UICollectionViewController {
let interactivePopGesture = navigationController?.interactivePopGestureRecognizer {
singleFingerPanGesture.require(toFail: interactivePopGesture)
}
PHPhotoLibrary.shared().register(self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let scale = UIScreen.main.scale
let cellWidth = view.bounds.width / 3
thumbnailSize = CGSize(width: cellWidth * scale, height: cellWidth * scale)
loadAssets()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let availableWidth = view.bounds.inset(by: view.safeAreaInsets).width
if self.availableWidth != availableWidth {
self.availableWidth = availableWidth
let size = (availableWidth - 8) / 3
flowLayout.itemSize = CGSize(width: size, height: size)
flowLayout.minimumInteritemSpacing = 4
flowLayout.minimumLineSpacing = 4
}
}
private func loadAssets() {
var items = [Item.showCamera]
switch PHPhotoLibrary.authorizationStatus(for: .readWrite) {
case .notDetermined:
PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in
@ -142,8 +151,11 @@ class AssetCollectionViewController: UICollectionViewController {
// todo: better UI for this
return
case .authorized, .limited:
// todo: show "add more" button for limited access
case .authorized:
break
case .limited:
items.append(.changeLimitedSelection)
break
@unknown default:
@ -156,7 +168,6 @@ class AssetCollectionViewController: UICollectionViewController {
fetchResult = fetchAssets(with: options)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.assets])
var items: [Item] = [.showCamera]
fetchResult.enumerateObjects { (asset, _, _) in
items.append(.asset(asset))
}
@ -176,11 +187,11 @@ class AssetCollectionViewController: UICollectionViewController {
// MARK: UICollectionViewDelegate
override func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool {
func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return false }
if let delegate = delegate,
case let .asset(asset) = item {
@ -189,29 +200,32 @@ class AssetCollectionViewController: UICollectionViewController {
return true
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .showCamera:
collectionView.deselectItem(at: indexPath, animated: false)
delegate?.captureFromCamera()
case .changeLimitedSelection:
// todo: change observer
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self)
case .asset(_):
updateItemsSelectedCount()
}
}
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
updateItemsSelectedCount()
}
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard case let .asset(asset) = dataSource.itemIdentifier(for: indexPath) else { return nil }
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
return AssetPreviewViewController(asset: asset)
}, actionProvider: nil)
}
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?,
let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell {
let parameters = UIPreviewParameters()
@ -237,6 +251,15 @@ extension AssetCollectionViewController {
}
enum Item: Hashable {
case showCamera
case changeLimitedSelection
case asset(PHAsset)
}
}
extension AssetCollectionViewController: PHPhotoLibraryChangeObserver {
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
self.loadAssets()
}
}
}

View File

@ -97,7 +97,18 @@ struct ComposeAttachmentRow: View {
mode = .recognizingText
DispatchQueue.global(qos: .userInitiated).async {
self.attachment.data.getData { (data, mimeType) in
self.attachment.data.getData { (result) in
let data: Data
do {
try data = result.get().0
} catch {
DispatchQueue.main.async {
self.mode = .allowEntry
self.isShowingTextRecognitionFailedAlert = true
self.textRecognitionErrorMessage = error.localizedDescription
}
return
}
let handler = VNImageRequestHandler(data: data, options: [:])
let request = VNRecognizeTextRequest { (request, error) in
DispatchQueue.main.async {

View File

@ -87,23 +87,17 @@ struct ComposeAttachmentsList: View {
}
private var canAddAttachment: Bool {
switch mastodonController.instance?.instanceType {
case nil:
return false
case .pleroma:
return true
case .mastodon:
if mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
return draft.attachments.count < 4 && draft.attachments.allSatisfy { $0.data.type == .image } && draft.poll == nil
} else {
return true
}
}
private var canAddPoll: Bool {
switch mastodonController.instance?.instanceType {
case nil:
return false
case .pleroma:
if mastodonController.instanceFeatures.pollsAndAttachments {
return true
case .mastodon:
} else {
return draft.attachments.isEmpty
}
}

View File

@ -32,6 +32,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
private var mainToolbar: UIToolbar!
private var inputAccessoryToolbar: UIToolbar!
private var visibilityBarButtonItems = [UIBarButtonItem]()
private var localOnlyItems = [UIBarButtonItem]()
override var inputAccessoryView: UIView? { inputAccessoryToolbar }
@ -54,6 +55,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
self.uiState.delegate = self
// main toolbar is shown at the bottom of the screen, the input accessory is attached to the keyboard while editing
// (except for MainComposeTextView which has its own accessory to add formatting buttons)
mainToolbar = createToolbar()
inputAccessoryToolbar = createToolbar()
@ -73,6 +75,11 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
.sink(receiveValue: self.visibilityChanged)
.store(in: &cancellables)
self.uiState.$draft
.flatMap(\.$localOnly)
.sink(receiveValue: self.localOnlyChanged)
.store(in: &cancellables)
self.uiState.$draft
.flatMap(\.objectWillChange)
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.global(qos: .utility))
@ -114,7 +121,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
toolbar.translatesAutoresizingMaskIntoConstraints = false
toolbar.isAccessibilityElement = true
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: nil, action: nil)
let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
visibilityBarButtonItems.append(visibilityItem)
visibilityChanged(draft.visibility)
@ -124,6 +131,14 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPresed))
]
if mastodonController.instanceFeatures.localOnlyPosts {
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
toolbar.items!.insert(item, at: 2)
localOnlyItems.append(item)
localOnlyChanged(draft.localOnly)
}
return toolbar
}
@ -185,11 +200,10 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
private func visibilityChanged(_ newVisibility: Status.Visibility) {
for item in visibilityBarButtonItems {
item.image = UIImage(systemName: newVisibility.imageName)
item.image!.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
let state = visibility == newVisibility ? UIMenuElement.State.on : .off
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { [unowned self] (_) in
self.draft.visibility = visibility
}
}
@ -197,15 +211,35 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
}
}
private func localOnlyChanged(_ localOnly: Bool) {
for item in localOnlyItems {
if localOnly {
item.image = UIImage(named: "link.broken")
item.accessibilityLabel = "Local-only"
} else {
item.image = UIImage(systemName: "link")
item.accessibilityLabel = "Federated"
}
item.menu = UIMenu(children: [
// todo: iOS 15, action subtitles
UIAction(title: "Local-only", image: UIImage(named: "link.broken"), state: localOnly ? .on : .off) { [unowned self] (_) in
self.draft.localOnly = true
},
UIAction(title: "Federated", image: UIImage(systemName: "link"), state: localOnly ? .off : .on) { [unowned self] (_) in
self.draft.localOnly = false
},
])
}
}
override func canPaste(_ itemProviders: [NSItemProvider]) -> Bool {
guard itemProviders.allSatisfy({ $0.canLoadObject(ofClass: CompositionAttachment.self) }) else { return false }
switch mastodonController.instance.instanceType {
case .pleroma:
return true
case .mastodon:
if mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
guard draft.attachments.allSatisfy({ $0.data.type == .image }) else { return false }
// todo: if providers are videos, this technically allows invalid video/image combinations
return itemProviders.count + draft.attachments.count <= 4
} else {
return true
}
}
@ -286,16 +320,15 @@ extension ComposeHostingController: ComposeUIStateDelegate {
extension ComposeHostingController: AssetPickerViewControllerDelegate {
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool {
switch mastodonController.instance.instanceType {
case .pleroma:
return true
case .mastodon:
if mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
if (type == .video && draft.attachments.count > 0) ||
draft.attachments.contains(where: { $0.data.type == .video }) ||
assetPicker.currentCollectionSelectedAssets.contains(where: { $0.type == .video }) {
return false
}
return draft.attachments.count + assetPicker.currentCollectionSelectedAssets.count < 4
} else {
return true
}
}

View File

@ -59,19 +59,21 @@ struct ComposePollView: View {
}
HStack {
// use .animation(nil) on the binding and .frame(maxWidth: .infinity) on labels so frame doesn't have a size change animation when the text changes
Picker(selection: $poll.multiple.animation(nil), label: Text(poll.multiple ? "Allow multiple choices" : "Single choice").frame(maxWidth: .infinity)) {
// use .animation(nil) on pickers so frame doesn't have a size change animation when the text changes
Picker(selection: $poll.multiple, label: Text(poll.multiple ? "Allow multiple choices" : "Single choice")) {
Text("Allow multiple choices").tag(true)
Text("Single choice").tag(false)
}
.animation(nil)
.pickerStyle(MenuPickerStyle())
.frame(maxWidth: .infinity)
Picker(selection: $duration.animation(nil), label: Text(verbatim: ComposePollView.formatter.string(from: duration.timeInterval)!).frame(maxWidth: .infinity)) {
Picker(selection: $duration, label: Text(verbatim: ComposePollView.formatter.string(from: duration.timeInterval)!)) {
ForEach(Duration.allCases, id: \.self) { (duration) in
Text(ComposePollView.formatter.string(from: duration.timeInterval)!).tag(duration)
}
}
.animation(nil)
.pickerStyle(MenuPickerStyle())
.frame(maxWidth: .infinity)
}

View File

@ -28,7 +28,7 @@ struct ComposeView: View {
}
var charactersRemaining: Int {
let limit = mastodonController.instance?.maxStatusCharacters ?? 500
let limit = mastodonController.instanceFeatures.maxStatusChars
let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0
return limit - (cwCount + CharacterCounter.count(text: draft.text, for: mastodonController.instance))
}
@ -65,7 +65,6 @@ struct ComposeView: View {
autocompleteSuggestions
}
.onAppear(perform: self.didAppear)
.navigationBarTitle("Compose")
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
.alert(isPresented: $isShowingPostErrorAlert) {
@ -154,11 +153,6 @@ struct ComposeView: View {
.disabled(!postButtonEnabled)
}
private func didAppear() {
let proxy = UIScrollView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
proxy.keyboardDismissMode = .interactive
}
private func cancel() {
if Preferences.shared.automaticallySaveDrafts {
// draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear
@ -215,7 +209,7 @@ struct ComposeView: View {
self.isPosting = false
case let .success(uploadedAttachments):
let request = Client.createStatus(text: draft.textForPosting,
let request = Client.createStatus(text: draft.textForPosting(on: mastodonController.instanceFeatures),
contentType: Preferences.shared.statusContentType,
inReplyTo: draft.inReplyToID,
media: uploadedAttachments,
@ -225,7 +219,8 @@ struct ComposeView: View {
language: nil,
pollOptions: draft.poll?.options.map(\.text),
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
pollMultiple: draft.poll?.multiple)
pollMultiple: draft.poll?.multiple,
localOnly: mastodonController.instanceFeatures.instanceType == .hometown ? draft.localOnly : nil)
self.mastodonController.run(request) { (response) in
switch response {
case let .failure(error):
@ -250,17 +245,17 @@ struct ComposeView: View {
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
let group = DispatchGroup()
var attachmentDatas = [(Data, String)?]()
var attachmentDataResults = [Result<(Data, String), CompositionAttachmentData.Error>?]()
for (index, compAttachment) in draft.attachments.enumerated() {
group.enter()
attachmentDatas.append(nil)
attachmentDataResults.append(nil)
compAttachment.data.getData { (data, mimeType) in
compAttachment.data.getData { (result) in
postProgress += 1
attachmentDatas[index] = (data, mimeType)
attachmentDataResults[index] = result
group.leave()
}
}
@ -277,7 +272,15 @@ struct ComposeView: View {
// posted status reflects order the user set.
// Pleroma does respect the order of the `media_ids` parameter.
for (index, (data, mimeType)) in attachmentDatas.map(\.unsafelyUnwrapped).enumerated() {
let datas: [(Data, String)]
do {
datas = try attachmentDataResults.map { try $0!.get() }
} catch {
completion(.failure(AttachmentUploadError(errors: [error])))
return
}
for (index, (data, mimeType)) in datas.enumerated() {
group.enter()
let compAttachment = draft.attachments[index]

View File

@ -8,6 +8,7 @@
import UIKit
import Pachyderm
import WebURLFoundationExtras
class EmojiCollectionViewCell: UICollectionViewCell {
@ -45,7 +46,7 @@ class EmojiCollectionViewCell: UICollectionViewCell {
func updateUI(emoji: Emoji) {
currentEmojiShortcode = emoji.shortcode
imageRequest = ImageCache.emojis.get(emoji.url) { [weak self] (_, image) in
imageRequest = ImageCache.emojis.get(URL(emoji.url)!) { [weak self] (_, image) in
guard let image = image else { return }
DispatchQueue.main.async { [weak self] in
guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return }

View File

@ -56,7 +56,10 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
var textDidChange: (UITextView) -> Void
@EnvironmentObject var uiState: ComposeUIState
@EnvironmentObject var mastodonController: MastodonController
// todo: should these be part of the coordinator?
@State var visibilityButton: UIBarButtonItem?
@State var localOnlyButton: UIBarButtonItem?
func makeUIView(context: Context) -> UITextView {
let textView = WrappedTextView()
@ -87,6 +90,22 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
self.visibilityButton = visibilityButton
}
if mastodonController.instanceFeatures.localOnlyPosts {
let image: UIImage
if uiState.draft.localOnly {
image = UIImage(named: "link.broken")!
} else {
image = UIImage(systemName: "link")!
}
let item = UIBarButtonItem(image: image, style: .plain, target: nil, action: nil)
toolbar.items!.insert(item, at: 2)
updateLocalOnlyMenu(item)
DispatchQueue.main.async {
self.localOnlyButton = item
}
}
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
@ -134,6 +153,17 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
visibilityButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
}
private func updateLocalOnlyMenu(_ localOnlyButton: UIBarButtonItem) {
localOnlyButton.menu = UIMenu(children: [
UIAction(title: "Local-only", image: UIImage(named: "link.broken"), state: uiState.draft.localOnly ? .on : .off) { (_) in
self.uiState.draft.localOnly = true
},
UIAction(title: "Federated", image: UIImage(systemName: "link"), state: uiState.draft.localOnly ? .off : .on) { (_) in
self.uiState.draft.localOnly = false
},
])
}
func updateUIView(_ uiView: UITextView, context: Context) {
if context.coordinator.skipSettingTextOnNextUpdate {
context.coordinator.skipSettingTextOnNextUpdate = false
@ -145,6 +175,14 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
visibilityButton.image = UIImage(systemName: visibility.imageName)
updateVisibilityMenu(visibilityButton)
}
if let localOnlyButton = localOnlyButton {
if uiState.draft.localOnly {
localOnlyButton.image = UIImage(named: "link.broken")
} else {
localOnlyButton.image = UIImage(systemName: "link")
}
updateLocalOnlyMenu(localOnlyButton)
}
context.coordinator.text = $text
context.coordinator.didChange = textDidChange
context.coordinator.uiState = uiState

View File

@ -138,7 +138,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
if case .mastodon = mastodonController.instance?.instanceType {
if mastodonController.instanceFeatures.instanceType.isMastodon {
snapshot.insertSections([.discover], afterSection: .bookmarks)
snapshot.appendItems([.trendingTags, .profileDirectory], toSection: .discover)
}
@ -154,7 +154,8 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
private func ownInstanceLoaded(_ instance: Instance) {
var snapshot = self.dataSource.snapshot()
if case .mastodon = instance.instanceType {
if mastodonController.instanceFeatures.instanceType.isMastodon,
!snapshot.sectionIdentifiers.contains(.discover) {
snapshot.insertSections([.discover], afterSection: .bookmarks)
snapshot.appendItems([.trendingTags, .profileDirectory], toSection: .discover)
}

View File

@ -11,6 +11,7 @@ import Pachyderm
class FeaturedProfileCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var clippingView: UIView!
@IBOutlet weak var headerImageView: UIImageView!
@IBOutlet weak var avatarContainerView: UIView!
@IBOutlet weak var avatarImageView: UIImageView!
@ -30,6 +31,17 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
noteTextView.defaultFont = .systemFont(ofSize: 16)
noteTextView.textContainer.lineBreakMode = .byTruncatingTail
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
backgroundColor = .clear
clippingView.layer.cornerRadius = 5
clippingView.layer.borderWidth = 1
clippingView.layer.masksToBounds = true
layer.shadowOpacity = 0.2
layer.shadowRadius = 8
layer.shadowOffset = .zero
layer.masksToBounds = false
updateLayerColors()
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
@ -75,6 +87,27 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
}
}
private func updateLayerColors() {
if traitCollection.userInterfaceStyle == .dark {
clippingView.layer.borderColor = UIColor.darkGray.withAlphaComponent(0.5).cgColor
layer.shadowColor = UIColor.darkGray.cgColor
} else {
clippingView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
layer.shadowColor = UIColor.black.cgColor
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors()
}
override func layoutSubviews() {
super.layoutSubviews()
layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: 5, cornerHeight: 5, transform: nil)
}
@objc private func preferencesChanged() {
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18121" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -17,67 +18,81 @@
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bo4-Sd-caI">
<rect key="frame" x="0.0" y="0.0" width="400" height="66"/>
<color key="backgroundColor" systemColor="systemGray5Color"/>
<constraints>
<constraint firstAttribute="height" constant="66" id="9Aa-Up-chJ"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RQe-uE-TEv">
<rect key="frame" x="8" y="34" width="64" height="64"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YkJ-rV-f3C">
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4wd-wq-Sh2">
<rect key="frame" x="2" y="2" width="60" height="60"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bo4-Sd-caI">
<rect key="frame" x="0.0" y="0.0" width="400" height="66"/>
<color key="backgroundColor" systemColor="systemGray5Color"/>
<constraints>
<constraint firstAttribute="width" constant="60" id="Xyl-Ry-J3r"/>
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="height" multiplier="1:1" id="YEc-fT-FRB"/>
<constraint firstAttribute="height" constant="66" id="9Aa-Up-chJ"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RQe-uE-TEv">
<rect key="frame" x="8" y="34" width="64" height="64"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4wd-wq-Sh2">
<rect key="frame" x="2" y="2" width="60" height="60"/>
<constraints>
<constraint firstAttribute="width" constant="60" id="Xyl-Ry-J3r"/>
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="height" multiplier="1:1" id="YEc-fT-FRB"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="width" secondItem="RQe-uE-TEv" secondAttribute="height" multiplier="1:1" id="4vR-IF-yS8"/>
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="width" constant="4" id="52Q-zq-k28"/>
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerY" secondItem="RQe-uE-TEv" secondAttribute="centerY" id="Ped-H7-QtP"/>
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="voW-Is-1b2" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="76" y="72" width="316" height="24"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="8" y="102" width="384" height="98"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="width" secondItem="RQe-uE-TEv" secondAttribute="height" multiplier="1:1" id="4vR-IF-yS8"/>
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="width" constant="4" id="52Q-zq-k28"/>
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerY" secondItem="RQe-uE-TEv" secondAttribute="centerY" id="Ped-H7-QtP"/>
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/>
<constraint firstAttribute="trailing" secondItem="bvj-F0-ggC" secondAttribute="trailing" constant="8" id="1sd-Df-jR1"/>
<constraint firstItem="bvj-F0-ggC" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" constant="8" id="35h-Wh-fvk"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="top" relation="greaterThanOrEqual" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="39l-yo-g8V"/>
<constraint firstItem="bo4-Sd-caI" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" id="3lQ-uN-93N"/>
<constraint firstAttribute="trailing" secondItem="voW-Is-1b2" secondAttribute="trailing" constant="8" id="Ckp-Bq-lB5"/>
<constraint firstItem="bo4-Sd-caI" firstAttribute="top" secondItem="YkJ-rV-f3C" secondAttribute="top" id="DWh-S5-PLQ"/>
<constraint firstAttribute="bottom" secondItem="bvj-F0-ggC" secondAttribute="bottom" id="MH3-7E-THx"/>
<constraint firstItem="RQe-uE-TEv" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" constant="8" id="Tzo-aN-Bxq"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="bottom" secondItem="4wd-wq-Sh2" secondAttribute="bottom" id="Wk6-u2-azz"/>
<constraint firstItem="RQe-uE-TEv" firstAttribute="centerY" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="bon-bj-qnk"/>
<constraint firstItem="bvj-F0-ggC" firstAttribute="top" secondItem="RQe-uE-TEv" secondAttribute="bottom" constant="4" id="dyg-LN-BDn"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="leading" secondItem="RQe-uE-TEv" secondAttribute="trailing" constant="4" id="shC-67-vC2"/>
<constraint firstAttribute="trailing" secondItem="bo4-Sd-caI" secondAttribute="trailing" id="wZn-gO-zue"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="voW-Is-1b2" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="76" y="72" width="316" height="24"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="8" y="102" width="384" height="94"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
</view>
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="bvj-F0-ggC" firstAttribute="top" secondItem="RQe-uE-TEv" secondAttribute="bottom" constant="4" id="8Nc-FF-kRX"/>
<constraint firstItem="bo4-Sd-caI" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="CJ1-Be-L45"/>
<constraint firstAttribute="bottom" secondItem="bvj-F0-ggC" secondAttribute="bottom" constant="4" id="Hza-qE-Agk"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="bottom" secondItem="4wd-wq-Sh2" secondAttribute="bottom" id="N0l-fE-AAX"/>
<constraint firstItem="RQe-uE-TEv" firstAttribute="centerY" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="Ngh-DO-Q0X"/>
<constraint firstItem="bvj-F0-ggC" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" constant="8" id="Rjq-1i-PV2"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="leading" secondItem="RQe-uE-TEv" secondAttribute="trailing" constant="4" id="WUb-3i-BFe"/>
<constraint firstAttribute="trailing" secondItem="bvj-F0-ggC" secondAttribute="trailing" constant="8" id="ZrT-Wa-pbY"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="top" relation="greaterThanOrEqual" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="g4l-yF-2wH"/>
<constraint firstAttribute="trailing" secondItem="bo4-Sd-caI" secondAttribute="trailing" id="geb-Qa-zZp"/>
<constraint firstAttribute="trailing" secondItem="voW-Is-1b2" secondAttribute="trailing" constant="8" id="l91-F6-kAL"/>
<constraint firstItem="bo4-Sd-caI" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="tUr-Oy-nXN"/>
<constraint firstItem="RQe-uE-TEv" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" constant="8" id="uZI-LM-bZW"/>
<constraint firstAttribute="trailing" secondItem="YkJ-rV-f3C" secondAttribute="trailing" id="Dy3-h1-zfM"/>
<constraint firstItem="YkJ-rV-f3C" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="Gld-3x-oE0"/>
<constraint firstItem="YkJ-rV-f3C" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="NIV-n8-4Rl"/>
<constraint firstAttribute="bottom" secondItem="YkJ-rV-f3C" secondAttribute="bottom" id="zNw-2z-Hlx"/>
</constraints>
<connections>
<outlet property="avatarContainerView" destination="RQe-uE-TEv" id="tBI-fT-26P"/>
<outlet property="avatarImageView" destination="4wd-wq-Sh2" id="rba-cv-8fb"/>
<outlet property="clippingView" destination="YkJ-rV-f3C" id="hLI-4z-yIc"/>
<outlet property="displayNameLabel" destination="voW-Is-1b2" id="XVS-4d-PKx"/>
<outlet property="headerImageView" destination="bo4-Sd-caI" id="YkL-Wi-BXb"/>
<outlet property="noteTextView" destination="bvj-F0-ggC" id="Bbm-ai-bu1"/>

View File

@ -44,11 +44,13 @@ class ProfileDirectoryFilterView: UICollectionReusableView {
fromLabel.translatesAutoresizingMaskIntoConstraints = false
fromLabel.text = NSLocalizedString("From", comment: "profile directory scope label")
fromLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
fromLabel.textAlignment = .right
let sortLabel = UILabel()
sortLabel.translatesAutoresizingMaskIntoConstraints = false
sortLabel.text = NSLocalizedString("Sort By", comment: "profile directory sort label")
sortLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
sortLabel.textAlignment = .right
let labelContainer = UIView()
labelContainer.addSubview(sortLabel)

View File

@ -45,7 +45,7 @@ class ProfileDirectoryViewController: UIViewController {
if case .compact = layoutEnvironment.traitCollection.horizontalSizeClass {
itemWidth = .fractionalWidth(1)
} else {
itemWidth = .absolute((layoutEnvironment.container.contentSize.width - 12) / 2)
itemWidth = .absolute((layoutEnvironment.container.contentSize.width - 16 - 8 * 2) / 2)
}
let itemSize = NSCollectionLayoutSize(widthDimension: itemWidth, heightDimension: itemHeight)
@ -54,15 +54,11 @@ class ProfileDirectoryViewController: UIViewController {
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: itemHeight)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, itemB])
group.interItemSpacing = .flexible(4)
group.interItemSpacing = .flexible(16)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 4
if case .compact = layoutEnvironment.traitCollection.horizontalSizeClass {
section.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0)
} else {
section.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
}
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
return section
}, configuration: configuration)

View File

@ -19,6 +19,8 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
self.mastodonController = mastodonController
super.init(style: .grouped)
dragEnabled = true
}
required init?(coder: NSCoder) {
@ -33,8 +35,6 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
// todo: enable drag
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView) { (tableView, indexPath, item) in
switch item {
case let .tag(hashtag):
@ -75,6 +75,31 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
}
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else {
return nil
}
return UIContextMenuConfiguration(identifier: nil) {
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
} actionProvider: { (_) in
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.tableView.cellForRow(at: indexPath)))
}
}
override func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else {
return []
}
let provider = NSItemProvider(object: hashtag.url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
provider.registerObject(activity, visibility: .all)
}
return [UIDragItem(itemProvider: provider)]
}
}
extension TrendingHashtagsViewController {
@ -85,3 +110,11 @@ extension TrendingHashtagsViewController {
case tag(Hashtag)
}
}
extension TrendingHashtagsViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController }
}
extension TrendingHashtagsViewController: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { self }
}

View File

@ -125,7 +125,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// todo: does this need to be in viewDidLayoutSubviews?
// limit the image height to the safe area height, so the image doesn't overlap the top controls
// while zoomed all the way out
let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
@ -138,7 +137,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
centerImage()
// todo: does this need to be in viewDidLayoutSubviews?
let notchedDeviceTopInsets: [CGFloat] = [
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
48, // iPhone XR, 11

View File

@ -145,7 +145,7 @@ class MainSidebarViewController: UIViewController {
snapshot.appendItems([
.tab(.compose)
], toSection: .compose)
if case .mastodon = mastodonController.instance?.instanceType {
if mastodonController.instanceFeatures.instanceType.isMastodon {
snapshot.insertSections([.discover], afterSection: .compose)
snapshot.appendItems([
.trendingTags,
@ -161,7 +161,8 @@ class MainSidebarViewController: UIViewController {
private func ownInstanceLoaded(_ instance: Instance) {
var snapshot = self.dataSource.snapshot()
if case .mastodon = mastodonController.instance?.instanceType {
if mastodonController.instanceFeatures.instanceType.isMastodon,
!snapshot.sectionIdentifiers.contains(.discover) {
snapshot.insertSections([.discover], afterSection: .compose)
snapshot.appendItems([
.trendingTags,

View File

@ -131,7 +131,7 @@ class InstanceSelectorTableViewController: UITableViewController {
let components = parseURLComponents(input: domain)
let url = components.url!
let client = Client(baseURL: url)
let client = Client(baseURL: url, session: .appDefault)
let request = Client.getInstance()
client.run(request) { (response) in
var snapshot = self.dataSource.snapshot()
@ -178,7 +178,7 @@ class InstanceSelectorTableViewController: UITableViewController {
private func createActivityIndicatorHeader() {
let header = UITableViewHeaderFooterView()
header.translatesAutoresizingMaskIntoConstraints = false
header.contentView.backgroundColor = .secondarySystemBackground
header.contentView.backgroundColor = .systemGroupedBackground
activityIndicator = UIActivityIndicatorView(style: .large)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false

View File

@ -9,7 +9,7 @@
import UIKit
import Pachyderm
class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEntry> {
class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<ProfileStatusesViewController.Section, ProfileStatusesViewController.Item> {
weak var mastodonController: MastodonController!
@ -43,50 +43,52 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
}
func updateUI(account: AccountMO) {
loadInitial()
if isViewLoaded {
reloadInitial()
}
}
override class func refreshCommandTitle() -> String {
return NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title")
}
override func headerSectionsCount() -> Int {
return 1
// MARK: - DiffableTimelineLikeTableViewController
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
cell.delegate = self
// todo: dataSource.sectionIdentifier is only available on iOS 15
cell.showPinned = dataSource.snapshot().indexOfSection(.pinned) == indexPath.section
cell.updateUI(statusID: item.id, state: item.state)
return cell
}
override func loadInitial() {
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
guard accountID != nil else {
completion(.failure(.noClient))
return
}
if !loaded {
loadPinnedStatuses()
}
super.loadInitial()
}
private func loadPinnedStatuses() {
guard kind == .statuses else {
return
}
getPinnedStatuses { (response) in
guard case let .success(statuses, _) = response,
!statuses.isEmpty else {
// todo: error message
return
}
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
let items = statuses.map { ($0.id, StatusState.unknown) }
DispatchQueue.main.async {
UIView.performWithoutAnimation {
if self.sections.count < 1 {
self.sections.append(items)
self.tableView.insertSections(IndexSet(integer: 0), with: .none)
getStatuses { (response) in
switch response {
case let .failure(error):
completion(.failure(.client(error)))
case let .success(statuses, pagination):
self.older = pagination?.older
self.newer = pagination?.newer
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
DispatchQueue.main.async {
var snapshot = self.dataSource.snapshot()
snapshot.appendSections([.statuses])
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .statuses)
if self.kind == .statuses {
self.loadPinnedStatuses(snapshot: { snapshot }, completion: completion)
} else {
self.sections[0] = items
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
completion(.success(snapshot))
}
}
}
@ -94,64 +96,94 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
}
}
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) {
getStatuses { (response) in
guard case let .success(statuses, pagination) = response,
!statuses.isEmpty else {
// todo: error message
completion([])
return
}
self.older = pagination?.older
self.newer = pagination?.newer
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
completion(statuses.map { ($0.id, .unknown) })
private func loadPinnedStatuses(snapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
guard kind == .statuses else {
completion(.success(snapshot()))
return
}
getPinnedStatuses { (response) in
switch response {
case let .failure(error):
completion(.failure(.client(error)))
case let .success(statuses, _):
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
DispatchQueue.main.async {
var snapshot = snapshot()
if snapshot.indexOfSection(.pinned) != nil {
snapshot.deleteSections([.pinned])
}
snapshot.insertSections([.pinned], beforeSection: .statuses)
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .pinned)
completion(.success(snapshot))
}
}
}
}
}
override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) {
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
guard let older = older else {
completion([])
completion(.failure(.noOlder))
return
}
getStatuses(for: older) { (response) in
guard case let .success(statuses, pagination) = response else {
// todo: error message
completion([])
return
}
self.older = pagination?.older
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
completion(statuses.map { ($0.id, .unknown) })
switch response {
case let .failure(error):
completion(.failure(.client(error)))
case let .success(statuses, pagination):
guard !statuses.isEmpty else {
completion(.failure(.noOlder))
return
}
if let older = pagination?.older {
self.older = older
}
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
var snapshot = currentSnapshot()
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .statuses)
completion(.success(snapshot))
}
}
}
}
override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) {
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
guard let newer = newer else {
completion([])
completion(.failure(.noNewer))
return
}
getStatuses(for: newer) { (response) in
guard case let .success(statuses, pagination) = response else {
// todo: error message
completion([])
return
}
if let newer = pagination?.newer {
self.newer = newer
}
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
completion(statuses.map { ($0.id, .unknown) })
switch response {
case let .failure(error):
completion(.failure(.client(error)))
case let .success(statuses, pagination):
guard !statuses.isEmpty else {
completion(.failure(.noNewer))
return
}
if let newer = pagination?.newer {
self.newer = newer
}
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
var snapshot = currentSnapshot()
let items = statuses.map { Item(id: $0.id, state: .unknown) }
if let first = snapshot.itemIdentifiers(inSection: .statuses).first {
snapshot.insertItems(items, beforeItem: first)
} else {
snapshot.appendItems(items, toSection: .statuses)
}
completion(.success(snapshot))
}
}
}
}
@ -178,53 +210,20 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
super.refresh()
if kind == .statuses {
getPinnedStatuses { (response) in
guard case let .success(newPinnedStatues, _) = response else {
// todo: error message
return
}
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatues) {
// if the user refreshes before the initial pinned statuses request completes, self.sections will be empty
let oldPinnedStatuses = self.sections.isEmpty ? [] : self.sections[0]
let pinnedStatues = newPinnedStatues.map { (status) -> TimelineEntry in
let state: StatusState
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
state = oldState
} else {
state = .unknown
}
return (status.id, state)
}
loadPinnedStatuses(snapshot: dataSource.snapshot) { (result) in
switch result {
case .failure(_):
break
case let .success(snapshot):
DispatchQueue.main.async {
UIView.performWithoutAnimation {
if self.sections.count < 1 {
self.sections.append(pinnedStatues)
self.tableView.insertSections(IndexSet(integer: 0), with: .none)
} else {
self.sections[0] = pinnedStatues
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
}
}
self.dataSource.apply(snapshot)
}
}
}
}
}
// MARK: - UITableViewDatasource
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
cell.delegate = self
cell.showPinned = indexPath.section == 0
let (id, state) = item(for: indexPath)
cell.updateUI(statusID: id, state: state)
return cell
}
}
extension ProfileStatusesViewController {
@ -233,6 +232,17 @@ extension ProfileStatusesViewController {
}
}
extension ProfileStatusesViewController {
enum Section: CaseIterable {
case pinned
case statuses
}
struct Item: Hashable {
let id: String
let state: StatusState
}
}
extension ProfileStatusesViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController }
}
@ -245,18 +255,12 @@ extension ProfileStatusesViewController: StatusTableViewCellDelegate {
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let ids = indexPaths.map { item(for: $0).id }
let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id }
prefetchStatuses(with: ids)
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
let ids: [String] = indexPaths.compactMap {
guard $0.section < sections.count,
$0.row < sections[$0.section].count else {
return nil
}
return item(for: $0).id
}
let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id }
cancelPrefetchingStatuses(with: ids)
}
}

View File

@ -211,9 +211,8 @@ class ProfileViewController: UIPageViewController {
// Layout and update the table view, otherwise the content jumps around when first scrolling it,
// if old was not scrolled all the way to the top
new.tableView.layoutIfNeeded()
UIView.performWithoutAnimation {
new.tableView.performBatchUpdates(nil, completion: nil)
}
let snapshot = new.dataSource.snapshot()
new.dataSource.apply(snapshot, animatingDifferences: false)
completion?(finished)
}

View File

@ -67,8 +67,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
}
private func pruneOffscreenRows() {
guard let lastVisibleRow = lastLastVisibleRow,
lastVisibleRow.section < tableView.numberOfSections else {
guard let lastVisibleRow = tableView.indexPathsForVisibleRows?.last else {
return
}
@ -77,30 +76,24 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
let lastVisibleRowSection = snapshot.sectionIdentifiers[lastVisibleRow.section]
let contentSections = snapshot.sectionIdentifiers.filter { timelineContentSections().contains($0) }
let contentSectionIndices = contentSections.compactMap(snapshot.indexOfSection(_:))
let maxContentSectionIndex = contentSectionIndices.max()!
guard let lastVisibleContentSectionIndex = contentSections.lastIndex(of: lastVisibleRowSection) else {
if lastVisibleRow.section < maxContentSectionIndex {
return
}
if lastVisibleContentSectionIndex < contentSections.count - 1 {
// there are more content sections below the current last visible one
let sectionsToRemove = contentSections[lastVisibleContentSectionIndex...]
snapshot.deleteSections(Array(sectionsToRemove))
willRemoveItems(sectionsToRemove.flatMap(snapshot.itemIdentifiers(inSection:)))
} else if lastVisibleContentSectionIndex == contentSections.count - 1 {
} else if lastVisibleRow.section == maxContentSectionIndex {
let items = snapshot.itemIdentifiers(inSection: lastVisibleRowSection)
if lastVisibleRow.row < items.count - pageSize {
let itemsToRemove = Array(items.suffix(pageSize))
let numberOfPagesToPrune = (items.count - lastVisibleRow.row - 1) / pageSize
if numberOfPagesToPrune > 0 {
let itemsToRemove = Array(items.suffix(numberOfPagesToPrune * pageSize))
snapshot.deleteItems(itemsToRemove)
willRemoveItems(itemsToRemove)
} else {
return
}
} else {
// unreachable
return
}

View File

@ -156,7 +156,6 @@ extension MenuPreviewProvider {
}
let bookmarked = status.bookmarked ?? false
let muted = status.muted
var actionsSection = [
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
@ -168,15 +167,6 @@ extension MenuPreviewProvider {
}
}
}),
createAction(identifier: "mute", title: muted ? "Unmute" : "Mute", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
guard let self = self else { return }
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
self.mastodonController?.run(request) { (response) in
if case let .success(status, _) = response {
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
}
}
})
]
if includeReply {
@ -186,18 +176,35 @@ extension MenuPreviewProvider {
}), at: 0)
}
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
let pinned = status.pinned ?? false
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
guard let self = self else { return }
let request = (pinned ? Status.unpin : Status.pin)(status.id)
self.mastodonController?.run(request, completion: { [weak self] (response) in
if let account = mastodonController.account {
// only allow muting conversations that either current user posted or is participating in (technically, is mentioned, since that's the best we can do)
if status.account.id == account.id || status.mentions.contains(where: { $0.id == account.id }) {
let muted = status.muted
actionsSection.append(createAction(identifier: "mute", title: muted ? "Unmute Conversation" : "Mute Conversation", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
guard let self = self else { return }
if case let .success(status, _) = response {
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
self.mastodonController?.run(request) { (response) in
if case let .success(status, _) = response {
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
}
}
})
}))
}))
}
// only allowing pinning user's own statuses
if account.id == status.account.id {
let pinned = status.pinned ?? false
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
guard let self = self else { return }
let request = (pinned ? Status.unpin : Status.pin)(status.id)
self.mastodonController?.run(request, completion: { [weak self] (response) in
guard let self = self else { return }
if case let .success(status, _) = response {
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
}
})
}))
}
}
if status.poll != nil {

View File

@ -1,254 +0,0 @@
//
// TimelineLikeTableViewController.swift
// Tusker
//
// Created by Shadowfacts on 11/15/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
/// A table view controller that manages common functionality between timeline-like UIs.
/// For example, this class handles loading new items when the user scrolls to the end,
/// refreshing, and pruning offscreen rows automatically.
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
private(set) var loaded = false
var sections: [[Item]] = []
private let pageSize = 20
private var lastLastVisibleRow: IndexPath?
init() {
super.init(style: .plain)
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: Self.refreshCommandTitle()))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func item(for indexPath: IndexPath) -> Item {
return sections[indexPath.section][indexPath.row]
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140
#if !targetEnvironment(macCatalyst)
self.refreshControl = UIRefreshControl()
self.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
#endif
if let prefetchSource = self as? UITableViewDataSourcePrefetching {
tableView.prefetchDataSource = prefetchSource
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadInitial()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
pruneOffscreenRows()
}
func loadInitial() {
guard !loaded else { return }
// set loaded immediately so we don't trigger another request while the current one is running
loaded = true
loadInitialItems() { (items) in
DispatchQueue.main.async {
guard items.count > 0 else {
// set loaded back to false so the next time the VC appears, we try to load again
// todo: this should probably retry automatically
self.loaded = false
return
}
if self.sections.count < self.headerSectionsCount() {
self.sections.insert(contentsOf: Array(repeating: [], count: self.headerSectionsCount() - self.sections.count), at: 0)
}
self.sections.append(items)
self.tableView.reloadData()
}
}
}
func reloadInitialItems() {
loaded = false
sections = []
loadInitial()
}
func cellHeightChanged() {
// causes the table view to recalculate the cell heights
tableView.beginUpdates()
tableView.endUpdates()
}
class func refreshCommandTitle() -> String {
return "Refresh"
}
// todo: these three should use Result<[Item], Client.Error> so we can differentiate between failed requests and there actually being no results
func loadInitialItems(completion: @escaping ([Item]) -> Void) {
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
}
func loadOlder(completion: @escaping ([Item]) -> Void) {
fatalError("loadOlder(completion:) must be implemented by subclasses")
}
func loadNewer(completion: @escaping ([Item]) -> Void) {
fatalError("loadNewer(completion:) must be implemented by subclasses")
}
func willRemoveRows(at indexPaths: [IndexPath]) {
}
func headerSectionsCount() -> Int {
return 0
}
private func pruneOffscreenRows() {
guard let lastVisibleRow = lastLastVisibleRow,
// never remove the last section
sections.count - headerSectionsCount() > 1 else {
return
}
let lastSectionIndex = sections.count - 1
if lastVisibleRow.section < lastSectionIndex {
// if there is a section below the last visible one
let sectionsToRemove = (lastVisibleRow.section + 1)...lastSectionIndex
let indexPathsToRemove = sectionsToRemove.flatMap { (section) in
sections[section].indices.map { (row) in
IndexPath(row: row, section: section)
}
}
willRemoveRows(at: indexPathsToRemove)
UIView.performWithoutAnimation {
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
}
sections.removeSubrange(sectionsToRemove)
} else if lastVisibleRow.section == lastSectionIndex {
let lastSection = sections.last!
let lastRowIndex = lastSection.count - 1
if lastVisibleRow.row < lastRowIndex - pageSize {
// if there are more than pageSize rows in the current section below the last visible one
let rowIndicesInLastSectionToRemove = (lastVisibleRow.row + pageSize)..<lastSection.count
let indexPathsToRemove = rowIndicesInLastSectionToRemove.map {
IndexPath(row: $0, section: lastSectionIndex)
}
willRemoveRows(at: indexPathsToRemove)
sections[lastSectionIndex].removeSubrange(rowIndicesInLastSectionToRemove)
UIView.performWithoutAnimation {
tableView.deleteRows(at: indexPathsToRemove, with: .none)
}
}
}
}
// MARK: - UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
fatalError("tableView(_:cellForRowAt:) must be implemented by subclasses")
}
// MARK: - UITableViewDelegate
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// this assumes that indexPathsForVisibleRows is always in order
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
if indexPath.section == sections.count - 1,
indexPath.row == sections[indexPath.section].count - 1 {
loadOlder() { (newItems) in
guard newItems.count > 0 else { return }
DispatchQueue.main.async {
let newRows = self.sections.last!.count..<(self.sections.last!.count + newItems.count)
let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.sections.count - 1) }
self.sections[self.sections.count - 1].append(contentsOf: newItems)
UIView.performWithoutAnimation {
self.tableView.insertRows(at: newIndexPaths, with: .none)
}
}
}
}
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
}
// MARK: - RefreshableViewController
func refresh() {
loadNewer() { (newItems) in
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()
guard newItems.count > 0 else { return }
let firstNonHeaderSection = self.headerSectionsCount()
self.sections[firstNonHeaderSection].insert(contentsOf: newItems, at: 0)
let newIndexPaths = (0..<newItems.count).map { IndexPath(row: $0, section: firstNonHeaderSection) }
UIView.performWithoutAnimation {
self.tableView.insertRows(at: newIndexPaths, with: .none)
}
// maintain the current position in the list (don't scroll to top)
self.tableView.scrollToRow(at: IndexPath(row: newItems.count, section: firstNonHeaderSection), at: .top, animated: false)
}
}
}
}
extension TimelineLikeTableViewController: BackgroundableViewController {
func sceneDidEnterBackground() {
pruneOffscreenRows()
}
}

View File

@ -8,6 +8,7 @@
import SwiftUI
import Pachyderm
import WebURLFoundationExtras
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
@ -20,7 +21,8 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
init(account: Account, fontSize: Int) {
self.account = account
self.fontSize = fontSize
self._text = State(initialValue: Text(verbatim: account.displayName))
let name = account.displayName.isEmpty ? account.username : account.displayName
self._text = State(initialValue: Text(verbatim: name))
}
var body: some View {
@ -47,7 +49,7 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
}
group.enter()
let request = ImageCache.emojis.get(emoji.url) { (_, image) in
let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
defer { group.leave() }
guard let image = image else { return }

View File

@ -0,0 +1,47 @@
//
// AssetPickerControlCollectionViewCell.swift
// Tusker
//
// Created by Shadowfacts on 1/21/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
class AssetPickerControlCollectionViewCell: UICollectionViewCell {
let imageView = UIImageView()
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
imageView.contentMode = .scaleAspectFit
imageView.setContentHuggingPriority(.defaultLow, for: .vertical)
label.font = .preferredFont(forTextStyle: .caption1)
label.textAlignment = .center
let stackView = UIStackView(arrangedSubviews: [
imageView,
label,
])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
imageView.widthAnchor.constraint(equalTo: widthAnchor, constant: -32)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX">
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="camera" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="fUY-i2-EtY">
<rect key="frame" x="16" y="16.5" width="48" height="46"/>
</imageView>
</subviews>
</view>
<constraints>
<constraint firstAttribute="bottom" secondItem="fUY-i2-EtY" secondAttribute="bottom" constant="16" id="HkO-Cn-2Na"/>
<constraint firstItem="fUY-i2-EtY" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" constant="16" id="bUq-pX-5XL"/>
<constraint firstAttribute="trailing" secondItem="fUY-i2-EtY" secondAttribute="trailing" constant="16" id="fyg-fN-FJu"/>
<constraint firstItem="fUY-i2-EtY" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" constant="16" id="oOU-Tc-Z5T"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
<point key="canvasLocation" x="132" y="154"/>
</collectionViewCell>
</objects>
<resources>
<image name="camera" catalog="system" width="64" height="48"/>
</resources>
</document>

View File

@ -17,6 +17,8 @@ protocol AttachmentViewDelegate: AnyObject {
class AttachmentView: GIFImageView {
static let queue = DispatchQueue(label: "Attachment Thumbnail", qos: .userInitiated, attributes: .concurrent)
weak var delegate: AttachmentViewDelegate?
var playImageView: UIImageView?
@ -108,7 +110,7 @@ class AttachmentView: GIFImageView {
}
if let hash = attachment.blurHash {
DispatchQueue.global(qos: .default).async { [weak self] in
AttachmentView.queue.async { [weak self] in
guard let self = self else { return }
let size: CGSize
if let meta = self.attachment.meta,
@ -191,8 +193,7 @@ class AttachmentView: GIFImageView {
})
} else {
let attachmentURL = self.attachment.url
// todo: use a single dispatch queue
DispatchQueue.global(qos: .userInitiated).async {
AttachmentView.queue.async {
let asset = AVURLAsset(url: attachmentURL)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
@ -237,7 +238,7 @@ class AttachmentView: GIFImageView {
func loadGifv() {
let attachmentURL = self.attachment.url
let asset = AVURLAsset(url: attachmentURL)
DispatchQueue.global(qos: .userInitiated).async {
AttachmentView.queue.async {
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }

View File

@ -8,6 +8,7 @@
import UIKit
import Pachyderm
import WebURLFoundationExtras
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
@ -56,7 +57,7 @@ extension BaseEmojiLabel {
foundEmojis = true
if let image = ImageCache.emojis.get(emoji.url)?.image {
if let image = ImageCache.emojis.get(URL(emoji.url)!)?.image {
// if the image is cached, add it immediately
emojiImages[emoji.shortcode] = image
} else {
@ -64,9 +65,9 @@ extension BaseEmojiLabel {
group.enter()
// todo: ImageCache.emojis.get here will re-check the memory and disk caches, there should be another method to force-refetch
let request = ImageCache.emojis.get(emoji.url) { (_, image) in
let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
guard let image = image,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else {
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: image) else {
group.leave()
return
}

View File

@ -8,6 +8,7 @@
import SwiftUI
import Pachyderm
import WebURLFoundationExtras
struct CustomEmojiImageView: View {
let emoji: Emoji
@ -33,7 +34,7 @@ struct CustomEmojiImageView: View {
}
private func loadImage() {
request = ImageCache.emojis.get(emoji.url) { (_, image) in
request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
DispatchQueue.main.async {
self.request = nil
if let image = image {

View File

@ -15,6 +15,9 @@ class HashtagHistoryView: UIView {
private let curveRadius: CGFloat = 10
/// The base background color used for the graph fill.
var effectiveBackgroundColor = UIColor.systemBackground
override func layoutSubviews() {
super.layoutSubviews()
@ -121,7 +124,7 @@ class HashtagHistoryView: UIView {
var tintGreen: CGFloat = 0
var tintBlue: CGFloat = 0
traitCollection.performAsCurrent {
backgroundColor!.getRed(&backgroundRed, green: &backgroundGreen, blue: &backgroundBlue, alpha: nil)
effectiveBackgroundColor.getRed(&backgroundRed, green: &backgroundGreen, blue: &backgroundBlue, alpha: nil)
tintColor.getRed(&tintRed, green: &tintGreen, blue: &tintBlue, alpha: nil)
}
let blendedRed = (backgroundRed + tintRed) / 2

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18121" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -39,7 +39,7 @@
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Xrw-2v-ybZ" customClass="HashtagHistoryView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="188" y="11" width="100" height="44"/>
<color key="backgroundColor" systemColor="secondarySystemGroupedBackgroundColor"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="W4C-uw-zWg"/>
<constraint firstAttribute="width" constant="100" id="XHb-vd-qNk"/>
@ -64,9 +64,4 @@
<point key="canvasLocation" x="132" y="132"/>
</tableViewCell>
</objects>
<resources>
<systemColor name="secondarySystemGroupedBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -171,14 +171,20 @@ class ProfileHeaderView: UIView {
}
private func updateRelationship() {
guard let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else {
return
}
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
guard let mastodonController = mastodonController,
let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else {
return
}
followsYouLabel.isHidden = !relationship.followedBy
}
@objc private func updateUIForPreferences() {
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
guard let mastodonController = mastodonController else {
return
}
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
fatalError("Missing cached account \(accountID!)")
}

View File

@ -28,7 +28,7 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: EmojiLabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var visibilityImageView: UIImageView!
@IBOutlet weak var metaIndicatorsView: StatusMetaIndicatorsView!
@IBOutlet weak var contentWarningLabel: EmojiLabel!
@IBOutlet weak var collapseButton: UIButton!
@IBOutlet weak var contentTextView: StatusContentTextView!
@ -43,27 +43,27 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
private(set) var nextThreadLinkView: UIView?
var statusID: String!
var accountID: String!
private(set) var accountID: String!
var favorited = false {
private var favorited = false {
didSet {
favoriteButton.tintColor = favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor
}
}
var reblogged = false {
private var reblogged = false {
didSet {
reblogButton.tintColor = reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor
}
}
var statusState: StatusState!
private(set) var statusState: StatusState!
var collapsible = false {
didSet {
collapseButton.isHidden = !collapsible
statusState?.collapsible = collapsible
}
}
var collapsed = false {
private var collapsed = false {
didSet {
statusState?.collapsed = collapsed
}
@ -166,16 +166,11 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
}
let reblogDisabled: Bool
switch mastodonController.instance?.instanceType {
case nil:
// todo: this handle a race condition in instance public timelines
// a somewhat better solution would be waiting to load the timeline until after the instance is loaded
reblogDisabled = true
case .mastodon:
reblogDisabled = status.visibility == .private || status.visibility == .direct
case .pleroma:
if mastodonController.instanceFeatures.boostToOriginalAudience {
// Pleroma allows 'Boost to original audience' for your own private posts
reblogDisabled = status.visibility == .direct || (status.visibility == .private && status.account.id != mastodonController.account.id)
} else {
reblogDisabled = status.visibility == .private || status.visibility == .direct
}
reblogButton.isEnabled = !reblogDisabled && mastodonController.loggedIn
@ -242,11 +237,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
}
func updateStatusIconsForPreferences(_ status: StatusMO) {
visibilityImageView.isHidden = !Preferences.shared.alwaysShowStatusVisibilityIcon
if Preferences.shared.alwaysShowStatusVisibilityIcon {
visibilityImageView.image = UIImage(systemName: status.visibility.unfilledImageName)
visibilityImageView.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "status visibility indicator accessibility label"), status.visibility.displayName)
}
metaIndicatorsView.updateUI(status: status)
let reblogButtonImage: UIImage
if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogButton.isEnabled {
reblogButtonImage = UIImage(systemName: "repeat")!

View File

@ -52,6 +52,9 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
contentTextView.defaultFont = .systemFont(ofSize: 18)
profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self))
metaIndicatorsView.allowedIndicators = [.visibility, .localOnly]
metaIndicatorsView.squeezeHorizontal = true
}
override func doUpdateUI(status: StatusMO, state: StatusState) {
@ -67,9 +70,10 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
override func updateStatusState(status: StatusMO) {
super.updateStatusState(status: status)
// todo: localize me
totalFavoritesButton.setTitle("\(status.favouritesCount) Favorite\(status.favouritesCount == 1 ? "" : "s")", for: .normal)
totalReblogsButton.setTitle("\(status.reblogsCount) Reblog\(status.reblogsCount == 1 ? "" : "s")", for: .normal)
let favoritesFormat = NSLocalizedString("favorites count", comment: "conv main status favorites button label")
totalFavoritesButton.setTitle(String.localizedStringWithFormat(favoritesFormat, status.favouritesCount), for: .normal)
let reblogsFormat = NSLocalizedString("reblogs count", comment: "conv main status reblogs button label")
totalReblogsButton.setTitle(String.localizedStringWithFormat(reblogsFormat, status.reblogsCount), for: .normal)
}
override func updateUI(account: AccountMO) {

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
@ -28,8 +29,8 @@
<constraint firstAttribute="width" constant="50" id="Yxp-Vr-dfl"/>
</constraints>
</imageView>
<label opaque="NO" contentMode="left" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="12" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lZY-2e-17d" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="58" y="0.0" width="255" height="29"/>
<label opaque="NO" contentMode="left" horizontalHuggingPriority="249" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="12" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lZY-2e-17d" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="58" y="0.0" width="146.5" height="29"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -40,29 +41,27 @@
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="3Qu-IO-5wt">
<rect key="frame" x="321" y="0.5" width="22" height="20.5"/>
<color key="tintColor" systemColor="secondaryLabelColor"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xD6-dy-0XV" customClass="StatusMetaIndicatorsView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="212.5" y="0.0" width="130.5" height="22"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="Kqh-qI-dSa"/>
<constraint firstAttribute="width" constant="22" id="QY1-tL-QHr"/>
<constraint firstAttribute="height" constant="22" placeholder="YES" id="wF5-Ii-LO5"/>
</constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
</imageView>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="SWg-Ka-QyP" secondAttribute="trailing" id="4g6-BT-eW4"/>
<constraint firstItem="xD6-dy-0XV" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="7Io-sX-c9k"/>
<constraint firstItem="lZY-2e-17d" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="8fU-y9-K5Z"/>
<constraint firstItem="lZY-2e-17d" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="Aqj-co-Szp"/>
<constraint firstItem="3Qu-IO-5wt" firstAttribute="leading" secondItem="lZY-2e-17d" secondAttribute="trailing" constant="8" id="MS8-zq-SWT"/>
<constraint firstAttribute="trailing" secondItem="3Qu-IO-5wt" secondAttribute="trailing" id="NWa-lL-aLk"/>
<constraint firstItem="xD6-dy-0XV" firstAttribute="leading" secondItem="lZY-2e-17d" secondAttribute="trailing" constant="8" id="PfV-YZ-k9j"/>
<constraint firstItem="mB9-HO-1vf" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="R7P-rD-Gbm"/>
<constraint firstAttribute="bottom" secondItem="mB9-HO-1vf" secondAttribute="bottom" id="Wd0-Qh-idS"/>
<constraint firstItem="mB9-HO-1vf" firstAttribute="leading" secondItem="Cnd-Fj-B7l" secondAttribute="leading" id="bxq-Fs-1aH"/>
<constraint firstItem="SWg-Ka-QyP" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="e45-gE-myI"/>
<constraint firstItem="SWg-Ka-QyP" firstAttribute="top" secondItem="lZY-2e-17d" secondAttribute="bottom" id="lvX-1b-8cN"/>
<constraint firstItem="3Qu-IO-5wt" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="pPU-WS-Y6B"/>
<constraint firstAttribute="trailing" secondItem="xD6-dy-0XV" secondAttribute="trailing" id="tfq-dR-UT7"/>
</constraints>
</view>
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cwQ-mR-L1b" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
@ -247,6 +246,7 @@
<outlet property="displayNameLabel" destination="lZY-2e-17d" id="7og-23-eHy"/>
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/>
<outlet property="favoriteButton" destination="DhN-rJ-jdA" id="b2Q-ch-kSP"/>
<outlet property="metaIndicatorsView" destination="xD6-dy-0XV" id="Smp-ox-cvj"/>
<outlet property="moreButton" destination="Ujo-Ap-dmK" id="2ba-5w-HDx"/>
<outlet property="pollView" destination="TLv-Xu-tT1" id="hJX-YD-lNr"/>
<outlet property="profileDetailContainerView" destination="Cnd-Fj-B7l" id="wco-VB-VQx"/>
@ -256,7 +256,6 @@
<outlet property="totalFavoritesButton" destination="yyj-Bs-Vjq" id="4pV-Qi-Z2X"/>
<outlet property="totalReblogsButton" destination="dem-vG-cPB" id="i9E-Qn-d76"/>
<outlet property="usernameLabel" destination="SWg-Ka-QyP" id="h2I-g4-AD9"/>
<outlet property="visibilityImageView" destination="3Qu-IO-5wt" id="sFB-ni-FcZ"/>
</connections>
<point key="canvasLocation" x="40.799999999999997" y="-122.78860569715144"/>
</view>
@ -265,8 +264,7 @@
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/>
<image name="chevron.down" catalog="system" width="128" height="72"/>
<image name="ellipsis" catalog="system" width="128" height="37"/>
<image name="globe" catalog="system" width="128" height="121"/>
<image name="repeat" catalog="system" width="128" height="99"/>
<image name="repeat" catalog="system" width="128" height="98"/>
<image name="star.fill" catalog="system" width="128" height="116"/>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@ -9,6 +9,7 @@
import UIKit
import Pachyderm
import SafariServices
import WebURLFoundationExtras
class StatusCardView: UIView {
@ -141,9 +142,9 @@ class StatusCardView: UIView {
if let imageURL = card.image {
placeholderImageView.isHidden = true
imageRequest = ImageCache.attachments.get(imageURL, completion: { (_, image) in
imageRequest = ImageCache.attachments.get(URL(imageURL)!, completion: { (_, image) in
guard let image = image,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: imageURL, image: image) else {
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(imageURL)!, image: image) else {
return
}
DispatchQueue.main.async {
@ -163,8 +164,7 @@ class StatusCardView: UIView {
let imageViewSize = self.imageView.bounds.size
// todo: merge this code with AttachmentView, use a single DispatchQueue
DispatchQueue.global(qos: .default).async { [weak self] in
AttachmentView.queue.async { [weak self] in
guard let self = self else { return }
let size: CGSize
@ -201,7 +201,7 @@ class StatusCardView: UIView {
setNeedsDisplay()
if let card = card, let delegate = navigationDelegate {
delegate.selected(url: card.url)
delegate.selected(url: URL(card.url)!)
}
}
@ -220,9 +220,9 @@ extension StatusCardView: UIContextMenuInteractionDelegate {
guard let card = card else { return nil }
return UIContextMenuConfiguration(identifier: nil) {
return SFSafariViewController(url: card.url)
return SFSafariViewController(url: URL(card.url)!)
} actionProvider: { (_) in
let actions = self.actionsForURL(card.url, sourceView: self)
let actions = self.actionsForURL(URL(card.url)!, sourceView: self)
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
}
}

View File

@ -0,0 +1,85 @@
//
// StatusMetaIndicatorsView.swift
// Tusker
//
// Created by Shadowfacts on 1/22/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class StatusMetaIndicatorsView: UIView {
var allowedIndicators: Indicator = .all
var squeezeHorizontal = false
private var images: [UIImageView] = []
func updateUI(status: StatusMO) {
images.forEach { $0.removeFromSuperview() }
var images: [UIImage] = []
if allowedIndicators.contains(.reply) && Preferences.shared.showIsStatusReplyIcon && status.inReplyToID != nil {
images.append(UIImage(systemName: "bubble.left.and.bubble.right")!)
}
if allowedIndicators.contains(.visibility) && Preferences.shared.alwaysShowStatusVisibilityIcon {
images.append(UIImage(systemName: status.visibility.unfilledImageName)!)
}
if allowedIndicators.contains(.localOnly) && status.localOnly {
images.append(UIImage(named: "link.broken")!)
}
self.images = []
for (index, image) in images.enumerated() {
let v = UIImageView(image: image)
v.translatesAutoresizingMaskIntoConstraints = false
v.contentMode = .scaleAspectFit
v.tintColor = .secondaryLabel
v.preferredSymbolConfiguration = .init(weight: .thin)
addSubview(v)
v.heightAnchor.constraint(equalToConstant: 22).isActive = true
if index % 2 == 0 {
if index == images.count - 1 {
v.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
v.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor).isActive = true
} else {
v.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
}
} else {
if squeezeHorizontal {
v.leadingAnchor.constraint(equalTo: self.images[index - 1].trailingAnchor, constant: 4).isActive = true
} else {
v.leadingAnchor.constraint(greaterThanOrEqualTo: self.images[index - 1].trailingAnchor, constant: 4).isActive = true
}
v.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
}
let row = index / 2
if row == 0 {
v.topAnchor.constraint(equalTo: topAnchor).isActive = true
} else {
v.topAnchor.constraint(equalTo: self.images[index - 1].bottomAnchor, constant: 4).isActive = true
}
v.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor).isActive = true
self.images.append(v)
}
}
}
extension StatusMetaIndicatorsView {
struct Indicator: OptionSet {
let rawValue: Int
static let reply = Indicator(rawValue: 1 << 0)
static let visibility = Indicator(rawValue: 1 << 1)
static let localOnly = Indicator(rawValue: 1 << 2)
static let all: Indicator = [.reply, .visibility, .localOnly]
}
}

View File

@ -22,7 +22,6 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
@IBOutlet weak var reblogLabel: EmojiLabel!
@IBOutlet weak var timestampLabel: UILabel!
@IBOutlet weak var pinImageView: UIImageView!
@IBOutlet weak var replyImageView: UIImageView!
var reblogStatusID: String?
var rebloggerID: String?
@ -80,6 +79,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
updateRebloggerLabel(reblogger: status.account)
status = rebloggedStatus
// necessary b/c statusID is initially set to the reblog status ID in updateUI(statusID:state:)
statusID = rebloggedStatus.id
} else {
reblogStatusID = nil
@ -91,9 +91,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
doUpdateTimestamp(status: status)
let pinned = showPinned && (status.pinned ?? false)
timestampLabel.isHidden = pinned
pinImageView.isHidden = !pinned
timestampLabel.isHidden = showPinned
pinImageView.isHidden = !showPinned
}
override func updateGrayscaleableUI(account: AccountMO, status: StatusMO) {
@ -117,9 +116,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
}
override func updateStatusIconsForPreferences(_ status: StatusMO) {
if showReplyIndicator {
metaIndicatorsView.allowedIndicators = .all
} else {
metaIndicatorsView.allowedIndicators = .all.subtracting(.reply)
}
super.updateStatusIconsForPreferences(status)
replyImageView.isHidden = !Preferences.shared.showIsStatusReplyIcon || !showReplyIndicator || status.inReplyToID == nil
}
private func updateTimestamp() {

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19115.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19107.4"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -141,28 +141,13 @@
</label>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qBn-Gk-DCa" customClass="StatusMetaIndicatorsView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="54" width="50" height="22"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
<rect key="frame" x="0.0" y="0.0" width="25.5" height="21.5"/>
<color key="tintColor" systemColor="secondaryLabelColor"/>
<accessibility key="accessibilityConfiguration" label="Is a reply"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="x0C-Qo-YVA"/>
</constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="LRh-Cc-1br">
<rect key="frame" x="30.5" y="0.5" width="19.5" height="20.5"/>
<color key="tintColor" systemColor="secondaryLabelColor"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="3Mk-NN-6fY"/>
</constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
</imageView>
</subviews>
</stackView>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="22" placeholder="YES" id="ipd-WE-P20"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TUP-Nz-5Yh">
<rect key="frame" x="0.0" y="169.5" width="335" height="26"/>
<subviews>
@ -226,15 +211,16 @@
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/>
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="trailing" secondItem="ve3-Y1-NQH" secondAttribute="trailingMargin" id="3l0-tE-Ak1"/>
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="top" secondItem="gIY-Wp-RSk" secondAttribute="bottom" constant="-4" id="4KL-a3-qyf"/>
<constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/>
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="oie-wK-IpU" secondAttribute="bottom" id="7Xp-Sa-Rfk"/>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="qBn-Gk-DCa" secondAttribute="trailing" constant="8" id="AQs-QN-j49"/>
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/>
<constraint firstItem="oie-wK-IpU" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="QKi-ny-jOJ"/>
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="QZ2-iO-ckC"/>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="top" id="fEd-wN-kuQ"/>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="oie-wK-IpU" secondAttribute="trailing" constant="8" id="fqd-p6-oGe"/>
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="qBn-Gk-DCa" secondAttribute="bottom" id="gxb-hp-7lU"/>
<constraint firstAttribute="trailingMargin" secondItem="gIY-Wp-RSk" secondAttribute="trailing" id="hKk-kO-wFT"/>
<constraint firstItem="qBn-Gk-DCa" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="iLD-VU-ixJ"/>
<constraint firstAttribute="bottom" secondItem="TUP-Nz-5Yh" secondAttribute="bottom" id="rmQ-QM-Llu"/>
<constraint firstItem="qBn-Gk-DCa" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="tKU-VP-n8P"/>
<constraint firstItem="qBn-Gk-DCa" firstAttribute="width" secondItem="QMP-j2-HLn" secondAttribute="width" id="v1v-Pp-ubE"/>
<constraint firstItem="QMP-j2-HLn" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="zeW-tQ-uJl"/>
</constraints>
</view>
@ -262,26 +248,23 @@
<outlet property="contentWarningLabel" destination="inI-Og-YiU" id="C7a-eK-qcx"/>
<outlet property="displayNameLabel" destination="gll-xe-FSr" id="vVS-WM-Wqx"/>
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/>
<outlet property="metaIndicatorsView" destination="qBn-Gk-DCa" id="HTg-JD-7zH"/>
<outlet property="moreButton" destination="982-J4-NGl" id="Pux-tL-aWe"/>
<outlet property="pinImageView" destination="wtt-8G-Ua1" id="mE8-oe-m1l"/>
<outlet property="pollView" destination="x3b-Zl-9F0" id="WIF-Oz-cnm"/>
<outlet property="reblogButton" destination="6tW-z8-Qh9" id="u2t-8D-kOn"/>
<outlet property="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/>
<outlet property="replyButton" destination="rKF-yF-KIa" id="rka-q1-o4a"/>
<outlet property="replyImageView" destination="KdQ-Zn-IhD" id="jqs-FK-K1N"/>
<outlet property="timestampLabel" destination="35d-EA-ReR" id="Ny2-nV-nqP"/>
<outlet property="usernameLabel" destination="j89-zc-SFa" id="bXX-FZ-fCp"/>
<outlet property="visibilityImageView" destination="LRh-Cc-1br" id="pxm-JK-jAz"/>
</connections>
<point key="canvasLocation" x="29.600000000000001" y="79.160419790104953"/>
</view>
</objects>
<resources>
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/>
<image name="bubble.left.and.bubble.right" catalog="system" width="128" height="96"/>
<image name="chevron.down" catalog="system" width="128" height="72"/>
<image name="ellipsis" catalog="system" width="128" height="37"/>
<image name="globe" catalog="system" width="128" height="121"/>
<image name="pin.fill" catalog="system" width="119" height="128"/>
<image name="repeat" catalog="system" width="128" height="98"/>
<image name="star.fill" catalog="system" width="128" height="116"/>

View File

@ -27,7 +27,7 @@ class StatusContentTextView: ContentTextView {
let status = mastodonController.persistentContainer.status(for: statusID) {
mention = status.mentions.first { (mention) in
// Mastodon and Pleroma include the @ in the <a> text, GNU Social does not
(text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host
(text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host!.serialized
}
} else {
mention = nil

View File

@ -61,5 +61,37 @@
<string>%u replies</string>
</dict>
</dict>
<key>favorites count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@favorites@</string>
<key>favorites</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>u</string>
<key>one</key>
<string>1 favorite</string>
<key>other</key>
<string>%u favorites</string>
</dict>
</dict>
<key>reblogs count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@reblogs@</string>
<key>reblogs</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>u</string>
<key>one</key>
<string>1 reblog</string>
<key>other</key>
<string>%u reblogs</string>
</dict>
</dict>
</dict>
</plist>