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 # 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) ## 2021.1 (23)
Features/Improvements: Features/Improvements:
- Synchronize GIF playback through animations and in gallery - Synchronize GIF playback through animations and in gallery

View File

@ -109,7 +109,9 @@ public class Client {
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval) var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
urlRequest.httpMethod = request.method.name urlRequest.httpMethod = request.method.name
urlRequest.httpBody = request.body.data 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 { if let accessToken = accessToken {
urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") 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 // MARK: - Self
public static func getSelfAccount() -> Request<Account> { public static func getSelfAccount() -> Request<Account> {
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials") return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
@ -315,7 +335,8 @@ public class Client {
language: String? = nil, language: String? = nil,
pollOptions: [String]? = nil, pollOptions: [String]? = nil,
pollExpiresIn: Int? = 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([ return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([
"status" => text, "status" => text,
"content_type" => contentType.mimeType, "content_type" => contentType.mimeType,
@ -326,6 +347,7 @@ public class Client {
"language" => language, "language" => language,
"poll[expires_in]" => pollExpiresIn, "poll[expires_in]" => pollExpiresIn,
"poll[multiple]" => pollMultiple, "poll[multiple]" => pollMultiple,
"local_only" => localOnly,
] + "media_ids" => media?.map { $0.id } + "poll[options]" => pollOptions)) ] + "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 url: URL
public let remoteURL: URL? public let remoteURL: URL?
public let previewURL: URL? public let previewURL: URL?
public let textURL: URL?
public let meta: Metadata? public let meta: Metadata?
public let description: String? public let description: String?
public let blurHash: String? public let blurHash: String?
@ -33,7 +32,6 @@ public class Attachment: Codable {
self.url = try container.decode(URL.self, forKey: .url) self.url = try container.decode(URL.self, forKey: .url)
self.previewURL = try? container.decode(URL?.self, forKey: .previewURL) self.previewURL = try? container.decode(URL?.self, forKey: .previewURL)
self.remoteURL = try? container.decode(URL?.self, forKey: .remoteURL) 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.meta = try? container.decode(Metadata?.self, forKey: .meta)
self.description = try? container.decode(String?.self, forKey: .description) self.description = try? container.decode(String?.self, forKey: .description)
self.blurHash = try? container.decode(String?.self, forKey: .blurHash) self.blurHash = try? container.decode(String?.self, forKey: .blurHash)
@ -45,7 +43,6 @@ public class Attachment: Codable {
case url case url
case remoteURL = "remote_url" case remoteURL = "remote_url"
case previewURL = "preview_url" case previewURL = "preview_url"
case textURL = "text_url"
case meta case meta
case description case description
case blurHash = "blurhash" case blurHash = "blurhash"

View File

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

View File

@ -7,29 +7,22 @@
// //
import Foundation import Foundation
import WebURL
public class Emoji: Codable { public class Emoji: Codable {
public let shortcode: String public let shortcode: String
public let url: URL // these shouldn't need to be WebURLs as they're not external resources,
public let staticURL: URL // 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 let visibleInPicker: Bool
public required init(from decoder: Decoder) throws { public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.shortcode = try container.decode(String.self, forKey: .shortcode) self.shortcode = try container.decode(String.self, forKey: .shortcode)
if let url = try? container.decode(URL.self, forKey: .url) { self.url = try container.decode(WebURL.self, forKey: .url)
self.url = url self.staticURL = try container.decode(WebURL.self, forKey: .staticURL)
} 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.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker) self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
} }

View File

@ -7,11 +7,13 @@
// //
import Foundation import Foundation
import WebURL
public class Mention: Codable { public class Mention: Codable {
public let url: URL public let url: WebURL
public let username: String public let username: String
public let acct: String public let acct: String
/// The instance-local ID of the user being mentioned.
public let id: String public let id: String
private enum CodingKeys: String, CodingKey { 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 bookmarked: Bool?
public let card: Card? public let card: Card?
public let poll: Poll? public let poll: Poll?
// Hometown, Glitch only
public let localOnly: Bool?
public var applicationName: String? { application?.name } public var applicationName: String? { application?.name }
@ -134,6 +136,7 @@ public final class Status: /*StatusProtocol,*/ Decodable {
case bookmarked case bookmarked
case card case card
case poll 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; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 52; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -90,7 +90,6 @@
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; }; D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A542263634100095BD04 /* PollOptionCheckboxView.swift */; }; D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A542263634100095BD04 /* PollOptionCheckboxView.swift */; };
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.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 */; }; D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; };
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; }; D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; };
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */; }; 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 */; }; D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.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 */; }; 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 */; }; D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.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 */; }; 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 */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; }; D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.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 */; }; D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */; };
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; }; D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.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 */; }; D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626321360D2300C9CBA2 /* AvatarStyle.swift */; };
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.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 */; }; 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 */; }; 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 */; }; D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */; };
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; }; D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; };
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationTableViewController.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 */; }; D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; }; D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.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 */; }; D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; }; D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.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 */; }; D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */; };
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; }; D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.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 */; }; D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; };
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; }; D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
@ -760,6 +768,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D6F1F9DF27B0613300CB7D88 /* WebURL in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -779,6 +788,7 @@
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */, D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */, D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */, D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -903,6 +913,7 @@
D61099F02145686D00432DC2 /* List.swift */, D61099F02145686D00432DC2 /* List.swift */,
D6109A062145756700432DC2 /* LoginSettings.swift */, D6109A062145756700432DC2 /* LoginSettings.swift */,
D61099F22145688600432DC2 /* Mention.swift */, D61099F22145688600432DC2 /* Mention.swift */,
D62E9980279C691F00C26176 /* NodeInfo.swift */,
D61099F4214568C300432DC2 /* Notification.swift */, D61099F4214568C300432DC2 /* Notification.swift */,
D623A53E2635F6910095BD04 /* Poll.swift */, D623A53E2635F6910095BD04 /* Poll.swift */,
D61099F62145693500432DC2 /* PushSubscription.swift */, D61099F62145693500432DC2 /* PushSubscription.swift */,
@ -914,6 +925,7 @@
D61099FE21456A4C00432DC2 /* Status.swift */, D61099FE21456A4C00432DC2 /* Status.swift */,
D6285B4E21EA695800FE4B39 /* StatusContentType.swift */, D6285B4E21EA695800FE4B39 /* StatusContentType.swift */,
D6109A10214607D500432DC2 /* Timeline.swift */, D6109A10214607D500432DC2 /* Timeline.swift */,
D62E9982279C69D400C26176 /* WellKnown.swift */,
); );
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
@ -976,11 +988,11 @@
children = ( children = (
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */, D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */,
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */, D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */,
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */,
D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */, D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */,
D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */, D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */,
D626493A23C1000300612E6E /* AlbumTableViewCell.swift */, D626493A23C1000300612E6E /* AlbumTableViewCell.swift */,
D626493B23C1000300612E6E /* AlbumTableViewCell.xib */, D626493B23C1000300612E6E /* AlbumTableViewCell.xib */,
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */,
); );
path = "Asset Picker"; path = "Asset Picker";
sourceTree = "<group>"; sourceTree = "<group>";
@ -1222,6 +1234,7 @@
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */, D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */, D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */,
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */, D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */,
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */,
); );
path = Status; path = Status;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1322,6 +1335,7 @@
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */, D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */, D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */, D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
D62E9984279CA23900C26176 /* URLSession+Development.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1362,7 +1376,6 @@
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */, D6E6F26221603F8B006A8599 /* CharacterCounter.swift */,
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */, D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */,
D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */, D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */,
D667383B23299340000A2373 /* InstanceType.swift */,
D61AC1D2232E928600C54D2D /* InstanceSelector.swift */, D61AC1D2232E928600C54D2D /* InstanceSelector.swift */,
); );
path = Utilities; path = Utilities;
@ -1523,7 +1536,6 @@
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */, D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */, D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */, D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */, D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */, D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */, D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
@ -1587,6 +1599,7 @@
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */, D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */, D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
D62E9988279DB2D100C26176 /* InstanceFeatures.swift */,
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */, D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
D6AEBB3F2321640F00E5038B /* Activities */, D6AEBB3F2321640F00E5038B /* Activities */,
D6F1F84E2193B9BE00F5FE67 /* Caching */, D6F1F84E2193B9BE00F5FE67 /* Caching */,
@ -1711,6 +1724,9 @@
dependencies = ( dependencies = (
); );
name = Pachyderm; name = Pachyderm;
packageProductDependencies = (
D6F1F9DE27B0613300CB7D88 /* WebURL */,
);
productName = Pachyderm; productName = Pachyderm;
productReference = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; productReference = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */;
productType = "com.apple.product-type.framework"; productType = "com.apple.product-type.framework";
@ -1744,6 +1760,7 @@
D6F953E52125197500CF0F2B /* Embed Frameworks */, D6F953E52125197500CF0F2B /* Embed Frameworks */,
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */, D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
D6E3438F2659849800C4AA01 /* Embed App Extensions */, D6E3438F2659849800C4AA01 /* Embed App Extensions */,
D6F1F9E127B0677000CB7D88 /* ShellScript */,
); );
buildRules = ( buildRules = (
); );
@ -1756,6 +1773,7 @@
D6B0539E23BD2BA300A066FA /* SheetController */, D6B0539E23BD2BA300A066FA /* SheetController */,
D69CCBBE249E6EFD000AF167 /* CrashReporter */, D69CCBBE249E6EFD000AF167 /* CrashReporter */,
D60CFFDA24A290BA00D00083 /* SwiftSoup */, D60CFFDA24A290BA00D00083 /* SwiftSoup */,
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
); );
productName = Tusker; productName = Tusker;
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */; productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
@ -1873,6 +1891,7 @@
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */, D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */,
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */, D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */, D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
); );
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */; productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -1926,7 +1945,6 @@
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */, D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */, D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */,
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */, D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */,
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */, D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */, D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */, D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
@ -1988,6 +2006,24 @@
shellPath = /bin/sh; 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"; 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 */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@ -1996,7 +2032,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D61099E5214561AB00432DC2 /* Application.swift in Sources */, D61099E5214561AB00432DC2 /* Application.swift in Sources */,
D667383C23299340000A2373 /* InstanceType.swift in Sources */, D62E9983279C69D400C26176 /* WellKnown.swift in Sources */,
D61099FF21456A4C00432DC2 /* Status.swift in Sources */, D61099FF21456A4C00432DC2 /* Status.swift in Sources */,
D61099E32144C38900432DC2 /* Emoji.swift in Sources */, D61099E32144C38900432DC2 /* Emoji.swift in Sources */,
D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */, D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */,
@ -2024,6 +2060,7 @@
D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */, D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */,
D6109A0921458C4A00432DC2 /* Empty.swift in Sources */, D6109A0921458C4A00432DC2 /* Empty.swift in Sources */,
D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */, D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */,
D62E9981279C691F00C26176 /* NodeInfo.swift in Sources */,
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */, D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */,
D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */, D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */,
D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */, D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */,
@ -2078,9 +2115,9 @@
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */, D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */, D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */, D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */, D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */, D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */, D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
@ -2100,6 +2137,7 @@
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */, D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */, D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */, D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */, D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */, D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
@ -2180,6 +2218,7 @@
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */, D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */, D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */,
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */, D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */, D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */, D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
@ -2256,6 +2295,7 @@
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */, D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */, D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */, D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */, 04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */, D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */, D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
@ -2390,11 +2430,11 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW; DEVELOPMENT_TEAM = V4WK9KR9U2;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -2406,6 +2446,8 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.Pachyderm; PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.Pachyderm;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -2421,11 +2463,11 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW; DEVELOPMENT_TEAM = V4WK9KR9U2;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -2437,6 +2479,8 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.Pachyderm; PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.Pachyderm;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -2617,7 +2661,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements; CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2626,7 +2670,8 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2021.1; MARKETING_VERSION = 2022.1;
OTHER_CODE_SIGN_FLAGS = "";
OTHER_LDFLAGS = ""; OTHER_LDFLAGS = "";
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14"; "OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker; PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
@ -2647,7 +2692,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements; CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2656,7 +2701,8 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@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"; "OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker; PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -2756,7 +2802,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements; CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist; INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2766,7 +2812,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2021.1; MARKETING_VERSION = 2022.1;
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker; PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -2783,7 +2829,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements; CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist; INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2793,7 +2839,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2021.1; MARKETING_VERSION = 2022.1;
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker; PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -2880,6 +2926,14 @@
minimumVersion = 2.3.2; 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" */ = { D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/microsoft/plcrashreporter"; repositoryURL = "https://github.com/microsoft/plcrashreporter";
@ -2904,6 +2958,11 @@
package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */; package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup; productName = SwiftSoup;
}; };
D6676CA427A8D0020052936B /* WebURLFoundationExtras */ = {
isa = XCSwiftPackageProductDependency;
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
productName = WebURLFoundationExtras;
};
D69CCBBE249E6EFD000AF167 /* CrashReporter */ = { D69CCBBE249E6EFD000AF167 /* CrashReporter */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */; package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
@ -2914,6 +2973,11 @@
package = D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */; package = D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */;
productName = SheetController; productName = SheetController;
}; };
D6F1F9DE27B0613300CB7D88 /* WebURL */ = {
isa = XCSwiftPackageProductDependency;
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
productName = WebURL;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */ /* Begin XCVersionGroup section */

View File

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

View File

@ -2,7 +2,7 @@
"object": { "object": {
"pins": [ "pins": [
{ {
"package": "plcrashreporter", "package": "PLCrashReporter",
"repositoryURL": "https://github.com/microsoft/plcrashreporter", "repositoryURL": "https://github.com/microsoft/plcrashreporter",
"state": { "state": {
"branch": null, "branch": null,
@ -19,6 +19,24 @@
"version": null "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", "package": "SwiftSoup",
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git", "repositoryURL": "https://github.com/scinfu/SwiftSoup.git",

View File

@ -1,6 +1,6 @@
{ {
"info" : { "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 { 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 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)) 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 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 #if DEBUG
private static let disableCaching = ProcessInfo.processInfo.environment.keys.contains("DISABLE_IMAGE_CACHE") private static let disableCaching = ProcessInfo.processInfo.environment.keys.contains("DISABLE_IMAGE_CACHE")
@ -22,6 +22,7 @@ class ImageCache {
#endif #endif
private let cache: ImageDataCache private let cache: ImageDataCache
private let desiredPixelSize: CGSize?
private var groups = MultiThreadDictionary<URL, RequestGroup>(name: "ImageCache request groups") 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) { 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? // 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)) 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) 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? { func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
let key = url.absoluteString let key = url.absoluteString
if !ImageCache.disableCaching,
let entry = try? cache.get(key, loadOriginal: loadOriginal) { let wrappedCompletion: ((Data?, UIImage?) -> Void)?
if let completion = completion { if let completion = completion {
backgroundQueue.async { wrappedCompletion = { (data, image) in
completion(entry.data, entry.image) 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 return nil
} else { } else {
if let group = groups[url] { if let group = groups[url] {
if let completion = completion { return group.addCallback(wrappedCompletion)
return group.addCallback(completion)
}
return nil
} else { } else {
let group = createGroup(url: url) let group = createGroup(url: url)
let request = group.addCallback(completion) let request = group.addCallback(wrappedCompletion)
group.run() group.run()
return request return request
} }
@ -122,21 +142,15 @@ class ImageCache {
task!.resume() 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 { func addCallback(_ completion: ((Data?, UIImage?) -> Void)?) -> Request {
let request = Request(callback: completion) let request = Request(callback: completion)
requests.append(request) requests.append(request)
updatePriority()
return request return request
} }
func cancelWithoutCallback() { func cancelWithoutCallback() {
if let request = requests.first(where: { $0.callback == nil && !$0.cancelled }) { if let request = requests.first(where: { $0.callback == nil && !$0.cancelled }) {
request.cancel() request.cancel()
updatePriority()
} }
} }
@ -145,8 +159,6 @@ class ImageCache {
if remaining <= 0 { if remaining <= 0 {
task?.cancel() task?.cancel()
complete(with: nil) complete(with: nil)
} else {
updatePriority()
} }
} }

View File

@ -44,6 +44,8 @@ class MastodonController: ObservableObject {
@Published private(set) var account: Account! @Published private(set) var account: Account!
@Published private(set) var instance: Instance! @Published private(set) var instance: Instance!
@Published private(set) var nodeInfo: NodeInfo!
@Published private(set) var instanceFeatures = InstanceFeatures()
private(set) var customEmojis: [Emoji]? private(set) var customEmojis: [Emoji]?
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]() private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
@ -56,7 +58,7 @@ class MastodonController: ObservableObject {
init(instanceURL: URL, transient: Bool = false) { init(instanceURL: URL, transient: Bool = false) {
self.instanceURL = instanceURL self.instanceURL = instanceURL
self.accountInfo = nil self.accountInfo = nil
self.client = Client(baseURL: instanceURL) self.client = Client(baseURL: instanceURL, session: .appDefault)
self.transient = transient self.transient = transient
} }
@ -161,6 +163,7 @@ class MastodonController: ObservableObject {
DispatchQueue.main.async { DispatchQueue.main.async {
self.ownInstanceRequest = nil self.ownInstanceRequest = nil
self.instance = instance self.instance = instance
self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo)
for completion in self.pendingOwnInstanceRequestCallbacks { for completion in self.pendingOwnInstanceRequestCallbacks {
completion(instance) 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 private var pollData: Data?
@NSManaged public var account: AccountMO @NSManaged public var account: AccountMO
@NSManaged public var reblog: StatusMO? @NSManaged public var reblog: StatusMO?
@NSManaged public var localOnly: Bool
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData) @LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
public var attachments: [Attachment] public var attachments: [Attachment]
@ -134,6 +135,7 @@ extension StatusMO {
self.url = status.url self.url = status.url
self.visibility = status.visibility self.visibility = status.visibility
self.poll = status.poll self.poll = status.poll
self.localOnly = status.localOnly ?? false
if let existing = container.account(for: status.account.id, in: context) { if let existing = container.account(for: status.account.id, in: context) {
existing.updateFrom(apiAccount: status.account, container: container) existing.updateFrom(apiAccount: status.account, container: container)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?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"> <entity name="Account" representedClassName="AccountMO" syncable="YES">
<attribute name="acct" attributeType="String"/> <attribute name="acct" attributeType="String"/>
<attribute name="avatar" attributeType="URI"/> <attribute name="avatar" attributeType="URI"/>
@ -43,7 +43,7 @@
<entity name="Status" representedClassName="StatusMO" syncable="YES"> <entity name="Status" representedClassName="StatusMO" syncable="YES">
<attribute name="applicationName" optional="YES" attributeType="String"/> <attribute name="applicationName" optional="YES" attributeType="String"/>
<attribute name="attachmentsData" attributeType="Binary"/> <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="cardData" optional="YES" attributeType="Binary"/>
<attribute name="content" attributeType="String"/> <attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
@ -54,6 +54,7 @@
<attribute name="id" attributeType="String"/> <attribute name="id" attributeType="String"/>
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/> <attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
<attribute name="inReplyToID" 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="mentionsData" attributeType="Binary"/>
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="pinnedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="pinnedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
@ -77,6 +78,6 @@
<elements> <elements>
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="329"/> <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="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> </elements>
</model> </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 { } else {
defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")! 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 { switch self {
case let .image(image): case let .image(image):
// Export as JPEG instead of PNG, otherweise photos straight from the camera are too large // Export as JPEG instead of PNG, otherweise photos straight from the camera are too large
// for Mastodon in its default configuration (max of 10MB). // 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. // 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): case let .asset(asset):
if asset.mediaType == .image { if asset.mediaType == .image {
let options = PHImageRequestOptions() let options = PHImageRequestOptions()
@ -63,7 +63,10 @@ enum CompositionAttachmentData {
options.resizeMode = .none options.resizeMode = .none
options.isNetworkAccessAllowed = true options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in 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 let mimeType: String
if dataUTI == "public.heic" { if dataUTI == "public.heic" {
@ -77,7 +80,7 @@ enum CompositionAttachmentData {
mimeType = UTType(dataUTI)!.preferredMIMEType! mimeType = UTType(dataUTI)!.preferredMIMEType!
} }
completion(data, mimeType) completion(.success((data, mimeType)))
} }
} else if asset.mediaType == .video { } else if asset.mediaType == .video {
let options = PHVideoRequestOptions() let options = PHVideoRequestOptions()
@ -100,20 +103,23 @@ enum CompositionAttachmentData {
case let .drawing(drawing): case let .drawing(drawing):
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1) 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.outputFileType = .mp4
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4") session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
session.exportAsynchronously { 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 { do {
let data = try Data(contentsOf: session.outputURL!) let data = try Data(contentsOf: session.outputURL!)
completion(data, "video/mp4") completion(.success((data, "video/mp4")))
} catch { } catch {
fatalError("Unable to load video: \(error)") completion(.failure(.export(error)))
} }
} }
} }
@ -121,6 +127,11 @@ enum CompositionAttachmentData {
enum AttachmentType { enum AttachmentType {
case image, video case image, video
} }
enum Error: Swift.Error {
case missingData
case export(Swift.Error)
}
} }
extension PHAsset { extension PHAsset {

View File

@ -21,6 +21,7 @@ class Draft: Codable, ObservableObject {
@Published var inReplyToID: String? @Published var inReplyToID: String?
@Published var visibility: Status.Visibility @Published var visibility: Status.Visibility
@Published var poll: Poll? @Published var poll: Poll?
@Published var localOnly: Bool
var initialText: String var initialText: String
@ -31,12 +32,6 @@ class Draft: Codable, ObservableObject {
poll?.hasContent == true 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) { init(accountID: String) {
self.id = UUID() self.id = UUID()
self.lastModified = Date() self.lastModified = Date()
@ -49,6 +44,7 @@ class Draft: Codable, ObservableObject {
self.inReplyToID = nil self.inReplyToID = nil
self.visibility = Preferences.shared.defaultPostVisibility self.visibility = Preferences.shared.defaultPostVisibility
self.poll = nil self.poll = nil
self.localOnly = false
self.initialText = "" self.initialText = ""
} }
@ -61,24 +57,13 @@ class Draft: Codable, ObservableObject {
self.accountID = try container.decode(String.self, forKey: .accountID) self.accountID = try container.decode(String.self, forKey: .accountID)
self.text = try container.decode(String.self, forKey: .text) self.text = try container.decode(String.self, forKey: .text)
if let enabled = try? container.decode(Bool.self, forKey: .contentWarningEnabled) { self.contentWarningEnabled = try container.decode(Bool.self, forKey: .contentWarningEnabled)
self.contentWarningEnabled = enabled self.contentWarning = try container.decode(String.self, forKey: .contentWarning)
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.attachments = try container.decode([CompositionAttachment].self, forKey: .attachments) self.attachments = try container.decode([CompositionAttachment].self, forKey: .attachments)
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID) self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility) self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility)
self.poll = try container.decode(Poll.self, forKey: .poll) 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) 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(inReplyToID, forKey: .inReplyToID)
try container.encode(visibility, forKey: .visibility) try container.encode(visibility, forKey: .visibility)
try container.encode(poll, forKey: .poll) try container.encode(poll, forKey: .poll)
try container.encode(localOnly, forKey: .localOnly)
try container.encode(initialText, forKey: .initialText) 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 { extension Draft: Equatable {
@ -121,6 +120,7 @@ extension Draft {
case inReplyToID case inReplyToID
case visibility case visibility
case poll case poll
case localOnly
case initialText case initialText
} }

View File

@ -18,16 +18,13 @@ protocol AssetCollectionViewControllerDelegate: AnyObject {
func captureFromCamera() func captureFromCamera()
} }
class AssetCollectionViewController: UICollectionViewController { class AssetCollectionViewController: UIViewController, UICollectionViewDelegate {
weak var delegate: AssetCollectionViewControllerDelegate? weak var delegate: AssetCollectionViewControllerDelegate?
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var flowLayout: UICollectionViewFlowLayout {
return collectionViewLayout as! UICollectionViewFlowLayout
}
private var availableWidth: CGFloat!
private var thumbnailSize: CGSize! private var thumbnailSize: CGSize!
private let imageManager = PHCachingImageManager() private let imageManager = PHCachingImageManager()
@ -41,7 +38,7 @@ class AssetCollectionViewController: UICollectionViewController {
} }
init() { init() {
super.init(collectionViewLayout: UICollectionViewFlowLayout()) super.init(nibName: nil, bundle: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -51,6 +48,17 @@ class AssetCollectionViewController: UICollectionViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.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 // 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, // 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 // 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.allowsSelection = true
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier) collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
let scale = UIScreen.main.scale let controlCell = UICollectionView.CellRegistration<AssetPickerControlCollectionViewCell, Item> { cell, indexPath, itemIdentifier in
let cellSize = flowLayout.itemSize switch itemIdentifier {
thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale) 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 dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
switch item { switch item {
case .showCamera: case .showCamera, .changeLimitedSelection:
return collectionView.dequeueReusableCell(withReuseIdentifier: cameraReuseIdentifier, for: indexPath) return collectionView.dequeueConfiguredReusableCell(using: controlCell, for: indexPath, item: item)
case let .asset(asset): case let .asset(asset):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell
@ -107,30 +123,23 @@ class AssetCollectionViewController: UICollectionViewController {
let interactivePopGesture = navigationController?.interactivePopGestureRecognizer { let interactivePopGesture = navigationController?.interactivePopGestureRecognizer {
singleFingerPanGesture.require(toFail: interactivePopGesture) singleFingerPanGesture.require(toFail: interactivePopGesture)
} }
PHPhotoLibrary.shared().register(self)
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
let scale = UIScreen.main.scale
let cellWidth = view.bounds.width / 3
thumbnailSize = CGSize(width: cellWidth * scale, height: cellWidth * scale)
loadAssets() 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() { private func loadAssets() {
var items = [Item.showCamera]
switch PHPhotoLibrary.authorizationStatus(for: .readWrite) { switch PHPhotoLibrary.authorizationStatus(for: .readWrite) {
case .notDetermined: case .notDetermined:
PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in
@ -142,8 +151,11 @@ class AssetCollectionViewController: UICollectionViewController {
// todo: better UI for this // todo: better UI for this
return return
case .authorized, .limited: case .authorized:
// todo: show "add more" button for limited access break
case .limited:
items.append(.changeLimitedSelection)
break break
@unknown default: @unknown default:
@ -156,7 +168,6 @@ class AssetCollectionViewController: UICollectionViewController {
fetchResult = fetchAssets(with: options) fetchResult = fetchAssets(with: options)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.assets]) snapshot.appendSections([.assets])
var items: [Item] = [.showCamera]
fetchResult.enumerateObjects { (asset, _, _) in fetchResult.enumerateObjects { (asset, _, _) in
items.append(.asset(asset)) items.append(.asset(asset))
} }
@ -176,11 +187,11 @@ class AssetCollectionViewController: UICollectionViewController {
// MARK: UICollectionViewDelegate // MARK: UICollectionViewDelegate
override func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool { func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool {
return true 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 } guard let item = dataSource.itemIdentifier(for: indexPath) else { return false }
if let delegate = delegate, if let delegate = delegate,
case let .asset(asset) = item { case let .asset(asset) = item {
@ -189,29 +200,32 @@ class AssetCollectionViewController: UICollectionViewController {
return true 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 } guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
switch item { switch item {
case .showCamera: case .showCamera:
collectionView.deselectItem(at: indexPath, animated: false) collectionView.deselectItem(at: indexPath, animated: false)
delegate?.captureFromCamera() delegate?.captureFromCamera()
case .changeLimitedSelection:
// todo: change observer
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self)
case .asset(_): case .asset(_):
updateItemsSelectedCount() updateItemsSelectedCount()
} }
} }
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
updateItemsSelectedCount() 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 } guard case let .asset(asset) = dataSource.itemIdentifier(for: indexPath) else { return nil }
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
return AssetPreviewViewController(asset: asset) return AssetPreviewViewController(asset: asset)
}, actionProvider: nil) }, 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?, if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?,
let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell { let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell {
let parameters = UIPreviewParameters() let parameters = UIPreviewParameters()
@ -237,6 +251,15 @@ extension AssetCollectionViewController {
} }
enum Item: Hashable { enum Item: Hashable {
case showCamera case showCamera
case changeLimitedSelection
case asset(PHAsset) 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 mode = .recognizingText
DispatchQueue.global(qos: .userInitiated).async { 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 handler = VNImageRequestHandler(data: data, options: [:])
let request = VNRecognizeTextRequest { (request, error) in let request = VNRecognizeTextRequest { (request, error) in
DispatchQueue.main.async { DispatchQueue.main.async {

View File

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

View File

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

View File

@ -59,19 +59,21 @@ struct ComposePollView: View {
} }
HStack { 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 // use .animation(nil) on pickers 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)) { Picker(selection: $poll.multiple, label: Text(poll.multiple ? "Allow multiple choices" : "Single choice")) {
Text("Allow multiple choices").tag(true) Text("Allow multiple choices").tag(true)
Text("Single choice").tag(false) Text("Single choice").tag(false)
} }
.animation(nil)
.pickerStyle(MenuPickerStyle()) .pickerStyle(MenuPickerStyle())
.frame(maxWidth: .infinity) .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 ForEach(Duration.allCases, id: \.self) { (duration) in
Text(ComposePollView.formatter.string(from: duration.timeInterval)!).tag(duration) Text(ComposePollView.formatter.string(from: duration.timeInterval)!).tag(duration)
} }
} }
.animation(nil)
.pickerStyle(MenuPickerStyle()) .pickerStyle(MenuPickerStyle())
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }

View File

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

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import WebURLFoundationExtras
class EmojiCollectionViewCell: UICollectionViewCell { class EmojiCollectionViewCell: UICollectionViewCell {
@ -45,7 +46,7 @@ class EmojiCollectionViewCell: UICollectionViewCell {
func updateUI(emoji: Emoji) { func updateUI(emoji: Emoji) {
currentEmojiShortcode = emoji.shortcode 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 } guard let image = image else { return }
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return } guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return }

View File

@ -56,7 +56,10 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
var textDidChange: (UITextView) -> Void var textDidChange: (UITextView) -> Void
@EnvironmentObject var uiState: ComposeUIState @EnvironmentObject var uiState: ComposeUIState
@EnvironmentObject var mastodonController: MastodonController
// todo: should these be part of the coordinator?
@State var visibilityButton: UIBarButtonItem? @State var visibilityButton: UIBarButtonItem?
@State var localOnlyButton: UIBarButtonItem?
func makeUIView(context: Context) -> UITextView { func makeUIView(context: Context) -> UITextView {
let textView = WrappedTextView() let textView = WrappedTextView()
@ -87,6 +90,22 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
self.visibilityButton = visibilityButton 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.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.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, 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) 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) { func updateUIView(_ uiView: UITextView, context: Context) {
if context.coordinator.skipSettingTextOnNextUpdate { if context.coordinator.skipSettingTextOnNextUpdate {
context.coordinator.skipSettingTextOnNextUpdate = false context.coordinator.skipSettingTextOnNextUpdate = false
@ -145,6 +175,14 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
visibilityButton.image = UIImage(systemName: visibility.imageName) visibilityButton.image = UIImage(systemName: visibility.imageName)
updateVisibilityMenu(visibilityButton) 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.text = $text
context.coordinator.didChange = textDidChange context.coordinator.didChange = textDidChange
context.coordinator.uiState = uiState context.coordinator.uiState = uiState

View File

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

View File

@ -11,6 +11,7 @@ import Pachyderm
class FeaturedProfileCollectionViewCell: UICollectionViewCell { class FeaturedProfileCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var clippingView: UIView!
@IBOutlet weak var headerImageView: UIImageView! @IBOutlet weak var headerImageView: UIImageView!
@IBOutlet weak var avatarContainerView: UIView! @IBOutlet weak var avatarContainerView: UIView!
@IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var avatarImageView: UIImageView!
@ -30,6 +31,17 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
noteTextView.defaultFont = .systemFont(ofSize: 16) noteTextView.defaultFont = .systemFont(ofSize: 16)
noteTextView.textContainer.lineBreakMode = .byTruncatingTail 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) 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() { @objc private func preferencesChanged() {
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <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="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.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"/> <rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bo4-Sd-caI"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YkJ-rV-f3C">
<rect key="frame" x="0.0" y="0.0" width="400" height="66"/> <rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
<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"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4wd-wq-Sh2"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bo4-Sd-caI">
<rect key="frame" x="2" y="2" width="60" height="60"/> <rect key="frame" x="0.0" y="0.0" width="400" height="66"/>
<color key="backgroundColor" systemColor="systemGray5Color"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="60" id="Xyl-Ry-J3r"/> <constraint firstAttribute="height" constant="66" id="9Aa-Up-chJ"/>
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="height" multiplier="1:1" id="YEc-fT-FRB"/>
</constraints> </constraints>
</imageView> </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> </subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints> <constraints>
<constraint firstAttribute="width" secondItem="RQe-uE-TEv" secondAttribute="height" multiplier="1:1" id="4vR-IF-yS8"/> <constraint firstAttribute="trailing" secondItem="bvj-F0-ggC" secondAttribute="trailing" constant="8" id="1sd-Df-jR1"/>
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="width" constant="4" id="52Q-zq-k28"/> <constraint firstItem="bvj-F0-ggC" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" constant="8" id="35h-Wh-fvk"/>
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerY" secondItem="RQe-uE-TEv" secondAttribute="centerY" id="Ped-H7-QtP"/> <constraint firstItem="voW-Is-1b2" firstAttribute="top" relation="greaterThanOrEqual" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="39l-yo-g8V"/>
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/> <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> </constraints>
</view> </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> </subviews>
</view> </view>
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/> <viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints> <constraints>
<constraint firstItem="bvj-F0-ggC" firstAttribute="top" secondItem="RQe-uE-TEv" secondAttribute="bottom" constant="4" id="8Nc-FF-kRX"/> <constraint firstAttribute="trailing" secondItem="YkJ-rV-f3C" secondAttribute="trailing" id="Dy3-h1-zfM"/>
<constraint firstItem="bo4-Sd-caI" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="CJ1-Be-L45"/> <constraint firstItem="YkJ-rV-f3C" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="Gld-3x-oE0"/>
<constraint firstAttribute="bottom" secondItem="bvj-F0-ggC" secondAttribute="bottom" constant="4" id="Hza-qE-Agk"/> <constraint firstItem="YkJ-rV-f3C" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="NIV-n8-4Rl"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="bottom" secondItem="4wd-wq-Sh2" secondAttribute="bottom" id="N0l-fE-AAX"/> <constraint firstAttribute="bottom" secondItem="YkJ-rV-f3C" secondAttribute="bottom" id="zNw-2z-Hlx"/>
<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"/>
</constraints> </constraints>
<connections> <connections>
<outlet property="avatarContainerView" destination="RQe-uE-TEv" id="tBI-fT-26P"/> <outlet property="avatarContainerView" destination="RQe-uE-TEv" id="tBI-fT-26P"/>
<outlet property="avatarImageView" destination="4wd-wq-Sh2" id="rba-cv-8fb"/> <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="displayNameLabel" destination="voW-Is-1b2" id="XVS-4d-PKx"/>
<outlet property="headerImageView" destination="bo4-Sd-caI" id="YkL-Wi-BXb"/> <outlet property="headerImageView" destination="bo4-Sd-caI" id="YkL-Wi-BXb"/>
<outlet property="noteTextView" destination="bvj-F0-ggC" id="Bbm-ai-bu1"/> <outlet property="noteTextView" destination="bvj-F0-ggC" id="Bbm-ai-bu1"/>

View File

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

View File

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

View File

@ -19,6 +19,8 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
self.mastodonController = mastodonController self.mastodonController = mastodonController
super.init(style: .grouped) super.init(style: .grouped)
dragEnabled = true
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -33,8 +35,6 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell") tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
// todo: enable drag
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView) { (tableView, indexPath, item) in dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView) { (tableView, indexPath, item) in
switch item { switch item {
case let .tag(hashtag): case let .tag(hashtag):
@ -75,6 +75,31 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil) 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 { extension TrendingHashtagsViewController {
@ -85,3 +110,11 @@ extension TrendingHashtagsViewController {
case tag(Hashtag) 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() { override func viewDidLayoutSubviews() {
super.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 // limit the image height to the safe area height, so the image doesn't overlap the top controls
// while zoomed all the way out // while zoomed all the way out
let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
@ -138,7 +137,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
centerImage() centerImage()
// todo: does this need to be in viewDidLayoutSubviews?
let notchedDeviceTopInsets: [CGFloat] = [ let notchedDeviceTopInsets: [CGFloat] = [
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max 44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
48, // iPhone XR, 11 48, // iPhone XR, 11

View File

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

View File

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

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEntry> { class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<ProfileStatusesViewController.Section, ProfileStatusesViewController.Item> {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
@ -43,50 +43,52 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
} }
func updateUI(account: AccountMO) { func updateUI(account: AccountMO) {
loadInitial() if isViewLoaded {
reloadInitial()
}
} }
override class func refreshCommandTitle() -> String { override class func refreshCommandTitle() -> String {
return NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title") return NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title")
} }
override func headerSectionsCount() -> Int { // MARK: - DiffableTimelineLikeTableViewController
return 1
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 { guard accountID != nil else {
completion(.failure(.noClient))
return return
} }
if !loaded { getStatuses { (response) in
loadPinnedStatuses() switch response {
} case let .failure(error):
completion(.failure(.client(error)))
super.loadInitial() case let .success(statuses, pagination):
} self.older = pagination?.older
self.newer = pagination?.newer
private func loadPinnedStatuses() { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
guard kind == .statuses else { DispatchQueue.main.async {
return var snapshot = self.dataSource.snapshot()
} snapshot.appendSections([.statuses])
getPinnedStatuses { (response) in snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .statuses)
guard case let .success(statuses, _) = response, if self.kind == .statuses {
!statuses.isEmpty else { self.loadPinnedStatuses(snapshot: { snapshot }, completion: completion)
// 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)
} else { } else {
self.sections[0] = items completion(.success(snapshot))
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
} }
} }
} }
@ -94,64 +96,94 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
} }
} }
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) { private func loadPinnedStatuses(snapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
getStatuses { (response) in guard kind == .statuses else {
guard case let .success(statuses, pagination) = response, completion(.success(snapshot()))
!statuses.isEmpty else { return
// todo: error message }
completion([]) getPinnedStatuses { (response) in
return switch response {
} case let .failure(error):
completion(.failure(.client(error)))
self.older = pagination?.older case let .success(statuses, _):
self.newer = pagination?.newer self.mastodonController.persistentContainer.addAll(statuses: statuses) {
DispatchQueue.main.async {
self.mastodonController.persistentContainer.addAll(statuses: statuses) { var snapshot = snapshot()
completion(statuses.map { ($0.id, .unknown) }) 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 { guard let older = older else {
completion([]) completion(.failure(.noOlder))
return return
} }
getStatuses(for: older) { (response) in getStatuses(for: older) { (response) in
guard case let .success(statuses, pagination) = response else { switch response {
// todo: error message case let .failure(error):
completion([]) completion(.failure(.client(error)))
return
}
self.older = pagination?.older case let .success(statuses, pagination):
guard !statuses.isEmpty else {
completion(.failure(.noOlder))
return
}
self.mastodonController.persistentContainer.addAll(statuses: statuses) { if let older = pagination?.older {
completion(statuses.map { ($0.id, .unknown) }) 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 { guard let newer = newer else {
completion([]) completion(.failure(.noNewer))
return return
} }
getStatuses(for: newer) { (response) in getStatuses(for: newer) { (response) in
guard case let .success(statuses, pagination) = response else { switch response {
// todo: error message case let .failure(error):
completion([]) completion(.failure(.client(error)))
return
}
if let newer = pagination?.newer { case let .success(statuses, pagination):
self.newer = newer guard !statuses.isEmpty else {
} completion(.failure(.noNewer))
return
}
self.mastodonController.persistentContainer.addAll(statuses: statuses) { if let newer = pagination?.newer {
completion(statuses.map { ($0.id, .unknown) }) 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() super.refresh()
if kind == .statuses { if kind == .statuses {
getPinnedStatuses { (response) in loadPinnedStatuses(snapshot: dataSource.snapshot) { (result) in
guard case let .success(newPinnedStatues, _) = response else { switch result {
// todo: error message case .failure(_):
return break
}
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatues) { case let .success(snapshot):
// 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)
}
DispatchQueue.main.async { DispatchQueue.main.async {
UIView.performWithoutAnimation { self.dataSource.apply(snapshot)
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)
}
}
} }
} }
} }
} }
} }
// 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 { 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 { extension ProfileStatusesViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController } var apiController: MastodonController { mastodonController }
} }
@ -245,18 +255,12 @@ extension ProfileStatusesViewController: StatusTableViewCellDelegate {
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { 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) prefetchStatuses(with: ids)
} }
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
let ids: [String] = indexPaths.compactMap { let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id }
guard $0.section < sections.count,
$0.row < sections[$0.section].count else {
return nil
}
return item(for: $0).id
}
cancelPrefetchingStatuses(with: ids) 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, // 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 // if old was not scrolled all the way to the top
new.tableView.layoutIfNeeded() new.tableView.layoutIfNeeded()
UIView.performWithoutAnimation { let snapshot = new.dataSource.snapshot()
new.tableView.performBatchUpdates(nil, completion: nil) new.dataSource.apply(snapshot, animatingDifferences: false)
}
completion?(finished) completion?(finished)
} }

View File

@ -67,8 +67,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
} }
private func pruneOffscreenRows() { private func pruneOffscreenRows() {
guard let lastVisibleRow = lastLastVisibleRow, guard let lastVisibleRow = tableView.indexPathsForVisibleRows?.last else {
lastVisibleRow.section < tableView.numberOfSections else {
return return
} }
@ -77,30 +76,24 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
let lastVisibleRowSection = snapshot.sectionIdentifiers[lastVisibleRow.section] let lastVisibleRowSection = snapshot.sectionIdentifiers[lastVisibleRow.section]
let contentSections = snapshot.sectionIdentifiers.filter { timelineContentSections().contains($0) } 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 return
} } else if lastVisibleRow.section == maxContentSectionIndex {
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 {
let items = snapshot.itemIdentifiers(inSection: lastVisibleRowSection) let items = snapshot.itemIdentifiers(inSection: lastVisibleRowSection)
if lastVisibleRow.row < items.count - pageSize { let numberOfPagesToPrune = (items.count - lastVisibleRow.row - 1) / pageSize
let itemsToRemove = Array(items.suffix(pageSize)) if numberOfPagesToPrune > 0 {
let itemsToRemove = Array(items.suffix(numberOfPagesToPrune * pageSize))
snapshot.deleteItems(itemsToRemove) snapshot.deleteItems(itemsToRemove)
willRemoveItems(itemsToRemove) willRemoveItems(itemsToRemove)
} else { } else {
return return
} }
} else { } else {
// unreachable
return return
} }

View File

@ -156,7 +156,6 @@ extension MenuPreviewProvider {
} }
let bookmarked = status.bookmarked ?? false let bookmarked = status.bookmarked ?? false
let muted = status.muted
var actionsSection = [ var actionsSection = [
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in 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 { if includeReply {
@ -186,18 +176,35 @@ extension MenuPreviewProvider {
}), at: 0) }), at: 0)
} }
if mastodonController.account != nil && mastodonController.account.id == status.account.id { if let account = mastodonController.account {
let pinned = status.pinned ?? false // only allow muting conversations that either current user posted or is participating in (technically, is mentioned, since that's the best we can do)
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in if status.account.id == account.id || status.mentions.contains(where: { $0.id == account.id }) {
guard let self = self else { return } let muted = status.muted
let request = (pinned ? Status.unpin : Status.pin)(status.id) actionsSection.append(createAction(identifier: "mute", title: muted ? "Unmute Conversation" : "Mute Conversation", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
self.mastodonController?.run(request, completion: { [weak self] (response) in
guard let self = self else { return } guard let self = self else { return }
if case let .success(status, _) = response { let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false) 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 { 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 SwiftUI
import Pachyderm import Pachyderm
import WebURLFoundationExtras
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
@ -20,7 +21,8 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
init(account: Account, fontSize: Int) { init(account: Account, fontSize: Int) {
self.account = account self.account = account
self.fontSize = fontSize 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 { var body: some View {
@ -47,7 +49,7 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
} }
group.enter() group.enter()
let request = ImageCache.emojis.get(emoji.url) { (_, image) in let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
defer { group.leave() } defer { group.leave() }
guard let image = image else { return } 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 { class AttachmentView: GIFImageView {
static let queue = DispatchQueue(label: "Attachment Thumbnail", qos: .userInitiated, attributes: .concurrent)
weak var delegate: AttachmentViewDelegate? weak var delegate: AttachmentViewDelegate?
var playImageView: UIImageView? var playImageView: UIImageView?
@ -108,7 +110,7 @@ class AttachmentView: GIFImageView {
} }
if let hash = attachment.blurHash { 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 } guard let self = self else { return }
let size: CGSize let size: CGSize
if let meta = self.attachment.meta, if let meta = self.attachment.meta,
@ -191,8 +193,7 @@ class AttachmentView: GIFImageView {
}) })
} else { } else {
let attachmentURL = self.attachment.url let attachmentURL = self.attachment.url
// todo: use a single dispatch queue AttachmentView.queue.async {
DispatchQueue.global(qos: .userInitiated).async {
let asset = AVURLAsset(url: attachmentURL) let asset = AVURLAsset(url: attachmentURL)
let generator = AVAssetImageGenerator(asset: asset) let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true generator.appliesPreferredTrackTransform = true
@ -237,7 +238,7 @@ class AttachmentView: GIFImageView {
func loadGifv() { func loadGifv() {
let attachmentURL = self.attachment.url let attachmentURL = self.attachment.url
let asset = AVURLAsset(url: attachmentURL) let asset = AVURLAsset(url: attachmentURL)
DispatchQueue.global(qos: .userInitiated).async { AttachmentView.queue.async {
let generator = AVAssetImageGenerator(asset: asset) let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true generator.appliesPreferredTrackTransform = true
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return } guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <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="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"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
@ -39,7 +39,7 @@
</stackView> </stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Xrw-2v-ybZ" customClass="HashtagHistoryView" customModule="Tusker" customModuleProvider="target"> <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"/> <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> <constraints>
<constraint firstAttribute="height" constant="44" id="W4C-uw-zWg"/> <constraint firstAttribute="height" constant="44" id="W4C-uw-zWg"/>
<constraint firstAttribute="width" constant="100" id="XHb-vd-qNk"/> <constraint firstAttribute="width" constant="100" id="XHb-vd-qNk"/>
@ -64,9 +64,4 @@
<point key="canvasLocation" x="132" y="132"/> <point key="canvasLocation" x="132" y="132"/>
</tableViewCell> </tableViewCell>
</objects> </objects>
<resources>
<systemColor name="secondarySystemGroupedBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document> </document>

View File

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

View File

@ -28,7 +28,7 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
@IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: EmojiLabel! @IBOutlet weak var displayNameLabel: EmojiLabel!
@IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var visibilityImageView: UIImageView! @IBOutlet weak var metaIndicatorsView: StatusMetaIndicatorsView!
@IBOutlet weak var contentWarningLabel: EmojiLabel! @IBOutlet weak var contentWarningLabel: EmojiLabel!
@IBOutlet weak var collapseButton: UIButton! @IBOutlet weak var collapseButton: UIButton!
@IBOutlet weak var contentTextView: StatusContentTextView! @IBOutlet weak var contentTextView: StatusContentTextView!
@ -43,27 +43,27 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
private(set) var nextThreadLinkView: UIView? private(set) var nextThreadLinkView: UIView?
var statusID: String! var statusID: String!
var accountID: String! private(set) var accountID: String!
var favorited = false { private var favorited = false {
didSet { didSet {
favoriteButton.tintColor = favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor favoriteButton.tintColor = favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor
} }
} }
var reblogged = false { private var reblogged = false {
didSet { didSet {
reblogButton.tintColor = reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor 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 { var collapsible = false {
didSet { didSet {
collapseButton.isHidden = !collapsible collapseButton.isHidden = !collapsible
statusState?.collapsible = collapsible statusState?.collapsible = collapsible
} }
} }
var collapsed = false { private var collapsed = false {
didSet { didSet {
statusState?.collapsed = collapsed statusState?.collapsed = collapsed
} }
@ -166,16 +166,11 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
} }
let reblogDisabled: Bool let reblogDisabled: Bool
switch mastodonController.instance?.instanceType { if mastodonController.instanceFeatures.boostToOriginalAudience {
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:
// Pleroma allows 'Boost to original audience' for your own private posts // Pleroma allows 'Boost to original audience' for your own private posts
reblogDisabled = status.visibility == .direct || (status.visibility == .private && status.account.id != mastodonController.account.id) 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 reblogButton.isEnabled = !reblogDisabled && mastodonController.loggedIn
@ -242,11 +237,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
} }
func updateStatusIconsForPreferences(_ status: StatusMO) { func updateStatusIconsForPreferences(_ status: StatusMO) {
visibilityImageView.isHidden = !Preferences.shared.alwaysShowStatusVisibilityIcon metaIndicatorsView.updateUI(status: status)
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)
}
let reblogButtonImage: UIImage let reblogButtonImage: UIImage
if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogButton.isEnabled { if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogButton.isEnabled {
reblogButtonImage = UIImage(systemName: "repeat")! reblogButtonImage = UIImage(systemName: "repeat")!

View File

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

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <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="Image references" minToolsVersion="12.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
@ -28,8 +29,8 @@
<constraint firstAttribute="width" constant="50" id="Yxp-Vr-dfl"/> <constraint firstAttribute="width" constant="50" id="Yxp-Vr-dfl"/>
</constraints> </constraints>
</imageView> </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"> <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="255" height="29"/> <rect key="frame" x="58" y="0.0" width="146.5" height="29"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@ -40,29 +41,27 @@
<color key="textColor" systemColor="secondaryLabelColor"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="3Qu-IO-5wt"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xD6-dy-0XV" customClass="StatusMetaIndicatorsView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="321" y="0.5" width="22" height="20.5"/> <rect key="frame" x="212.5" y="0.0" width="130.5" height="22"/>
<color key="tintColor" systemColor="secondaryLabelColor"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="22" id="Kqh-qI-dSa"/> <constraint firstAttribute="height" constant="22" placeholder="YES" id="wF5-Ii-LO5"/>
<constraint firstAttribute="width" constant="22" id="QY1-tL-QHr"/>
</constraints> </constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/> </view>
</imageView>
</subviews> </subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints> <constraints>
<constraint firstAttribute="trailing" secondItem="SWg-Ka-QyP" secondAttribute="trailing" id="4g6-BT-eW4"/> <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="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="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 firstItem="xD6-dy-0XV" firstAttribute="leading" secondItem="lZY-2e-17d" secondAttribute="trailing" constant="8" id="PfV-YZ-k9j"/>
<constraint firstAttribute="trailing" secondItem="3Qu-IO-5wt" secondAttribute="trailing" id="NWa-lL-aLk"/>
<constraint firstItem="mB9-HO-1vf" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="R7P-rD-Gbm"/> <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 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="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="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="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> </constraints>
</view> </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"> <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="displayNameLabel" destination="lZY-2e-17d" id="7og-23-eHy"/>
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/> <outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/>
<outlet property="favoriteButton" destination="DhN-rJ-jdA" id="b2Q-ch-kSP"/> <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="moreButton" destination="Ujo-Ap-dmK" id="2ba-5w-HDx"/>
<outlet property="pollView" destination="TLv-Xu-tT1" id="hJX-YD-lNr"/> <outlet property="pollView" destination="TLv-Xu-tT1" id="hJX-YD-lNr"/>
<outlet property="profileDetailContainerView" destination="Cnd-Fj-B7l" id="wco-VB-VQx"/> <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="totalFavoritesButton" destination="yyj-Bs-Vjq" id="4pV-Qi-Z2X"/>
<outlet property="totalReblogsButton" destination="dem-vG-cPB" id="i9E-Qn-d76"/> <outlet property="totalReblogsButton" destination="dem-vG-cPB" id="i9E-Qn-d76"/>
<outlet property="usernameLabel" destination="SWg-Ka-QyP" id="h2I-g4-AD9"/> <outlet property="usernameLabel" destination="SWg-Ka-QyP" id="h2I-g4-AD9"/>
<outlet property="visibilityImageView" destination="3Qu-IO-5wt" id="sFB-ni-FcZ"/>
</connections> </connections>
<point key="canvasLocation" x="40.799999999999997" y="-122.78860569715144"/> <point key="canvasLocation" x="40.799999999999997" y="-122.78860569715144"/>
</view> </view>
@ -265,8 +264,7 @@
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/> <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="chevron.down" catalog="system" width="128" height="72"/>
<image name="ellipsis" catalog="system" width="128" height="37"/> <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="98"/>
<image name="repeat" catalog="system" width="128" height="99"/>
<image name="star.fill" catalog="system" width="128" height="116"/> <image name="star.fill" catalog="system" width="128" height="116"/>
<systemColor name="labelColor"> <systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import SafariServices import SafariServices
import WebURLFoundationExtras
class StatusCardView: UIView { class StatusCardView: UIView {
@ -141,9 +142,9 @@ class StatusCardView: UIView {
if let imageURL = card.image { if let imageURL = card.image {
placeholderImageView.isHidden = true placeholderImageView.isHidden = true
imageRequest = ImageCache.attachments.get(imageURL, completion: { (_, image) in imageRequest = ImageCache.attachments.get(URL(imageURL)!, completion: { (_, image) in
guard let image = image, guard let image = image,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: imageURL, image: image) else { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(imageURL)!, image: image) else {
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {
@ -163,8 +164,7 @@ class StatusCardView: UIView {
let imageViewSize = self.imageView.bounds.size let imageViewSize = self.imageView.bounds.size
// todo: merge this code with AttachmentView, use a single DispatchQueue AttachmentView.queue.async { [weak self] in
DispatchQueue.global(qos: .default).async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
let size: CGSize let size: CGSize
@ -201,7 +201,7 @@ class StatusCardView: UIView {
setNeedsDisplay() setNeedsDisplay()
if let card = card, let delegate = navigationDelegate { 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 } guard let card = card else { return nil }
return UIContextMenuConfiguration(identifier: nil) { return UIContextMenuConfiguration(identifier: nil) {
return SFSafariViewController(url: card.url) return SFSafariViewController(url: URL(card.url)!)
} actionProvider: { (_) in } 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) 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 reblogLabel: EmojiLabel!
@IBOutlet weak var timestampLabel: UILabel! @IBOutlet weak var timestampLabel: UILabel!
@IBOutlet weak var pinImageView: UIImageView! @IBOutlet weak var pinImageView: UIImageView!
@IBOutlet weak var replyImageView: UIImageView!
var reblogStatusID: String? var reblogStatusID: String?
var rebloggerID: String? var rebloggerID: String?
@ -80,6 +79,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
updateRebloggerLabel(reblogger: status.account) updateRebloggerLabel(reblogger: status.account)
status = rebloggedStatus status = rebloggedStatus
// necessary b/c statusID is initially set to the reblog status ID in updateUI(statusID:state:)
statusID = rebloggedStatus.id statusID = rebloggedStatus.id
} else { } else {
reblogStatusID = nil reblogStatusID = nil
@ -91,9 +91,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
doUpdateTimestamp(status: status) doUpdateTimestamp(status: status)
let pinned = showPinned && (status.pinned ?? false) timestampLabel.isHidden = showPinned
timestampLabel.isHidden = pinned pinImageView.isHidden = !showPinned
pinImageView.isHidden = !pinned
} }
override func updateGrayscaleableUI(account: AccountMO, status: StatusMO) { override func updateGrayscaleableUI(account: AccountMO, status: StatusMO) {
@ -117,9 +116,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
} }
override func updateStatusIconsForPreferences(_ status: StatusMO) { override func updateStatusIconsForPreferences(_ status: StatusMO) {
if showReplyIndicator {
metaIndicatorsView.allowedIndicators = .all
} else {
metaIndicatorsView.allowedIndicators = .all.subtracting(.reply)
}
super.updateStatusIconsForPreferences(status) super.updateStatusIconsForPreferences(status)
replyImageView.isHidden = !Preferences.shared.showIsStatusReplyIcon || !showReplyIndicator || status.inReplyToID == nil
} }
private func updateTimestamp() { private func updateTimestamp() {

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <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="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -141,28 +141,13 @@
</label> </label>
</subviews> </subviews>
</stackView> </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"/> <rect key="frame" x="0.0" y="54" width="50" height="22"/>
<subviews> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<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"> <constraints>
<rect key="frame" x="0.0" y="0.0" width="25.5" height="21.5"/> <constraint firstAttribute="height" constant="22" placeholder="YES" id="ipd-WE-P20"/>
<color key="tintColor" systemColor="secondaryLabelColor"/> </constraints>
<accessibility key="accessibilityConfiguration" label="Is a reply"/> </view>
<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>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TUP-Nz-5Yh"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TUP-Nz-5Yh">
<rect key="frame" x="0.0" y="169.5" width="335" height="26"/> <rect key="frame" x="0.0" y="169.5" width="335" height="26"/>
<subviews> <subviews>
@ -226,15 +211,16 @@
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/> <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="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="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="gIY-Wp-RSk" firstAttribute="leading" secondItem="qBn-Gk-DCa" secondAttribute="trailing" constant="8" id="AQs-QN-j49"/>
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="oie-wK-IpU" secondAttribute="bottom" id="7Xp-Sa-Rfk"/>
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/> <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="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="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 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 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"/> <constraint firstItem="QMP-j2-HLn" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="zeW-tQ-uJl"/>
</constraints> </constraints>
</view> </view>
@ -262,26 +248,23 @@
<outlet property="contentWarningLabel" destination="inI-Og-YiU" id="C7a-eK-qcx"/> <outlet property="contentWarningLabel" destination="inI-Og-YiU" id="C7a-eK-qcx"/>
<outlet property="displayNameLabel" destination="gll-xe-FSr" id="vVS-WM-Wqx"/> <outlet property="displayNameLabel" destination="gll-xe-FSr" id="vVS-WM-Wqx"/>
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/> <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="moreButton" destination="982-J4-NGl" id="Pux-tL-aWe"/>
<outlet property="pinImageView" destination="wtt-8G-Ua1" id="mE8-oe-m1l"/> <outlet property="pinImageView" destination="wtt-8G-Ua1" id="mE8-oe-m1l"/>
<outlet property="pollView" destination="x3b-Zl-9F0" id="WIF-Oz-cnm"/> <outlet property="pollView" destination="x3b-Zl-9F0" id="WIF-Oz-cnm"/>
<outlet property="reblogButton" destination="6tW-z8-Qh9" id="u2t-8D-kOn"/> <outlet property="reblogButton" destination="6tW-z8-Qh9" id="u2t-8D-kOn"/>
<outlet property="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/> <outlet property="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/>
<outlet property="replyButton" destination="rKF-yF-KIa" id="rka-q1-o4a"/> <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="timestampLabel" destination="35d-EA-ReR" id="Ny2-nV-nqP"/>
<outlet property="usernameLabel" destination="j89-zc-SFa" id="bXX-FZ-fCp"/> <outlet property="usernameLabel" destination="j89-zc-SFa" id="bXX-FZ-fCp"/>
<outlet property="visibilityImageView" destination="LRh-Cc-1br" id="pxm-JK-jAz"/>
</connections> </connections>
<point key="canvasLocation" x="29.600000000000001" y="79.160419790104953"/> <point key="canvasLocation" x="29.600000000000001" y="79.160419790104953"/>
</view> </view>
</objects> </objects>
<resources> <resources>
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/> <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="chevron.down" catalog="system" width="128" height="72"/>
<image name="ellipsis" catalog="system" width="128" height="37"/> <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="pin.fill" catalog="system" width="119" height="128"/>
<image name="repeat" catalog="system" width="128" height="98"/> <image name="repeat" catalog="system" width="128" height="98"/>
<image name="star.fill" catalog="system" width="128" height="116"/> <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) { let status = mastodonController.persistentContainer.status(for: statusID) {
mention = status.mentions.first { (mention) in mention = status.mentions.first { (mention) in
// Mastodon and Pleroma include the @ in the <a> text, GNU Social does not // 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 { } else {
mention = nil mention = nil

View File

@ -61,5 +61,37 @@
<string>%u replies</string> <string>%u replies</string>
</dict> </dict>
</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> </dict>
</plist> </plist>