Compare commits
No commits in common. "fc7e7f502be7c61dc02a584597dee86ea62aa026" and "6261318df12980955cf5c7b2f2dd338ab8d5c3b9" have entirely different histories.
fc7e7f502b
...
6261318df1
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,19 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2023.5 (90)
|
|
||||||
Features/Improvements:
|
|
||||||
- Improve performance when scrolling through timeline
|
|
||||||
- Improve error messages when editing filters
|
|
||||||
- Enable editing posts on Pleroma 2.5+
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix share sheet extension not working with Apple News
|
|
||||||
- Fix crash when sharing certain photos with share extension
|
|
||||||
- Fix reblog button being enabled on Direct posts
|
|
||||||
- Fix expanded statuses collapsing when opening Conversation
|
|
||||||
- Fix main post in Conversation flickering when context loaded
|
|
||||||
- Fix link card images not loading on Mastodon
|
|
||||||
|
|
||||||
## 2023.5 (89)
|
## 2023.5 (89)
|
||||||
This build is a hotfix for an issue loading notifications in certain circumstances. The changelong for the previous build (adding post editing) is included below.
|
This build is a hotfix for an issue loading notifications in certain circumstances. The changelong for the previous build (adding post editing) is included below.
|
||||||
|
|
||||||
|
|
|
@ -95,21 +95,19 @@ class AttachmentThumbnailController: ViewController {
|
||||||
self.gifController = GIFController(gifData: data)
|
self.gifController = GIFController(gifData: data)
|
||||||
} else if type.conforms(to: .image),
|
} else if type.conforms(to: .image),
|
||||||
let image = UIImage(data: data) {
|
let image = UIImage(data: data) {
|
||||||
// using prepareThumbnail on images from PHPicker results in extremely high memory usage,
|
if fullSize {
|
||||||
// crashing share extension. see FB12186346
|
|
||||||
// if fullSize {
|
|
||||||
image.prepareForDisplay { prepared in
|
image.prepareForDisplay { prepared in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.image = image
|
self.image = prepared
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// } else {
|
} else {
|
||||||
// image.prepareThumbnail(of: CGSize(width: 80, height: 80)) { prepared in
|
image.prepareThumbnail(of: CGSize(width: 80, height: 80)) { prepared in
|
||||||
// DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
// self.image = prepared
|
self.image = prepared
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ public class InstanceFeatures: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var needsWideColorGamutHack: Bool {
|
public var needsWideColorGamutHack: Bool {
|
||||||
if case .mastodon(_, let version) = instanceType {
|
if case .mastodon(_, .some(let version)) = instanceType {
|
||||||
return version < Version(4, 0, 0)
|
return version < Version(4, 0, 0)
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true
|
||||||
|
@ -116,16 +116,8 @@ public class InstanceFeatures: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var editStatuses: Bool {
|
public var editStatuses: Bool {
|
||||||
switch instanceType {
|
// todo: does this require a particular akkoma version?
|
||||||
case .mastodon(_, let v) where v >= Version(3, 5, 0):
|
hasMastodonVersion(3, 5, 0) || instanceType.isPleroma(.akkoma(nil))
|
||||||
return true
|
|
||||||
case .pleroma(.vanilla(let v)) where v >= Version(2, 5, 0):
|
|
||||||
return true
|
|
||||||
case .pleroma(.akkoma(nil)):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var needsEditAttachmentsInSeparateRequest: Bool {
|
public var needsEditAttachmentsInSeparateRequest: Bool {
|
||||||
|
@ -196,7 +188,7 @@ public class InstanceFeatures: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hasMastodonVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
public func hasMastodonVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
||||||
if case .mastodon(_, let version) = instanceType {
|
if case .mastodon(_, .some(let version)) = instanceType {
|
||||||
return version >= Version(major, minor, patch)
|
return version >= Version(major, minor, patch)
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
|
@ -205,7 +197,7 @@ public class InstanceFeatures: ObservableObject {
|
||||||
|
|
||||||
func hasPleromaVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
func hasPleromaVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
||||||
switch instanceType {
|
switch instanceType {
|
||||||
case .pleroma(.vanilla(let version)), .pleroma(.akkoma(let version)):
|
case .pleroma(.vanilla(.some(let version))), .pleroma(.akkoma(.some(let version))):
|
||||||
return version >= Version(major, minor, patch)
|
return version >= Version(major, minor, patch)
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -291,3 +283,61 @@ extension InstanceFeatures {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension InstanceFeatures {
|
||||||
|
@_spi(InstanceType) public struct Version: Equatable, Comparable, CustomStringConvertible {
|
||||||
|
private static let regex = try! NSRegularExpression(pattern: "^(\\d+)\\.(\\d+)\\.(\\d+).*$")
|
||||||
|
|
||||||
|
let major: Int
|
||||||
|
let minor: Int
|
||||||
|
let patch: Int
|
||||||
|
|
||||||
|
init(_ major: Int, _ minor: Int, _ patch: Int) {
|
||||||
|
self.major = major
|
||||||
|
self.minor = minor
|
||||||
|
self.patch = patch
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(string: String) {
|
||||||
|
guard let match = Version.regex.firstMatch(in: string, range: NSRange(location: 0, length: string.utf16.count)),
|
||||||
|
match.numberOfRanges == 4 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let majorStr = (string as NSString).substring(with: match.range(at: 1))
|
||||||
|
let minorStr = (string as NSString).substring(with: match.range(at: 2))
|
||||||
|
let patchStr = (string as NSString).substring(with: match.range(at: 3))
|
||||||
|
guard let major = Int(majorStr),
|
||||||
|
let minor = Int(minorStr),
|
||||||
|
let patch = Int(patchStr) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.major = major
|
||||||
|
self.minor = minor
|
||||||
|
self.patch = patch
|
||||||
|
}
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
"\(major).\(minor).\(patch)"
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: Version, rhs: Version) -> Bool {
|
||||||
|
return lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func < (lhs: InstanceFeatures.Version, rhs: InstanceFeatures.Version) -> Bool {
|
||||||
|
if lhs.major < rhs.major {
|
||||||
|
return true
|
||||||
|
} else if lhs.major > rhs.major {
|
||||||
|
return false
|
||||||
|
} else if lhs.minor < rhs.minor {
|
||||||
|
return true
|
||||||
|
} else if lhs.minor > rhs.minor {
|
||||||
|
return false
|
||||||
|
} else if lhs.patch < rhs.patch {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
//
|
|
||||||
// Version.swift
|
|
||||||
// InstanceFeatures
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/14/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@_spi(InstanceType) public struct Version: Equatable, Comparable, CustomStringConvertible {
|
|
||||||
private static let regex = try! NSRegularExpression(pattern: "^(\\d+)\\.(\\d+)\\.(\\d+).*$")
|
|
||||||
|
|
||||||
let major: Int
|
|
||||||
let minor: Int
|
|
||||||
let patch: Int
|
|
||||||
|
|
||||||
init(_ major: Int, _ minor: Int, _ patch: Int) {
|
|
||||||
self.major = major
|
|
||||||
self.minor = minor
|
|
||||||
self.patch = patch
|
|
||||||
}
|
|
||||||
|
|
||||||
init?(string: String) {
|
|
||||||
guard let match = Version.regex.firstMatch(in: string, range: NSRange(location: 0, length: string.utf16.count)),
|
|
||||||
match.numberOfRanges == 4 else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let majorStr = (string as NSString).substring(with: match.range(at: 1))
|
|
||||||
let minorStr = (string as NSString).substring(with: match.range(at: 2))
|
|
||||||
let patchStr = (string as NSString).substring(with: match.range(at: 3))
|
|
||||||
guard let major = Int(majorStr),
|
|
||||||
let minor = Int(minorStr),
|
|
||||||
let patch = Int(patchStr) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.major = major
|
|
||||||
self.minor = minor
|
|
||||||
self.patch = patch
|
|
||||||
}
|
|
||||||
|
|
||||||
public var description: String {
|
|
||||||
"\(major).\(minor).\(patch)"
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func ==(lhs: Version, rhs: Version) -> Bool {
|
|
||||||
return lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func <(lhs: Version, rhs: Version) -> Bool {
|
|
||||||
if lhs.major < rhs.major {
|
|
||||||
return true
|
|
||||||
} else if lhs.major > rhs.major {
|
|
||||||
return false
|
|
||||||
} else if lhs.minor < rhs.minor {
|
|
||||||
return true
|
|
||||||
} else if lhs.minor > rhs.minor {
|
|
||||||
return false
|
|
||||||
} else if lhs.patch < rhs.patch {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func <(lhs: Version?, rhs: Version) -> Bool {
|
|
||||||
guard let lhs else {
|
|
||||||
// nil is less than or equal to everything
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return lhs < rhs
|
|
||||||
}
|
|
||||||
|
|
||||||
func >=(lhs: Version?, rhs: Version) -> Bool {
|
|
||||||
guard let lhs else {
|
|
||||||
// nil is less than or equal to everything
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return lhs >= rhs
|
|
||||||
}
|
|
|
@ -539,7 +539,7 @@ extension Client {
|
||||||
self.type = type
|
self.type = type
|
||||||
}
|
}
|
||||||
|
|
||||||
public var errorDescription: String? {
|
public var localizedDescription: String {
|
||||||
switch type {
|
switch type {
|
||||||
case .networkError(let error):
|
case .networkError(let error):
|
||||||
return "Network Error: \(error.localizedDescription)"
|
return "Network Error: \(error.localizedDescription)"
|
||||||
|
|
|
@ -24,9 +24,7 @@ public final class CollapseState: Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func copy() -> CollapseState {
|
public func copy() -> CollapseState {
|
||||||
let new = CollapseState(collapsible: self.collapsible, collapsed: self.collapsed)
|
return CollapseState(collapsible: self.collapsible, collapsed: self.collapsed)
|
||||||
new.statusPropertiesHash = self.statusPropertiesHash
|
|
||||||
return new
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
|
|
@ -12,8 +12,6 @@
|
||||||
<integer>4</integer>
|
<integer>4</integer>
|
||||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
<key>NSExtensionActivationSupportsText</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
|
@ -71,43 +71,36 @@ class ShareViewController: UIViewController {
|
||||||
|
|
||||||
private func getDraftConfigurationFromExtensionContext() async -> (String, [DraftAttachment]) {
|
private func getDraftConfigurationFromExtensionContext() async -> (String, [DraftAttachment]) {
|
||||||
guard let extensionContext,
|
guard let extensionContext,
|
||||||
let inputItem = (extensionContext.inputItems as? [NSExtensionItem])?.first else {
|
let inputItem = (extensionContext.inputItems as? [NSExtensionItem])?.first,
|
||||||
|
let itemProvider = inputItem.attachments?.first else {
|
||||||
return ("", [])
|
return ("", [])
|
||||||
}
|
}
|
||||||
var text: String = ""
|
if let url: NSURL = await getObject(from: itemProvider) {
|
||||||
var url: URL?
|
if let title = inputItem.attributedTitle ?? inputItem.attributedContentText {
|
||||||
var attachments: [DraftAttachment] = []
|
return ("\n\n\(title.string)\n\(url.absoluteString ?? "")", [])
|
||||||
|
} else {
|
||||||
for itemProvider in inputItem.attachments ?? [] {
|
return ("\n\n\(url.absoluteString ?? "")", [])
|
||||||
if let attached: NSURL = await getObject(from: itemProvider) {
|
|
||||||
if url == nil {
|
|
||||||
url = attached as URL
|
|
||||||
}
|
|
||||||
} else if let s: NSString = await getObject(from: itemProvider) {
|
|
||||||
if text.isEmpty {
|
|
||||||
text = s as String
|
|
||||||
}
|
|
||||||
} else if let attachment: DraftAttachment = await getObject(from: itemProvider) {
|
|
||||||
attachments.append(attachment)
|
|
||||||
}
|
}
|
||||||
}
|
} else if let text: NSString = await getObject(from: itemProvider) {
|
||||||
|
return ("\n\n\(text)", [])
|
||||||
|
} else if let attributedContent = inputItem.attributedContentText {
|
||||||
|
return ("\n\n\(attributedContent.string)", [])
|
||||||
|
} else {
|
||||||
|
let attachments = await withTaskGroup(of: DraftAttachment?.self, returning: [DraftAttachment].self) { group in
|
||||||
|
for provider in inputItem.attachments! {
|
||||||
|
group.addTask { @MainActor in
|
||||||
|
await self.getObject(from: provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if text.isEmpty,
|
return await group.reduce(into: [], { partialResult, result in
|
||||||
let s = inputItem.attributedTitle ?? inputItem.attributedContentText {
|
if let result {
|
||||||
text = s.string
|
partialResult.append(result)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if let url {
|
|
||||||
if !text.isEmpty {
|
|
||||||
text += "\n"
|
|
||||||
}
|
}
|
||||||
text += url.absoluteString
|
return ("", attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !text.isEmpty {
|
|
||||||
text = "\n\n\(text)"
|
|
||||||
}
|
|
||||||
return (text, attachments)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getObject<T: NSItemProviderReading>(from itemProvider: NSItemProvider) async -> T? {
|
private func getObject<T: NSItemProviderReading>(from itemProvider: NSItemProvider) async -> T? {
|
||||||
|
|
|
@ -246,7 +246,7 @@
|
||||||
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */; };
|
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */; };
|
||||||
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */; };
|
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */; };
|
||||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */; };
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */; };
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */; };
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */; };
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */; };
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */; };
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; };
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; };
|
||||||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
||||||
|
@ -301,8 +301,6 @@
|
||||||
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; };
|
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; };
|
||||||
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */; };
|
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */; };
|
||||||
D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; };
|
D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; };
|
||||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; };
|
|
||||||
D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; };
|
|
||||||
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
||||||
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
||||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
||||||
|
@ -639,7 +637,7 @@
|
||||||
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
|
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedPageViewController.swift; sourceTree = "<group>"; };
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedPageViewController.swift; sourceTree = "<group>"; };
|
||||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameView.swift; sourceTree = "<group>"; };
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableViewController.swift; sourceTree = "<group>"; };
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -702,8 +700,6 @@
|
||||||
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditContentTextView.swift; sourceTree = "<group>"; };
|
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = "<group>"; };
|
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = "<group>"; };
|
||||||
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = "<group>"; };
|
|
||||||
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
|
|
||||||
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
||||||
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1353,7 +1349,7 @@
|
||||||
D6BED1722126661300F02DA0 /* Views */ = {
|
D6BED1722126661300F02DA0 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */,
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
||||||
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
||||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
||||||
|
@ -1375,7 +1371,6 @@
|
||||||
D65B4B69297777D900DABDFB /* StatusNotFoundView.swift */,
|
D65B4B69297777D900DABDFB /* StatusNotFoundView.swift */,
|
||||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
||||||
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */,
|
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */,
|
||||||
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */,
|
|
||||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||||
|
@ -1388,7 +1383,6 @@
|
||||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||||
D641C78A213DD926004B4513 /* Status */,
|
D641C78A213DD926004B4513 /* Status */,
|
||||||
D64AAE8F26C80DB600FC57FB /* Toast */,
|
D64AAE8F26C80DB600FC57FB /* Toast */,
|
||||||
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */,
|
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1951,7 +1945,6 @@
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||||
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
||||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
||||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
|
|
||||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||||
D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */,
|
D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */,
|
||||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||||
|
@ -2016,7 +2009,7 @@
|
||||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
||||||
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
|
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
||||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
||||||
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
||||||
|
@ -2112,7 +2105,6 @@
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
||||||
D6ADB6E828E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift in Sources */,
|
D6ADB6E828E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift in Sources */,
|
||||||
D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */,
|
|
||||||
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */,
|
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */,
|
||||||
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */,
|
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */,
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||||
|
@ -2378,7 +2370,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 = 91;
|
CURRENT_PROJECT_VERSION = 89;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2444,7 +2436,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 = 91;
|
CURRENT_PROJECT_VERSION = 89;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2470,7 +2462,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 91;
|
CURRENT_PROJECT_VERSION = 89;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
|
@ -2499,7 +2491,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 91;
|
CURRENT_PROJECT_VERSION = 89;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
|
@ -2528,7 +2520,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 91;
|
CURRENT_PROJECT_VERSION = 89;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
|
@ -2683,7 +2675,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 = 91;
|
CURRENT_PROJECT_VERSION = 89;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2714,7 +2706,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 = 91;
|
CURRENT_PROJECT_VERSION = 89;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2820,7 +2812,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 = 91;
|
CURRENT_PROJECT_VERSION = 89;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2846,7 +2838,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 = 91;
|
CURRENT_PROJECT_VERSION = 89;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "1430"
|
|
||||||
wasCreatedForAppExtension = "YES"
|
|
||||||
version = "1.7">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "YES"
|
|
||||||
buildImplicitDependencies = "YES">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D6A4531229EF64BA00032932"
|
|
||||||
BuildableName = "ShareExtension.appex"
|
|
||||||
BlueprintName = "ShareExtension"
|
|
||||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D6D4DDCB212518A000E1C4BB"
|
|
||||||
BuildableName = "Tusker.app"
|
|
||||||
BlueprintName = "Tusker"
|
|
||||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
|
||||||
</BuildAction>
|
|
||||||
<TestAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
shouldAutocreateTestPlan = "YES">
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
launchStyle = "1"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
allowLocationSimulation = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D6A4531229EF64BA00032932"
|
|
||||||
BuildableName = "ShareExtension.appex"
|
|
||||||
BlueprintName = "ShareExtension"
|
|
||||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
<MacroExpansion>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D6D4DDCB212518A000E1C4BB"
|
|
||||||
BuildableName = "Tusker.app"
|
|
||||||
BlueprintName = "Tusker"
|
|
||||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</MacroExpansion>
|
|
||||||
<AdditionalOptions>
|
|
||||||
<AdditionalOption
|
|
||||||
key = "MallocStackLogging"
|
|
||||||
value = ""
|
|
||||||
isEnabled = "YES">
|
|
||||||
</AdditionalOption>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
askForAppToLaunch = "Yes"
|
|
||||||
launchAutomaticallySubstyle = "2">
|
|
||||||
<RemoteRunnable
|
|
||||||
runnableDebuggingMode = "1"
|
|
||||||
BundleIdentifier = "com.apple.mobileslideshow"
|
|
||||||
RemotePath = "/Applications/MobileSlideShow.app">
|
|
||||||
</RemoteRunnable>
|
|
||||||
</ProfileAction>
|
|
||||||
<AnalyzeAction
|
|
||||||
buildConfiguration = "Debug">
|
|
||||||
</AnalyzeAction>
|
|
||||||
<ArchiveAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
revealArchiveInOrganizer = "YES">
|
|
||||||
</ArchiveAction>
|
|
||||||
</Scheme>
|
|
|
@ -88,10 +88,6 @@
|
||||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
<CommandLineArgument
|
|
||||||
argument = "-com.apple.CoreData.CloudKitDebug 0"
|
|
||||||
isEnabled = "YES">
|
|
||||||
</CommandLineArgument>
|
|
||||||
<CommandLineArgument
|
<CommandLineArgument
|
||||||
argument = "-UIFocusLoggingEnabled YES"
|
argument = "-UIFocusLoggingEnabled YES"
|
||||||
isEnabled = "NO">
|
isEnabled = "NO">
|
||||||
|
|
|
@ -37,20 +37,16 @@ class ImageCache {
|
||||||
completion?(entry.data, entry.image)
|
completion?(entry.data, entry.image)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return getFromSource(url, completion: completion)
|
return Task.detached(priority: .userInitiated) {
|
||||||
}
|
let result = await self.fetch(url: url)
|
||||||
}
|
switch result {
|
||||||
|
case .data(let data):
|
||||||
func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
completion?(data, nil)
|
||||||
return Task.detached(priority: .userInitiated) {
|
case .dataAndImage(let data, let image):
|
||||||
let result = await self.fetch(url: url)
|
completion?(data, image)
|
||||||
switch result {
|
case .none:
|
||||||
case .data(let data):
|
completion?(nil, nil)
|
||||||
completion?(data, nil)
|
}
|
||||||
case .dataAndImage(let data, let image):
|
|
||||||
completion?(data, image)
|
|
||||||
case .none:
|
|
||||||
completion?(nil, nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import UIKit
|
||||||
|
|
||||||
extension NSTextAttachment {
|
extension NSTextAttachment {
|
||||||
// Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
|
// Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
|
||||||
@available(iOS, deprecated: 15.0)
|
|
||||||
convenience init(emojiImage image: UIImage, in font: UIFont, with textColor: UIColor = .label) {
|
convenience init(emojiImage image: UIImage, in font: UIFont, with textColor: UIColor = .label) {
|
||||||
let adjustedCapHeight = font.capHeight - 1
|
let adjustedCapHeight = font.capHeight - 1
|
||||||
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
|
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
|
||||||
|
|
|
@ -19,7 +19,7 @@ extension StatusEdit: CollapseStateResolving {}
|
||||||
extension CollapseState {
|
extension CollapseState {
|
||||||
|
|
||||||
func resolveFor(status: CollapseStateResolving, height: () -> CGFloat, textLength: Int? = nil) -> Bool {
|
func resolveFor(status: CollapseStateResolving, height: () -> CGFloat, textLength: Int? = nil) -> Bool {
|
||||||
let newHash = hashStatusProperties(status: status)
|
lazy var newHash = hashStatusProperties(status: status)
|
||||||
guard unknown || statusPropertiesHash != newHash else {
|
guard unknown || statusPropertiesHash != newHash else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ protocol Configurable {
|
||||||
func configure(_ closure: (T) -> Void) -> T
|
func configure(_ closure: (T) -> Void) -> T
|
||||||
}
|
}
|
||||||
extension Configurable where Self: UIView {
|
extension Configurable where Self: UIView {
|
||||||
@inline(__always)
|
|
||||||
func configure(_ closure: (Self) -> Void) -> Self {
|
func configure(_ closure: (Self) -> Void) -> Self {
|
||||||
closure(self)
|
closure(self)
|
||||||
return self
|
return self
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct ImageGrayscalifier {
|
struct ImageGrayscalifier {
|
||||||
static let queue = DispatchQueue(label: "ImageGrayscalifier", qos: .userInitiated)
|
static let queue = DispatchQueue(label: "ImageGrayscalifier", qos: .default)
|
||||||
|
|
||||||
private static let context = CIContext()
|
private static let context = CIContext()
|
||||||
private static let cache = NSCache<NSURL, UIImage>()
|
private static let cache = NSCache<NSURL, UIImage>()
|
||||||
|
@ -24,17 +24,6 @@ struct ImageGrayscalifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func convert(url: URL?, image: UIImage) -> UIImage? {
|
|
||||||
if let url,
|
|
||||||
let cached = cache.object(forKey: url as NSURL) {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
guard let cgImage = image.cgImage else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return doConvert(CIImage(cgImage: cgImage), url: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func convert(url: URL?, data: Data) -> UIImage? {
|
static func convert(url: URL?, data: Data) -> UIImage? {
|
||||||
if let url = url,
|
if let url = url,
|
||||||
let cached = cache.object(forKey: url as NSURL) {
|
let cached = cache.object(forKey: url as NSURL) {
|
||||||
|
|
|
@ -41,7 +41,7 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||||
fetchAvatar: { @MainActor in await ImageCache.avatars.get($0).1 },
|
fetchAvatar: { @MainActor in await ImageCache.avatars.get($0).1 },
|
||||||
fetchAttachment: { @MainActor in await ImageCache.attachments.get($0).1 },
|
fetchAttachment: { @MainActor in await ImageCache.attachments.get($0).1 },
|
||||||
fetchStatus: { mastodonController.persistentContainer.status(for: $0) },
|
fetchStatus: { mastodonController.persistentContainer.status(for: $0) },
|
||||||
displayNameLabel: { AnyView(AccountDisplayNameView(account: $0, textStyle: $1, emojiSize: $2)) },
|
displayNameLabel: { AnyView(AccountDisplayNameLabel(account: $0, textStyle: $1, emojiSize: $2)) },
|
||||||
replyContentView: { AnyView(ComposeReplyContentView(status: $0, mastodonController: mastodonController, heightChanged: $1)) },
|
replyContentView: { AnyView(ComposeReplyContentView(status: $0, mastodonController: mastodonController, heightChanged: $1)) },
|
||||||
emojiImageView: { AnyView(CustomEmojiImageView(emoji: $0)) }
|
emojiImageView: { AnyView(CustomEmojiImageView(emoji: $0)) }
|
||||||
)
|
)
|
||||||
|
|
|
@ -147,7 +147,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
Item.status(id: node.status.id, node: node, state: .unknown, prevLink: index > 0, nextLink: true)
|
Item.status(id: node.status.id, node: node, state: .unknown, prevLink: index > 0, nextLink: true)
|
||||||
}
|
}
|
||||||
snapshot.appendItems(parentItems, toSection: .ancestors)
|
snapshot.appendItems(parentItems, toSection: .ancestors)
|
||||||
snapshot.reconfigureItems([mainStatusItem])
|
snapshot.reloadItems([mainStatusItem])
|
||||||
|
|
||||||
// convert sub-threads into items for section and add to snapshot
|
// convert sub-threads into items for section and add to snapshot
|
||||||
self.addFlattenedChildThreadsToSnapshot(tree.descendants, mainStatus: mainStatus, snapshot: &snapshot)
|
self.addFlattenedChildThreadsToSnapshot(tree.descendants, mainStatus: mainStatus, snapshot: &snapshot)
|
||||||
|
|
|
@ -15,7 +15,7 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
||||||
@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!
|
||||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
@IBOutlet weak var noteTextView: StatusContentTextView!
|
@IBOutlet weak var noteTextView: StatusContentTextView!
|
||||||
|
|
||||||
var account: Account?
|
var account: Account?
|
||||||
|
|
|
@ -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="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" 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>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||||
<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"/>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/>
|
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/>
|
||||||
</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="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
<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"/>
|
<rect key="frame" x="76" y="72" width="316" height="24"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
|
|
|
@ -21,7 +21,7 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
|
||||||
@IBOutlet weak var headerImageView: CachedImageView!
|
@IBOutlet weak var headerImageView: CachedImageView!
|
||||||
@IBOutlet weak var avatarContainerView: UIView!
|
@IBOutlet weak var avatarContainerView: UIView!
|
||||||
@IBOutlet weak var avatarImageView: CachedImageView!
|
@IBOutlet weak var avatarImageView: CachedImageView!
|
||||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
@IBOutlet weak var noteTextView: StatusContentTextView!
|
@IBOutlet weak var noteTextView: StatusContentTextView!
|
||||||
@IBOutlet weak var suggestionSourceButton: UIButton!
|
@IBOutlet weak var suggestionSourceButton: UIButton!
|
||||||
|
|
|
@ -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="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="collection view cell content view" minToolsVersion="11.0"/>
|
<capability name="collection view cell content view" 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"/>
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
<constraint firstAttribute="width" constant="90" id="wav-YT-e4Y"/>
|
<constraint firstAttribute="width" constant="90" id="wav-YT-e4Y"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XCk-sZ-ujT" customClass="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XCk-sZ-ujT" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="106" y="100" width="333" height="29"/>
|
<rect key="frame" x="106" y="100" width="333" 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"/>
|
||||||
|
|
|
@ -65,7 +65,7 @@ struct MuteAccountView: View {
|
||||||
)
|
)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
AccountDisplayNameView(account: account, textStyle: .headline, emojiSize: 17)
|
AccountDisplayNameLabel(account: account, textStyle: .headline, emojiSize: 17)
|
||||||
Text("@\(account.acct)")
|
Text("@\(account.acct)")
|
||||||
.fontWeight(.light)
|
.fontWeight(.light)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
|
@ -55,7 +55,7 @@ struct ReportView: View {
|
||||||
)
|
)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
AccountDisplayNameView(account: account, textStyle: .headline, emojiSize: 17)
|
AccountDisplayNameLabel(account: account, textStyle: .headline, emojiSize: 17)
|
||||||
Text("@\(account.acct)")
|
Text("@\(account.acct)")
|
||||||
.fontWeight(.light)
|
.fontWeight(.light)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
|
@ -117,8 +117,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = state.resolveFor(status: edit, height: {
|
_ = state.resolveFor(status: edit, height: {
|
||||||
let width = self.bounds.width - 2*16
|
layoutIfNeeded()
|
||||||
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
|
return contentContainer.visibleSubviewHeight
|
||||||
})
|
})
|
||||||
collapseButton.isHidden = !state.collapsible!
|
collapseButton.isHidden = !state.collapsible!
|
||||||
contentContainer.setCollapsed(state.collapsed!)
|
contentContainer.setCollapsed(state.collapsed!)
|
||||||
|
|
|
@ -9,9 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class StatusEditPollView: UIStackView, StatusContentPollView {
|
class StatusEditPollView: UIStackView {
|
||||||
|
|
||||||
private var titleLabels: [EmojiLabel] = []
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
@ -27,7 +25,6 @@ class StatusEditPollView: UIStackView, StatusContentPollView {
|
||||||
|
|
||||||
func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) {
|
func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) {
|
||||||
arrangedSubviews.forEach { $0.removeFromSuperview() }
|
arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
titleLabels = []
|
|
||||||
|
|
||||||
for option in poll?.options ?? [] {
|
for option in poll?.options ?? [] {
|
||||||
// the edit poll doesn't actually include the multiple value
|
// the edit poll doesn't actually include the multiple value
|
||||||
|
@ -36,7 +33,6 @@ class StatusEditPollView: UIStackView, StatusContentPollView {
|
||||||
let label = EmojiLabel()
|
let label = EmojiLabel()
|
||||||
label.text = option.title
|
label.text = option.title
|
||||||
label.setEmojis(emojis, identifier: Optional<String>.none)
|
label.setEmojis(emojis, identifier: Optional<String>.none)
|
||||||
titleLabels.append(label)
|
|
||||||
let stack = UIStackView(arrangedSubviews: [
|
let stack = UIStackView(arrangedSubviews: [
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
|
@ -48,14 +44,4 @@ class StatusEditPollView: UIStackView, StatusContentPollView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
var height: CGFloat = 0
|
|
||||||
height += CGFloat(arrangedSubviews.count - 1) * 4
|
|
||||||
let labelWidth = effectiveWidth /* checkbox size: */ - 20 /* spacing: */ - 8
|
|
||||||
for titleLabel in titleLabels {
|
|
||||||
height += titleLabel.sizeThatFits(CGSize(width: labelWidth, height: UIView.layoutFittingCompressedSize.height)).height
|
|
||||||
}
|
|
||||||
return height
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
var mastodonController: MastodonController! { delegate?.apiController }
|
var mastodonController: MastodonController! { delegate?.apiController }
|
||||||
|
|
||||||
@IBOutlet weak var avatarImageView: UIImageView!
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
@IBOutlet weak var noteLabel: EmojiLabel!
|
@IBOutlet weak var noteLabel: EmojiLabel!
|
||||||
|
|
||||||
|
|
|
@ -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="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" 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>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||||
<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"/>
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Iif-9m-vM5">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Iif-9m-vM5">
|
||||||
<rect key="frame" x="74" y="11" width="230" height="78"/>
|
<rect key="frame" x="74" y="11" width="230" height="78"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fhc-bZ-lkB" customClass="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fhc-bZ-lkB" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="230" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="230" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Pachyderm
|
||||||
class LargeAccountDetailView: UIView {
|
class LargeAccountDetailView: UIView {
|
||||||
|
|
||||||
var avatarImageView = UIImageView()
|
var avatarImageView = UIImageView()
|
||||||
var displayNameLabel = AccountDisplayNameLabel()
|
var displayNameLabel = EmojiLabel()
|
||||||
var usernameLabel = UILabel()
|
var usernameLabel = UILabel()
|
||||||
|
|
||||||
var avatarRequest: ImageCache.Request?
|
var avatarRequest: ImageCache.Request?
|
||||||
|
|
|
@ -2,31 +2,106 @@
|
||||||
// AccountDisplayNameLabel.swift
|
// AccountDisplayNameLabel.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 5/14/23.
|
// Created by Shadowfacts on 9/7/20.
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
class AccountDisplayNameLabel: EmojiLabel {
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
|
||||||
private var accountID: String?
|
struct AccountDisplayNameLabel: View {
|
||||||
// store the display name, so that if it changes the label updates w/o changing the id
|
let account: any AccountProtocol
|
||||||
private var accountDisplayName: String?
|
let textStyle: Font.TextStyle
|
||||||
|
@ScaledMetric var emojiSize: CGFloat
|
||||||
|
@State var text: Text
|
||||||
|
@State var emojiRequests = [ImageCache.Request]()
|
||||||
|
|
||||||
func updateForAccountDisplayName(account: some AccountProtocol) {
|
init(account: any AccountProtocol, textStyle: Font.TextStyle, emojiSize: CGFloat) {
|
||||||
guard accountID != account.id || accountDisplayName != account.displayName || Preferences.shared.hideCustomEmojiInUsernames == hasEmojis else {
|
self.account = account
|
||||||
return
|
self.textStyle = textStyle
|
||||||
}
|
self._emojiSize = ScaledMetric(wrappedValue: emojiSize, relativeTo: textStyle)
|
||||||
accountID = account.id
|
let name = account.displayName.isEmpty ? account.username : account.displayName
|
||||||
accountDisplayName = account.displayName
|
self._text = State(initialValue: Text(verbatim: name))
|
||||||
self.text = accountDisplayName
|
|
||||||
if Preferences.shared.hideCustomEmojiInUsernames {
|
|
||||||
self.removeEmojis()
|
|
||||||
} else {
|
|
||||||
self.setEmojis(account.emojis, identifier: account.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
text
|
||||||
|
.font(.system(textStyle).weight(.semibold))
|
||||||
|
.onAppear(perform: self.loadEmojis)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadEmojis() {
|
||||||
|
let fullRange = NSRange(account.displayName.startIndex..., in: account.displayName)
|
||||||
|
let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange)
|
||||||
|
guard !matches.isEmpty else { return }
|
||||||
|
|
||||||
|
let emojiImages = MultiThreadDictionary<String, Image>()
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
|
||||||
|
for emoji in account.emojis {
|
||||||
|
guard matches.contains(where: { (match) in
|
||||||
|
let matchShortcode = (account.displayName as NSString).substring(with: match.range(at: 1))
|
||||||
|
return emoji.shortcode == matchShortcode
|
||||||
|
}) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
group.enter()
|
||||||
|
let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
|
||||||
|
defer { group.leave() }
|
||||||
|
guard let image = image else { return }
|
||||||
|
|
||||||
|
let size = CGSize(width: emojiSize, height: emojiSize)
|
||||||
|
let renderer = UIGraphicsImageRenderer(size: size)
|
||||||
|
let resized = renderer.image { (ctx) in
|
||||||
|
image.draw(in: CGRect(origin: .zero, size: size))
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiImages[emoji.shortcode] = Image(uiImage: resized)
|
||||||
|
}
|
||||||
|
if let request = request {
|
||||||
|
emojiRequests.append(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
var text: Text?
|
||||||
|
|
||||||
|
var endIndex = account.displayName.utf16.count
|
||||||
|
|
||||||
|
// iterate backwards as to not alter the indices of earlier matches
|
||||||
|
for match in matches.reversed() {
|
||||||
|
let shortcode = (account.displayName as NSString).substring(with: match.range(at: 1))
|
||||||
|
guard let image = emojiImages[shortcode] else { continue }
|
||||||
|
|
||||||
|
let afterCurrentMatch = (account.displayName as NSString).substring(with: NSRange(location: match.range.upperBound, length: endIndex - match.range.upperBound))
|
||||||
|
|
||||||
|
if let subsequent = text {
|
||||||
|
text = Text(image) + Text(verbatim: afterCurrentMatch) + subsequent
|
||||||
|
} else {
|
||||||
|
text = Text(image) + Text(verbatim: afterCurrentMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
endIndex = match.range.lowerBound
|
||||||
|
}
|
||||||
|
|
||||||
|
let beforeLastMatch = (account.displayName as NSString).substring(to: endIndex)
|
||||||
|
|
||||||
|
if let text = text {
|
||||||
|
self.text = Text(verbatim: beforeLastMatch) + text
|
||||||
|
} else {
|
||||||
|
self.text = Text(verbatim: beforeLastMatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//struct AccountDisplayNameLabel_Previews: PreviewProvider {
|
||||||
|
// static var previews: some View {
|
||||||
|
// AccountDisplayNameLabel()
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
//
|
|
||||||
// AccountDisplayNameView.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 9/7/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Pachyderm
|
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
|
||||||
|
|
||||||
struct AccountDisplayNameView: View {
|
|
||||||
let account: any AccountProtocol
|
|
||||||
let textStyle: Font.TextStyle
|
|
||||||
@ScaledMetric var emojiSize: CGFloat
|
|
||||||
@State var text: Text
|
|
||||||
@State var emojiRequests = [ImageCache.Request]()
|
|
||||||
|
|
||||||
init(account: any AccountProtocol, textStyle: Font.TextStyle, emojiSize: CGFloat) {
|
|
||||||
self.account = account
|
|
||||||
self.textStyle = textStyle
|
|
||||||
self._emojiSize = ScaledMetric(wrappedValue: emojiSize, relativeTo: textStyle)
|
|
||||||
let name = account.displayName.isEmpty ? account.username : account.displayName
|
|
||||||
self._text = State(initialValue: Text(verbatim: name))
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
text
|
|
||||||
.font(.system(textStyle).weight(.semibold))
|
|
||||||
.onAppear(perform: self.loadEmojis)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadEmojis() {
|
|
||||||
let fullRange = NSRange(account.displayName.startIndex..., in: account.displayName)
|
|
||||||
let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange)
|
|
||||||
guard !matches.isEmpty else { return }
|
|
||||||
|
|
||||||
let emojiImages = MultiThreadDictionary<String, Image>()
|
|
||||||
|
|
||||||
let group = DispatchGroup()
|
|
||||||
|
|
||||||
for emoji in account.emojis {
|
|
||||||
guard matches.contains(where: { (match) in
|
|
||||||
let matchShortcode = (account.displayName as NSString).substring(with: match.range(at: 1))
|
|
||||||
return emoji.shortcode == matchShortcode
|
|
||||||
}) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
group.enter()
|
|
||||||
let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
|
|
||||||
defer { group.leave() }
|
|
||||||
guard let image = image else { return }
|
|
||||||
|
|
||||||
let size = CGSize(width: emojiSize, height: emojiSize)
|
|
||||||
let renderer = UIGraphicsImageRenderer(size: size)
|
|
||||||
let resized = renderer.image { (ctx) in
|
|
||||||
image.draw(in: CGRect(origin: .zero, size: size))
|
|
||||||
}
|
|
||||||
|
|
||||||
emojiImages[emoji.shortcode] = Image(uiImage: resized)
|
|
||||||
}
|
|
||||||
if let request = request {
|
|
||||||
emojiRequests.append(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group.notify(queue: .main) {
|
|
||||||
var text: Text?
|
|
||||||
|
|
||||||
var endIndex = account.displayName.utf16.count
|
|
||||||
|
|
||||||
// iterate backwards as to not alter the indices of earlier matches
|
|
||||||
for match in matches.reversed() {
|
|
||||||
let shortcode = (account.displayName as NSString).substring(with: match.range(at: 1))
|
|
||||||
guard let image = emojiImages[shortcode] else { continue }
|
|
||||||
|
|
||||||
let afterCurrentMatch = (account.displayName as NSString).substring(with: NSRange(location: match.range.upperBound, length: endIndex - match.range.upperBound))
|
|
||||||
|
|
||||||
if let subsequent = text {
|
|
||||||
text = Text(image) + Text(verbatim: afterCurrentMatch) + subsequent
|
|
||||||
} else {
|
|
||||||
text = Text(image) + Text(verbatim: afterCurrentMatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
endIndex = match.range.lowerBound
|
|
||||||
}
|
|
||||||
|
|
||||||
let beforeLastMatch = (account.displayName as NSString).substring(to: endIndex)
|
|
||||||
|
|
||||||
if let text = text {
|
|
||||||
self.text = Text(verbatim: beforeLastMatch) + text
|
|
||||||
} else {
|
|
||||||
self.text = Text(verbatim: beforeLastMatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//struct AccountDisplayNameLabel_Previews: PreviewProvider {
|
|
||||||
// static var previews: some View {
|
|
||||||
// AccountDisplayNameView()
|
|
||||||
// }
|
|
||||||
//}
|
|
|
@ -75,7 +75,9 @@ class AttachmentView: GIFImageView {
|
||||||
gifPlaybackModeChanged()
|
gifPlaybackModeChanged()
|
||||||
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
self.displayImage()
|
ImageGrayscalifier.queue.async {
|
||||||
|
self.displayImage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if getBadges().isEmpty != Preferences.shared.showAttachmentBadges {
|
if getBadges().isEmpty != Preferences.shared.showAttachmentBadges {
|
||||||
|
@ -187,57 +189,51 @@ class AttachmentView: GIFImageView {
|
||||||
|
|
||||||
func loadImage() {
|
func loadImage() {
|
||||||
let attachmentURL = attachment.url
|
let attachmentURL = attachment.url
|
||||||
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, image) in
|
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
|
||||||
guard let self = self,
|
guard let self = self, let data = data else { return }
|
||||||
self.attachment.url == attachmentURL else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.attachmentRequest = nil
|
self.attachmentRequest = nil
|
||||||
|
}
|
||||||
if attachmentURL.pathExtension == "gif",
|
if self.attachment.url.pathExtension == "gif" {
|
||||||
let data {
|
self.source = .gifData(attachmentURL, data)
|
||||||
self.source = .gifData(attachmentURL, data, image)
|
let controller = GIFController(gifData: data)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
controller.attach(to: self)
|
||||||
if self.autoplayGifs {
|
if self.autoplayGifs {
|
||||||
let controller = GIFController(gifData: data)
|
|
||||||
controller.attach(to: self)
|
|
||||||
controller.startAnimating()
|
controller.startAnimating()
|
||||||
} else {
|
|
||||||
self.displayImage()
|
|
||||||
}
|
}
|
||||||
} else if let image {
|
}
|
||||||
self.source = .image(attachmentURL, image)
|
|
||||||
|
if !self.autoplayGifs {
|
||||||
self.displayImage()
|
self.displayImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self.source = .imageData(attachmentURL, data)
|
||||||
|
self.displayImage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadVideo() {
|
func loadVideo() {
|
||||||
if let previewURL = self.attachment.previewURL {
|
if let previewURL = self.attachment.previewURL {
|
||||||
attachmentRequest = ImageCache.attachments.get(previewURL, completion: { [weak self] (_, image) in
|
attachmentRequest = ImageCache.attachments.get(previewURL, completion: { [weak self] (data, _ )in
|
||||||
guard let self, let image else { return }
|
guard let self = self, let data = data else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.attachmentRequest = nil
|
self.attachmentRequest = nil
|
||||||
self.source = .image(previewURL, image)
|
self.source = .imageData(previewURL, data)
|
||||||
self.displayImage()
|
self.displayImage()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let attachmentURL = self.attachment.url
|
let attachmentURL = self.attachment.url
|
||||||
AttachmentView.queue.async { [weak self] in
|
AttachmentView.queue.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
|
||||||
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
||||||
UIImage(cgImage: image).prepareForDisplay { [weak self] image in
|
self.source = .cgImage(attachmentURL, image)
|
||||||
DispatchQueue.main.async { [weak self] in
|
self.displayImage()
|
||||||
guard let self, let image else { return }
|
|
||||||
self.source = .image(attachmentURL, image)
|
|
||||||
self.displayImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,10 +276,8 @@ class AttachmentView: GIFImageView {
|
||||||
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 }
|
||||||
DispatchQueue.main.async {
|
self.source = .cgImage(attachmentURL, image)
|
||||||
self.source = .cgImage(attachmentURL, image)
|
self.displayImage()
|
||||||
self.displayImage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
|
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
|
||||||
|
@ -301,51 +295,33 @@ class AttachmentView: GIFImageView {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func displayImage() {
|
private func displayImage() {
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
|
let image: UIImage?
|
||||||
|
|
||||||
switch source {
|
switch source {
|
||||||
case nil:
|
case nil:
|
||||||
self.image = nil
|
image = nil
|
||||||
|
|
||||||
case let .image(url, sourceImage):
|
case let .imageData(url, data), let .gifData(url, data):
|
||||||
if isGrayscale {
|
if isGrayscale {
|
||||||
ImageGrayscalifier.queue.async { [weak self] in
|
image = ImageGrayscalifier.convert(url: url, data: data)
|
||||||
let grayscale = ImageGrayscalifier.convert(url: url, image: sourceImage)
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.image = grayscale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
self.image = sourceImage
|
image = UIImage(data: data)
|
||||||
}
|
|
||||||
|
|
||||||
case let .gifData(url, _, sourceImage):
|
|
||||||
if isGrayscale,
|
|
||||||
let sourceImage {
|
|
||||||
ImageGrayscalifier.queue.async { [weak self] in
|
|
||||||
let grayscale = ImageGrayscalifier.convert(url: url, image: sourceImage)
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.image = grayscale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.image = sourceImage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .cgImage(url, cgImage):
|
case let .cgImage(url, cgImage):
|
||||||
if isGrayscale {
|
if isGrayscale {
|
||||||
ImageGrayscalifier.queue.async { [weak self] in
|
image = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
|
||||||
let grayscale = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.image = grayscale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
image = UIImage(cgImage: cgImage)
|
image = UIImage(cgImage: cgImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.image = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createBadgesView(_ badges: Badges) {
|
private func createBadgesView(_ badges: Badges) {
|
||||||
|
@ -433,8 +409,8 @@ class AttachmentView: GIFImageView {
|
||||||
|
|
||||||
fileprivate extension AttachmentView {
|
fileprivate extension AttachmentView {
|
||||||
enum Source {
|
enum Source {
|
||||||
case image(URL, UIImage)
|
case imageData(URL, Data)
|
||||||
case gifData(URL, Data, UIImage?)
|
case gifData(URL, Data)
|
||||||
case cgImage(URL, CGImage)
|
case cgImage(URL, CGImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,15 +22,6 @@ class AttachmentsContainerView: UIView {
|
||||||
let attachmentStacks: NSHashTable<UIStackView> = .weakObjects()
|
let attachmentStacks: NSHashTable<UIStackView> = .weakObjects()
|
||||||
var moreView: UIView?
|
var moreView: UIView?
|
||||||
private var aspectRatioConstraint: NSLayoutConstraint?
|
private var aspectRatioConstraint: NSLayoutConstraint?
|
||||||
private(set) var aspectRatio: CGFloat = 16/9 {
|
|
||||||
didSet {
|
|
||||||
if aspectRatio != aspectRatioConstraint?.multiplier {
|
|
||||||
aspectRatioConstraint?.isActive = false
|
|
||||||
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: aspectRatio)
|
|
||||||
aspectRatioConstraint!.isActive = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var blurView: UIVisualEffectView?
|
var blurView: UIVisualEffectView?
|
||||||
var hideButtonView: UIVisualEffectView?
|
var hideButtonView: UIVisualEffectView?
|
||||||
|
@ -102,8 +93,7 @@ class AttachmentsContainerView: UIView {
|
||||||
fillView(attachmentView)
|
fillView(attachmentView)
|
||||||
sendSubviewToBack(attachmentView)
|
sendSubviewToBack(attachmentView)
|
||||||
accessibilityElements.append(attachmentView)
|
accessibilityElements.append(attachmentView)
|
||||||
if Preferences.shared.showUncroppedMediaInline,
|
if let attachmentAspectRatio = attachmentView.attachmentAspectRatio {
|
||||||
let attachmentAspectRatio = attachmentView.attachmentAspectRatio {
|
|
||||||
aspectRatio = attachmentAspectRatio
|
aspectRatio = attachmentAspectRatio
|
||||||
}
|
}
|
||||||
case 2:
|
case 2:
|
||||||
|
@ -276,7 +266,18 @@ class AttachmentsContainerView: UIView {
|
||||||
accessibilityElements.append(moreView)
|
accessibilityElements.append(moreView)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.aspectRatio = aspectRatio
|
if Preferences.shared.showUncroppedMediaInline {
|
||||||
|
if aspectRatioConstraint?.multiplier != aspectRatio {
|
||||||
|
aspectRatioConstraint?.isActive = false
|
||||||
|
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: aspectRatio)
|
||||||
|
aspectRatioConstraint!.isActive = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if aspectRatioConstraint == nil {
|
||||||
|
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: 16/9)
|
||||||
|
aspectRatioConstraint!.isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.isHidden = true
|
self.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,16 +40,6 @@ extension BaseEmojiLabel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
|
|
||||||
let adjustedCapHeight = emojiFont.capHeight - 1
|
|
||||||
func emojiImageSize(_ image: UIImage) -> CGSize {
|
|
||||||
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
|
|
||||||
var scale: CGFloat = 1.4
|
|
||||||
scale *= UIScreen.main.scale
|
|
||||||
imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * scale, height: imageSizeMatchingFontSize.height * scale)
|
|
||||||
return imageSizeMatchingFontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
let emojiImages = MultiThreadDictionary<String, UIImage>()
|
let emojiImages = MultiThreadDictionary<String, UIImage>()
|
||||||
var foundEmojis = false
|
var foundEmojis = false
|
||||||
|
|
||||||
|
@ -67,35 +57,21 @@ extension BaseEmojiLabel {
|
||||||
foundEmojis = true
|
foundEmojis = true
|
||||||
|
|
||||||
if let image = ImageCache.emojis.get(URL(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
|
||||||
// we generate the thumbnail on the main thread, because it's usually fast enough
|
emojiImages[emoji.shortcode] = image
|
||||||
// and the delay caused by doing it asynchronously looks works.
|
|
||||||
// todo: consider caching these somewhere? the cache needs to take into account both the url and the font size, which may vary across labels, so can't just use ImageCache
|
|
||||||
if let thumbnail = image.preparingThumbnail(of: emojiImageSize(image)),
|
|
||||||
let cgImage = thumbnail.cgImage {
|
|
||||||
// the thumbnail API takes a pixel size and returns an image with scale 1, but we want the actual screen scale, so convert
|
|
||||||
// see FB12187798
|
|
||||||
emojiImages[emoji.shortcode] = UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// otherwise, perform the network request
|
// otherwise, perform the network request
|
||||||
|
|
||||||
group.enter()
|
group.enter()
|
||||||
let request = ImageCache.emojis.getFromSource(URL(emoji.url)!) { (_, image) in
|
// todo: ImageCache.emojis.get here will re-check the memory and disk caches, there should be another method to force-refetch
|
||||||
guard let image else {
|
let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
|
||||||
group.leave()
|
guard let image = image,
|
||||||
return
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: image) else {
|
||||||
}
|
group.leave()
|
||||||
image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in
|
return
|
||||||
guard let thumbnail = thumbnail?.cgImage,
|
|
||||||
case let rescaled = UIImage(cgImage: thumbnail, scale: UIScreen.main.scale, orientation: .up),
|
|
||||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: rescaled) else {
|
|
||||||
group.leave()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
emojiImages[emoji.shortcode] = transformedImage
|
|
||||||
group.leave()
|
|
||||||
}
|
}
|
||||||
|
emojiImages[emoji.shortcode] = transformedImage
|
||||||
|
group.leave()
|
||||||
}
|
}
|
||||||
if let request = request {
|
if let request = request {
|
||||||
emojiRequests.append(request)
|
emojiRequests.append(request)
|
||||||
|
@ -115,8 +91,10 @@ extension BaseEmojiLabel {
|
||||||
// even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878)
|
// even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878)
|
||||||
// so, just ignore the warnings
|
// so, just ignore the warnings
|
||||||
let emojiAttachments = emojiImages.withLock {
|
let emojiAttachments = emojiImages.withLock {
|
||||||
|
let emojiFont = self.emojiFont
|
||||||
|
let emojiTextColor = self.emojiTextColor
|
||||||
return $0.mapValues { image in
|
return $0.mapValues { image in
|
||||||
NSTextAttachment(image: image)
|
NSTextAttachment(emojiImage: image, in: emojiFont, with: emojiTextColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil
|
let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil
|
||||||
|
|
|
@ -18,7 +18,7 @@ class CachedImageView: UIImageView {
|
||||||
|
|
||||||
override var image: UIImage? {
|
override var image: UIImage? {
|
||||||
didSet {
|
didSet {
|
||||||
blurHashTask?.cancel()
|
fetchTask?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ class ConfirmReblogStatusPreviewView: UIView {
|
||||||
vStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
vStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
hStack.addArrangedSubview(vStack)
|
hStack.addArrangedSubview(vStack)
|
||||||
|
|
||||||
let displayNameLabel = AccountDisplayNameLabel()
|
let displayNameLabel = EmojiLabel()
|
||||||
displayNameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1).addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]]), size: 0)
|
displayNameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1).addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]]), size: 0)
|
||||||
displayNameLabel.adjustsFontSizeToFitWidth = true
|
displayNameLabel.adjustsFontSizeToFitWidth = true
|
||||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
|
|
@ -38,3 +38,24 @@ class EmojiLabel: UILabel, BaseEmojiLabel {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension EmojiLabel {
|
||||||
|
func updateForAccountDisplayName(account: Account) {
|
||||||
|
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||||
|
self.text = account.displayName
|
||||||
|
self.removeEmojis()
|
||||||
|
} else {
|
||||||
|
self.text = account.displayName
|
||||||
|
self.setEmojis(account.emojis, identifier: account.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func updateForAccountDisplayName(account: AccountMO) {
|
||||||
|
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||||
|
self.text = account.displayNameWithoutCustomEmoji
|
||||||
|
self.removeEmojis()
|
||||||
|
} else {
|
||||||
|
self.text = account.displayOrUserName
|
||||||
|
self.setEmojis(account.emojis, identifier: account.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,27 +11,24 @@ import Pachyderm
|
||||||
|
|
||||||
class PollOptionView: UIView {
|
class PollOptionView: UIView {
|
||||||
|
|
||||||
private static let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
|
private let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
|
||||||
|
|
||||||
private(set) var label: EmojiLabel!
|
let checkbox: PollOptionCheckboxView
|
||||||
private(set) var checkbox: PollOptionCheckboxView?
|
|
||||||
|
|
||||||
init(poll: Poll, option: Poll.Option, mastodonController: MastodonController) {
|
init(poll: Poll, option: Poll.Option, mastodonController: MastodonController) {
|
||||||
|
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
let minHeight: CGFloat = 35
|
let minHeight: CGFloat = 35
|
||||||
layer.cornerRadius = 0.1 * minHeight
|
layer.cornerRadius = 0.1 * minHeight
|
||||||
layer.cornerCurve = .continuous
|
layer.cornerCurve = .continuous
|
||||||
backgroundColor = PollOptionView.unselectedBackgroundColor
|
backgroundColor = unselectedBackgroundColor
|
||||||
|
|
||||||
let showCheckbox = poll.ownVotes?.isEmpty == false || !poll.effectiveExpired
|
checkbox.translatesAutoresizingMaskIntoConstraints = false
|
||||||
if showCheckbox {
|
addSubview(checkbox)
|
||||||
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
|
|
||||||
checkbox!.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(checkbox!)
|
|
||||||
}
|
|
||||||
|
|
||||||
label = EmojiLabel()
|
let label = EmojiLabel()
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
label.numberOfLines = 0
|
label.numberOfLines = 0
|
||||||
label.font = .preferredFont(forTextStyle: .callout)
|
label.font = .preferredFont(forTextStyle: .callout)
|
||||||
|
@ -91,8 +88,12 @@ class PollOptionView: UIView {
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
minHeightConstraint,
|
minHeightConstraint,
|
||||||
|
|
||||||
|
checkbox.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
checkbox.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||||
|
|
||||||
label.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
label.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
||||||
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
||||||
|
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8),
|
||||||
label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor, constant: -4),
|
label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor, constant: -4),
|
||||||
|
|
||||||
percentLabel.topAnchor.constraint(equalTo: topAnchor),
|
percentLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
@ -100,16 +101,6 @@ class PollOptionView: UIView {
|
||||||
percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||||
])
|
])
|
||||||
|
|
||||||
if let checkbox {
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
checkbox.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
||||||
checkbox.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
|
||||||
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8),
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8).isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
isAccessibilityElement = true
|
isAccessibilityElement = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class PollOptionsView: UIControl {
|
||||||
var mastodonController: MastodonController!
|
var mastodonController: MastodonController!
|
||||||
|
|
||||||
var checkedOptionIndices: [Int] {
|
var checkedOptionIndices: [Int] {
|
||||||
options.enumerated().filter { $0.element.checkbox?.isChecked == true }.map(\.offset)
|
options.enumerated().filter { $0.element.checkbox.isChecked }.map(\.offset)
|
||||||
}
|
}
|
||||||
var checkedOptionsChanged: (() -> Void)?
|
var checkedOptionsChanged: (() -> Void)?
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ class PollOptionsView: UIControl {
|
||||||
|
|
||||||
override var isEnabled: Bool {
|
override var isEnabled: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
options.forEach { $0.checkbox?.readOnly = !isEnabled }
|
options.forEach { $0.checkbox.readOnly = !isEnabled }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,11 +65,9 @@ class PollOptionsView: UIControl {
|
||||||
|
|
||||||
options = poll.options.enumerated().map { (index, opt) in
|
options = poll.options.enumerated().map { (index, opt) in
|
||||||
let optionView = PollOptionView(poll: poll, option: opt, mastodonController: mastodonController)
|
let optionView = PollOptionView(poll: poll, option: opt, mastodonController: mastodonController)
|
||||||
if let checkbox = optionView.checkbox {
|
optionView.checkbox.readOnly = !isEnabled
|
||||||
checkbox.readOnly = !isEnabled
|
optionView.checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
|
||||||
checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
|
optionView.checkbox.voted = poll.voted ?? false
|
||||||
checkbox.voted = poll.voted ?? false
|
|
||||||
}
|
|
||||||
stack.addArrangedSubview(optionView)
|
stack.addArrangedSubview(optionView)
|
||||||
return optionView
|
return optionView
|
||||||
}
|
}
|
||||||
|
@ -77,25 +75,15 @@ class PollOptionsView: UIControl {
|
||||||
accessibilityElements = options
|
accessibilityElements = options
|
||||||
}
|
}
|
||||||
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
var height: CGFloat = 0
|
|
||||||
height += CGFloat(options.count - 1) * stack.spacing
|
|
||||||
for option in options {
|
|
||||||
// this isn't the actual width, but it's close enough for the estimate
|
|
||||||
height += option.label.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height
|
|
||||||
}
|
|
||||||
return height
|
|
||||||
}
|
|
||||||
|
|
||||||
private func selectOption(_ option: PollOptionView) {
|
private func selectOption(_ option: PollOptionView) {
|
||||||
if poll.multiple {
|
if poll.multiple {
|
||||||
option.checkbox?.isChecked.toggle()
|
option.checkbox.isChecked.toggle()
|
||||||
} else {
|
} else {
|
||||||
for opt in options {
|
for opt in options {
|
||||||
if opt === option {
|
if opt === option {
|
||||||
opt.checkbox?.isChecked = true
|
opt.checkbox.isChecked = true
|
||||||
} else {
|
} else {
|
||||||
opt.checkbox?.isChecked = false
|
opt.checkbox.isChecked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class StatusPollView: UIView, StatusContentPollView {
|
class StatusPollView: UIView {
|
||||||
|
|
||||||
private static let formatter: DateComponentsFormatter = {
|
private static let formatter: DateComponentsFormatter = {
|
||||||
let f = DateComponentsFormatter()
|
let f = DateComponentsFormatter()
|
||||||
|
@ -140,11 +140,6 @@ class StatusPollView: UIView, StatusContentPollView {
|
||||||
voteButton.isEnabled = false
|
voteButton.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
guard let poll else { return 0 }
|
|
||||||
return optionsView.estimateHeight(effectiveWidth: effectiveWidth) + infoLabel.sizeThatFits(UIView.layoutFittingExpandedSize).height
|
|
||||||
}
|
|
||||||
|
|
||||||
private func checkedOptionsChanged() {
|
private func checkedOptionsChanged() {
|
||||||
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
|
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ class ProfileHeaderView: UIView {
|
||||||
@IBOutlet weak var avatarImageView: UIImageView!
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
@IBOutlet weak var moreButton: ProfileHeaderButton!
|
@IBOutlet weak var moreButton: ProfileHeaderButton!
|
||||||
@IBOutlet weak var followButton: ProfileHeaderButton!
|
@IBOutlet weak var followButton: ProfileHeaderButton!
|
||||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
@IBOutlet weak var lockImageView: UIImageView!
|
@IBOutlet weak var lockImageView: UIImageView!
|
||||||
@IBOutlet weak var vStack: UIStackView!
|
@IBOutlet weak var vStack: UIStackView!
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
<constraint firstItem="TkY-oK-if4" firstAttribute="centerX" secondItem="wT9-2J-uSY" secondAttribute="centerX" id="ozz-sa-gSc"/>
|
<constraint firstItem="TkY-oK-if4" firstAttribute="centerX" secondItem="wT9-2J-uSY" secondAttribute="centerX" id="ozz-sa-gSc"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vcl-Gl-kXl" customClass="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vcl-Gl-kXl" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="144" y="206" width="254" height="29"/>
|
<rect key="frame" x="144" y="206" width="254" 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"/>
|
||||||
|
|
|
@ -39,7 +39,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
||||||
}
|
}
|
||||||
|
|
||||||
let displayNameLabel = AccountDisplayNameLabel().configure {
|
let displayNameLabel = EmojiLabel().configure {
|
||||||
$0.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 24, weight: .semibold))
|
$0.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 24, weight: .semibold))
|
||||||
$0.adjustsFontForContentSizeCategory = true
|
$0.adjustsFontForContentSizeCategory = true
|
||||||
}
|
}
|
||||||
|
@ -191,13 +191,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var favoriteButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
private(set) lazy var favoriteButton = UIButton().configure {
|
||||||
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
||||||
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var reblogButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
private(set) lazy var reblogButton = UIButton().configure {
|
||||||
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
||||||
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||||
|
@ -348,6 +348,24 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
accountDetailAccessibilityElement.navigationDelegate = delegate
|
accountDetailAccessibilityElement.navigationDelegate = delegate
|
||||||
accountDetailAccessibilityElement.accountID = accountID
|
accountDetailAccessibilityElement.accountID = accountID
|
||||||
|
|
||||||
|
let metaButtonAttributes = AttributeContainer([
|
||||||
|
.font: ConversationMainStatusCollectionViewCell.metaFont
|
||||||
|
])
|
||||||
|
|
||||||
|
let favoritesFormat = NSLocalizedString("favorites count", comment: "conv main status favorites button label")
|
||||||
|
var favoritesConfig = UIButton.Configuration.plain()
|
||||||
|
favoritesConfig.baseForegroundColor = .secondaryLabel
|
||||||
|
favoritesConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(favoritesFormat, status.favouritesCount), attributes: metaButtonAttributes)
|
||||||
|
favoritesConfig.contentInsets = .zero
|
||||||
|
favoritesCountButton.configuration = favoritesConfig
|
||||||
|
|
||||||
|
let reblogsFormat = NSLocalizedString("reblogs count", comment: "conv main status reblogs button label")
|
||||||
|
var reblogsConfig = UIButton.Configuration.plain()
|
||||||
|
reblogsConfig.baseForegroundColor = .secondaryLabel
|
||||||
|
reblogsConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(reblogsFormat, status.reblogsCount), attributes: metaButtonAttributes)
|
||||||
|
reblogsConfig.contentInsets = .zero
|
||||||
|
reblogsCountButton.configuration = reblogsConfig
|
||||||
|
|
||||||
var timestampAndClientText = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: status.createdAt)
|
var timestampAndClientText = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: status.createdAt)
|
||||||
if let application = status.applicationName {
|
if let application = status.applicationName {
|
||||||
timestampAndClientText += " • \(application)"
|
timestampAndClientText += " • \(application)"
|
||||||
|
@ -358,9 +376,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
editTimestampButton.isHidden = false
|
editTimestampButton.isHidden = false
|
||||||
var config = UIButton.Configuration.plain()
|
var config = UIButton.Configuration.plain()
|
||||||
config.baseForegroundColor = .secondaryLabel
|
config.baseForegroundColor = .secondaryLabel
|
||||||
config.attributedTitle = AttributedString("Edited on \(ConversationMainStatusCollectionViewCell.dateFormatter.string(from: editedAt))", attributes: AttributeContainer([
|
config.attributedTitle = AttributedString("Edited on \(ConversationMainStatusCollectionViewCell.dateFormatter.string(from: editedAt))", attributes: metaButtonAttributes)
|
||||||
.font: ConversationMainStatusCollectionViewCell.metaFont
|
|
||||||
]))
|
|
||||||
config.contentInsets = .zero
|
config.contentInsets = .zero
|
||||||
editTimestampButton.configuration = config
|
editTimestampButton.configuration = config
|
||||||
} else {
|
} else {
|
||||||
|
@ -376,33 +392,6 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
baseCreateObservers()
|
baseCreateObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateStatusState(status: StatusMO) {
|
|
||||||
baseUpdateStatusState(status: status)
|
|
||||||
|
|
||||||
let attributes = AttributeContainer([
|
|
||||||
.font: ConversationMainStatusCollectionViewCell.metaFont
|
|
||||||
])
|
|
||||||
|
|
||||||
let favoritesFormat = NSLocalizedString("favorites count", comment: "conv main status favorites button label")
|
|
||||||
var favoritesConfig = UIButton.Configuration.plain()
|
|
||||||
favoritesConfig.baseForegroundColor = .secondaryLabel
|
|
||||||
favoritesConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(favoritesFormat, status.favouritesCount), attributes: attributes)
|
|
||||||
favoritesConfig.contentInsets = .zero
|
|
||||||
favoritesCountButton.configuration = favoritesConfig
|
|
||||||
|
|
||||||
let reblogsFormat = NSLocalizedString("reblogs count", comment: "conv main status reblogs button label")
|
|
||||||
var reblogsConfig = UIButton.Configuration.plain()
|
|
||||||
reblogsConfig.baseForegroundColor = .secondaryLabel
|
|
||||||
reblogsConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(reblogsFormat, status.reblogsCount), attributes: attributes)
|
|
||||||
reblogsConfig.contentInsets = .zero
|
|
||||||
reblogsCountButton.configuration = reblogsConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func estimateContentHeight() -> CGFloat {
|
|
||||||
let width = bounds.width - 2*16
|
|
||||||
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIForPreferences(status: StatusMO) {
|
func updateUIForPreferences(status: StatusMO) {
|
||||||
baseUpdateUIForPreferences(status: status)
|
baseUpdateUIForPreferences(status: status)
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,6 +199,38 @@ class StatusCardView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadBlurHash() {
|
||||||
|
guard let card = card, let hash = card.blurhash else { return }
|
||||||
|
|
||||||
|
AttachmentView.queue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let size: CGSize
|
||||||
|
if let width = card.width, let height = card.height {
|
||||||
|
let aspectRatio = CGFloat(width) / CGFloat(height)
|
||||||
|
if aspectRatio > 1 {
|
||||||
|
size = CGSize(width: 32, height: 32 / aspectRatio)
|
||||||
|
} else {
|
||||||
|
size = CGSize(width: 32 * aspectRatio, height: 32)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
size = CGSize(width: 32, height: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let preview = UIImage(blurHash: hash, size: size) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self,
|
||||||
|
self.card?.url == card.url,
|
||||||
|
self.imageView.image == nil else { return }
|
||||||
|
|
||||||
|
self.imageView.image = preview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
hStack.backgroundColor = activeBackgroundColor
|
hStack.backgroundColor = activeBackgroundColor
|
||||||
setNeedsDisplay()
|
setNeedsDisplay()
|
||||||
|
|
|
@ -20,14 +20,14 @@ protocol StatusCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate,
|
||||||
protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate {
|
protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate {
|
||||||
// MARK: Subviews
|
// MARK: Subviews
|
||||||
var avatarImageView: CachedImageView { get }
|
var avatarImageView: CachedImageView { get }
|
||||||
var displayNameLabel: AccountDisplayNameLabel { get }
|
var displayNameLabel: EmojiLabel { get }
|
||||||
var usernameLabel: UILabel { get }
|
var usernameLabel: UILabel { get }
|
||||||
var contentWarningLabel: EmojiLabel { get }
|
var contentWarningLabel: EmojiLabel { get }
|
||||||
var collapseButton: StatusCollapseButton { get }
|
var collapseButton: StatusCollapseButton { get }
|
||||||
var contentContainer: StatusContentContainer<StatusContentTextView, StatusPollView> { get }
|
var contentContainer: StatusContentContainer<StatusContentTextView, StatusPollView> { get }
|
||||||
var replyButton: UIButton { get }
|
var replyButton: UIButton { get }
|
||||||
var favoriteButton: ToggleableButton { get }
|
var favoriteButton: UIButton { get }
|
||||||
var reblogButton: ToggleableButton { get }
|
var reblogButton: UIButton { get }
|
||||||
var moreButton: UIButton { get }
|
var moreButton: UIButton { get }
|
||||||
var prevThreadLinkView: UIView? { get set }
|
var prevThreadLinkView: UIView? { get set }
|
||||||
var nextThreadLinkView: UIView? { get set }
|
var nextThreadLinkView: UIView? { get set }
|
||||||
|
@ -45,8 +45,6 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
||||||
var cancellables: Set<AnyCancellable> { get set }
|
var cancellables: Set<AnyCancellable> { get set }
|
||||||
|
|
||||||
func updateUIForPreferences(status: StatusMO)
|
func updateUIForPreferences(status: StatusMO)
|
||||||
func updateStatusState(status: StatusMO)
|
|
||||||
func estimateContentHeight() -> CGFloat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: UI Configuration
|
// MARK: UI Configuration
|
||||||
|
@ -60,13 +58,7 @@ extension StatusCollectionViewCell {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.filter { [unowned self] in $0 == self.statusID }
|
.filter { [unowned self] in $0 == self.statusID }
|
||||||
.sink { [unowned self] _ in
|
.sink { [unowned self] _ in
|
||||||
if let status = self.mastodonController.persistentContainer.status(for: self.statusID) {
|
self.delegate?.statusCellNeedsReconfigure(self, animated: true, completion: nil)
|
||||||
// update immediately w/o animation
|
|
||||||
self.favoriteButton.active = status.favourited
|
|
||||||
self.reblogButton.active = status.reblogged
|
|
||||||
|
|
||||||
self.delegate?.statusCellNeedsReconfigure(self, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
@ -106,7 +98,6 @@ extension StatusCollectionViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUIForPreferences(status: status)
|
updateUIForPreferences(status: status)
|
||||||
updateStatusState(status: status)
|
|
||||||
|
|
||||||
contentWarningLabel.text = status.spoilerText
|
contentWarningLabel.text = status.spoilerText
|
||||||
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
||||||
|
@ -114,17 +105,35 @@ extension StatusCollectionViewCell {
|
||||||
contentWarningLabel.setEmojis(status.emojis, identifier: statusID)
|
contentWarningLabel.setEmojis(status.emojis, identifier: statusID)
|
||||||
}
|
}
|
||||||
|
|
||||||
reblogButton.isEnabled = reblogEnabled(status: status)
|
|
||||||
replyButton.isEnabled = mastodonController.loggedIn
|
replyButton.isEnabled = mastodonController.loggedIn
|
||||||
favoriteButton.isEnabled = mastodonController.loggedIn
|
|
||||||
|
|
||||||
let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight)
|
favoriteButton.isEnabled = mastodonController.loggedIn
|
||||||
// let didResolve = statusState.resolveFor(status: status) {
|
if status.favourited {
|
||||||
//// // layout so that we can take the content height into consideration when deciding whether to collapse
|
favoriteButton.tintColor = UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)
|
||||||
//// layoutIfNeeded()
|
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
|
||||||
//// return contentContainer.visibleSubviewHeight
|
} else {
|
||||||
// return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: )
|
favoriteButton.tintColor = nil
|
||||||
// }
|
favoriteButton.accessibilityLabel = NSLocalizedString("Favorite", comment: "favorite button accessibility label")
|
||||||
|
}
|
||||||
|
|
||||||
|
reblogButton.isEnabled = reblogEnabled(status: status)
|
||||||
|
if status.reblogged {
|
||||||
|
reblogButton.tintColor = UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)
|
||||||
|
reblogButton.accessibilityLabel = NSLocalizedString("Undo Reblog", comment: "undo reblog button accessibility label")
|
||||||
|
} else {
|
||||||
|
reblogButton.tintColor = nil
|
||||||
|
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||||
|
// do not include reply action here, because the cell already contains a button for it
|
||||||
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? [])
|
||||||
|
|
||||||
|
let didResolve = statusState.resolveFor(status: status) {
|
||||||
|
// layout so that we can take the content height into consideration when deciding whether to collapse
|
||||||
|
layoutIfNeeded()
|
||||||
|
return contentContainer.visibleSubviewHeight
|
||||||
|
}
|
||||||
if didResolve {
|
if didResolve {
|
||||||
if statusState.collapsible! && showStatusAutomatically {
|
if statusState.collapsible! && showStatusAutomatically {
|
||||||
statusState.collapsed = false
|
statusState.collapsed = false
|
||||||
|
@ -148,9 +157,7 @@ extension StatusCollectionViewCell {
|
||||||
guard mastodonController.loggedIn else {
|
guard mastodonController.loggedIn else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if status.visibility == .direct {
|
if status.visibility == .direct || status.visibility == .private {
|
||||||
return false
|
|
||||||
} else if status.visibility == .private {
|
|
||||||
if mastodonController.instanceFeatures.boostToOriginalAudience,
|
if mastodonController.instanceFeatures.boostToOriginalAudience,
|
||||||
status.account.id == mastodonController.account?.id {
|
status.account.id == mastodonController.account?.id {
|
||||||
return true
|
return true
|
||||||
|
@ -205,30 +212,6 @@ extension StatusCollectionViewCell {
|
||||||
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
func baseUpdateStatusState(status: StatusMO) {
|
|
||||||
favoriteButton.active = status.favourited
|
|
||||||
if status.favourited {
|
|
||||||
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
|
|
||||||
} else {
|
|
||||||
favoriteButton.accessibilityLabel = NSLocalizedString("Favorite", comment: "favorite button accessibility label")
|
|
||||||
}
|
|
||||||
reblogButton.active = status.reblogged
|
|
||||||
if status.reblogged {
|
|
||||||
reblogButton.accessibilityLabel = NSLocalizedString("Undo Reblog", comment: "undo reblog button accessibility label")
|
|
||||||
} else {
|
|
||||||
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
|
||||||
// do not include reply action here, because the cell already contains a button for it
|
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? [])
|
|
||||||
|
|
||||||
contentContainer.pollView.isHidden = status.poll == nil
|
|
||||||
contentContainer.pollView.mastodonController = mastodonController
|
|
||||||
contentContainer.pollView.delegate = delegate
|
|
||||||
contentContainer.pollView.updateUI(status: status, poll: status.poll)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setShowThreadLinks(prev: Bool, next: Bool) {
|
func setShowThreadLinks(prev: Bool, next: Bool) {
|
||||||
if prev {
|
if prev {
|
||||||
if let prevThreadLinkView {
|
if let prevThreadLinkView {
|
||||||
|
|
|
@ -8,11 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol StatusContentPollView: UIView {
|
class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UIView {
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusContentContainer<ContentView: ContentTextView, PollView: StatusContentPollView>: UIView {
|
|
||||||
|
|
||||||
private var useTopSpacer = false
|
private var useTopSpacer = false
|
||||||
private let topSpacer = UIView().configure {
|
private let topSpacer = UIView().configure {
|
||||||
|
@ -29,10 +25,9 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
|
||||||
$0.isSelectable = false
|
$0.isSelectable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var cardViewHeight: CGFloat { 90 }
|
|
||||||
let cardView = StatusCardView().configure {
|
let cardView = StatusCardView().configure {
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight),
|
$0.heightAnchor.constraint(equalToConstant: 90),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,30 +125,11 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
|
||||||
}
|
}
|
||||||
isCollapsed = collapsed
|
isCollapsed = collapsed
|
||||||
|
|
||||||
// don't call setNeedsUpdateConstraints b/c that destroys/recreates a bunch of other constraints
|
// ensure that we have a lastSubviewBottomConstraint
|
||||||
// if there is no lastSubviewBottomConstraint, then we already need a constraint update, so we don't need to do anything here
|
updateConstraintsIfNeeded()
|
||||||
if let lastSubviewBottomConstraint {
|
// force unwrap because the content container should always have at least one view
|
||||||
lastSubviewBottomConstraint.isActive = !collapsed
|
lastSubviewBottomConstraint!.isActive = !collapsed
|
||||||
zeroHeightConstraint.isActive = collapsed
|
zeroHeightConstraint.isActive = collapsed
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// used only for collapsing automatically based on height, doesn't need to be accurate
|
|
||||||
// just roughly inline with the content height
|
|
||||||
func estimateVisibleSubviewHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
var height: CGFloat = 0
|
|
||||||
height += contentTextView.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height
|
|
||||||
if !cardView.isHidden {
|
|
||||||
height += StatusContentContainer.cardViewHeight
|
|
||||||
}
|
|
||||||
if !attachmentsView.isHidden {
|
|
||||||
height += effectiveWidth / attachmentsView.aspectRatio
|
|
||||||
}
|
|
||||||
if !pollView.isHidden {
|
|
||||||
let pollHeight = pollView.estimateHeight(effectiveWidth: effectiveWidth)
|
|
||||||
height += pollHeight
|
|
||||||
}
|
|
||||||
return height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
$0.spacing = 4
|
$0.spacing = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
let displayNameLabel = AccountDisplayNameLabel().configure {
|
let displayNameLabel = EmojiLabel().configure {
|
||||||
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
||||||
.traits: [
|
.traits: [
|
||||||
UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue,
|
UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue,
|
||||||
|
@ -238,13 +238,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var favoriteButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
private(set) lazy var favoriteButton = UIButton().configure {
|
||||||
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
||||||
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var reblogButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
private(set) lazy var reblogButton = UIButton().configure {
|
||||||
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
||||||
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||||
|
@ -619,15 +619,10 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
metaIndicatorsView.updateUI(status: status)
|
metaIndicatorsView.updateUI(status: status)
|
||||||
|
|
||||||
timelineReasonIcon.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.timelineReasonIconSize
|
timelineReasonIcon.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.timelineReasonIconSize
|
||||||
}
|
if let rebloggerID,
|
||||||
|
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
||||||
func updateStatusState(status: StatusMO) {
|
updateRebloggerLabel(reblogger: reblogger)
|
||||||
baseUpdateStatusState(status: status)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func estimateContentHeight() -> CGFloat {
|
|
||||||
let width = bounds.width /* leading spacing: */ - 16 /* avatar: */ - 50 /* spacing: 8 */ - 8 /* trailing spacing: */ - 16
|
|
||||||
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTimestamp() {
|
private func updateTimestamp() {
|
||||||
|
@ -699,11 +694,6 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
|
|
||||||
updateUIForPreferences(status: status)
|
updateUIForPreferences(status: status)
|
||||||
|
|
||||||
if let rebloggerID,
|
|
||||||
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
|
||||||
updateRebloggerLabel(reblogger: reblogger)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
updateGrayscaleableUI(status: status)
|
updateGrayscaleableUI(status: status)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
//
|
|
||||||
// ToggleableButton.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/13/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class ToggleableButton: UIButton {
|
|
||||||
|
|
||||||
let activeColor: UIColor
|
|
||||||
|
|
||||||
var active: Bool {
|
|
||||||
didSet {
|
|
||||||
tintColor = active ? activeColor : nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(activeColor: UIColor) {
|
|
||||||
self.activeColor = activeColor
|
|
||||||
self.active = false
|
|
||||||
super.init(frame: .zero)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue