Compare commits
20 Commits
6261318df1
...
fc7e7f502b
Author | SHA1 | Date |
---|---|---|
Shadowfacts | fc7e7f502b | |
Shadowfacts | 38a2ebd32b | |
Shadowfacts | 3b965b92f2 | |
Shadowfacts | 421cb7ba03 | |
Shadowfacts | 8319935a3d | |
Shadowfacts | 91ef386a41 | |
Shadowfacts | c8eec17180 | |
Shadowfacts | c94e60d49b | |
Shadowfacts | b00170c3f9 | |
Shadowfacts | b37e5fffbf | |
Shadowfacts | 8c27a9368f | |
Shadowfacts | 735659dee6 | |
Shadowfacts | bf02b185ed | |
Shadowfacts | 4ccf5d21a4 | |
Shadowfacts | 9ac1c43511 | |
Shadowfacts | 76b9496fe6 | |
Shadowfacts | ae8191ca0e | |
Shadowfacts | a9a9bfebeb | |
Shadowfacts | 2d8e2f0824 | |
Shadowfacts | 6f18d46037 |
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,5 +1,19 @@
|
|||
# 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)
|
||||
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,19 +95,21 @@ class AttachmentThumbnailController: ViewController {
|
|||
self.gifController = GIFController(gifData: data)
|
||||
} else if type.conforms(to: .image),
|
||||
let image = UIImage(data: data) {
|
||||
if fullSize {
|
||||
// using prepareThumbnail on images from PHPicker results in extremely high memory usage,
|
||||
// crashing share extension. see FB12186346
|
||||
// if fullSize {
|
||||
image.prepareForDisplay { prepared in
|
||||
DispatchQueue.main.async {
|
||||
self.image = prepared
|
||||
}
|
||||
}
|
||||
} else {
|
||||
image.prepareThumbnail(of: CGSize(width: 80, height: 80)) { prepared in
|
||||
DispatchQueue.main.async {
|
||||
self.image = prepared
|
||||
}
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
// } else {
|
||||
// image.prepareThumbnail(of: CGSize(width: 80, height: 80)) { prepared in
|
||||
// DispatchQueue.main.async {
|
||||
// self.image = prepared
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ public class InstanceFeatures: ObservableObject {
|
|||
}
|
||||
|
||||
public var needsWideColorGamutHack: Bool {
|
||||
if case .mastodon(_, .some(let version)) = instanceType {
|
||||
if case .mastodon(_, let version) = instanceType {
|
||||
return version < Version(4, 0, 0)
|
||||
} else {
|
||||
return true
|
||||
|
@ -116,8 +116,16 @@ public class InstanceFeatures: ObservableObject {
|
|||
}
|
||||
|
||||
public var editStatuses: Bool {
|
||||
// todo: does this require a particular akkoma version?
|
||||
hasMastodonVersion(3, 5, 0) || instanceType.isPleroma(.akkoma(nil))
|
||||
switch instanceType {
|
||||
case .mastodon(_, let v) where v >= Version(3, 5, 0):
|
||||
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 {
|
||||
|
@ -188,7 +196,7 @@ public class InstanceFeatures: ObservableObject {
|
|||
}
|
||||
|
||||
public func hasMastodonVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
||||
if case .mastodon(_, .some(let version)) = instanceType {
|
||||
if case .mastodon(_, let version) = instanceType {
|
||||
return version >= Version(major, minor, patch)
|
||||
} else {
|
||||
return false
|
||||
|
@ -197,7 +205,7 @@ public class InstanceFeatures: ObservableObject {
|
|||
|
||||
func hasPleromaVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
||||
switch instanceType {
|
||||
case .pleroma(.vanilla(.some(let version))), .pleroma(.akkoma(.some(let version))):
|
||||
case .pleroma(.vanilla(let version)), .pleroma(.akkoma(let version)):
|
||||
return version >= Version(major, minor, patch)
|
||||
default:
|
||||
return false
|
||||
|
@ -283,61 +291,3 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
public var localizedDescription: String {
|
||||
public var errorDescription: String? {
|
||||
switch type {
|
||||
case .networkError(let error):
|
||||
return "Network Error: \(error.localizedDescription)"
|
||||
|
|
|
@ -24,7 +24,9 @@ public final class CollapseState: Sendable {
|
|||
}
|
||||
|
||||
public func copy() -> CollapseState {
|
||||
return CollapseState(collapsible: self.collapsible, collapsed: self.collapsed)
|
||||
let new = CollapseState(collapsible: self.collapsible, collapsed: self.collapsed)
|
||||
new.statusPropertiesHash = self.statusPropertiesHash
|
||||
return new
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
<integer>4</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
|
|
|
@ -71,36 +71,43 @@ class ShareViewController: UIViewController {
|
|||
|
||||
private func getDraftConfigurationFromExtensionContext() async -> (String, [DraftAttachment]) {
|
||||
guard let extensionContext,
|
||||
let inputItem = (extensionContext.inputItems as? [NSExtensionItem])?.first,
|
||||
let itemProvider = inputItem.attachments?.first else {
|
||||
let inputItem = (extensionContext.inputItems as? [NSExtensionItem])?.first else {
|
||||
return ("", [])
|
||||
}
|
||||
if let url: NSURL = await getObject(from: itemProvider) {
|
||||
if let title = inputItem.attributedTitle ?? inputItem.attributedContentText {
|
||||
return ("\n\n\(title.string)\n\(url.absoluteString ?? "")", [])
|
||||
} else {
|
||||
return ("\n\n\(url.absoluteString ?? "")", [])
|
||||
var text: String = ""
|
||||
var url: URL?
|
||||
var attachments: [DraftAttachment] = []
|
||||
|
||||
for itemProvider in inputItem.attachments ?? [] {
|
||||
if let attached: NSURL = await getObject(from: itemProvider) {
|
||||
if url == nil {
|
||||
url = attached as URL
|
||||
}
|
||||
} 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)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
return await group.reduce(into: [], { partialResult, result in
|
||||
if let result {
|
||||
partialResult.append(result)
|
||||
if text.isEmpty,
|
||||
let s = inputItem.attributedTitle ?? inputItem.attributedContentText {
|
||||
text = s.string
|
||||
}
|
||||
})
|
||||
|
||||
if let url {
|
||||
if !text.isEmpty {
|
||||
text += "\n"
|
||||
}
|
||||
return ("", attachments)
|
||||
text += url.absoluteString
|
||||
}
|
||||
|
||||
if !text.isEmpty {
|
||||
text = "\n\n\(text)"
|
||||
}
|
||||
return (text, attachments)
|
||||
}
|
||||
|
||||
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 */; };
|
||||
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */; };
|
||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */; };
|
||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */; };
|
||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */; };
|
||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F3B2560365300F6E31D /* RefreshableViewController.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 */; };
|
||||
|
@ -301,6 +301,8 @@
|
|||
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; };
|
||||
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.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 */; };
|
||||
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
||||
|
@ -637,7 +639,7 @@
|
|||
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>"; };
|
||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
|
||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
|
||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameView.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>"; };
|
||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
||||
|
@ -700,6 +702,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1349,7 +1353,7 @@
|
|||
D6BED1722126661300F02DA0 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */,
|
||||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
||||
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
||||
|
@ -1371,6 +1375,7 @@
|
|||
D65B4B69297777D900DABDFB /* StatusNotFoundView.swift */,
|
||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
||||
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */,
|
||||
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */,
|
||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||
|
@ -1383,6 +1388,7 @@
|
|||
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||
D641C78A213DD926004B4513 /* Status */,
|
||||
D64AAE8F26C80DB600FC57FB /* Toast */,
|
||||
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1945,6 +1951,7 @@
|
|||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
|
||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||
D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */,
|
||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||
|
@ -2009,7 +2016,7 @@
|
|||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
||||
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
|
||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
||||
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
||||
|
@ -2105,6 +2112,7 @@
|
|||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
||||
D6ADB6E828E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift in Sources */,
|
||||
D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */,
|
||||
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */,
|
||||
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */,
|
||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||
|
@ -2370,7 +2378,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
@ -2436,7 +2444,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
|
@ -2462,7 +2470,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||
|
@ -2491,7 +2499,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||
|
@ -2520,7 +2528,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||
|
@ -2675,7 +2683,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
@ -2706,7 +2714,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
@ -2812,7 +2820,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
|
@ -2838,7 +2846,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 91;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
<?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,6 +88,10 @@
|
|||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.CloudKitDebug 0"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-UIFocusLoggingEnabled YES"
|
||||
isEnabled = "NO">
|
||||
|
|
|
@ -37,6 +37,11 @@ class ImageCache {
|
|||
completion?(entry.data, entry.image)
|
||||
return nil
|
||||
} else {
|
||||
return getFromSource(url, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
||||
return Task.detached(priority: .userInitiated) {
|
||||
let result = await self.fetch(url: url)
|
||||
switch result {
|
||||
|
@ -49,7 +54,6 @@ class ImageCache {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
|
||||
if !ImageCache.disableCaching,
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
|
||||
extension NSTextAttachment {
|
||||
// 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) {
|
||||
let adjustedCapHeight = font.capHeight - 1
|
||||
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
|
||||
|
|
|
@ -19,7 +19,7 @@ extension StatusEdit: CollapseStateResolving {}
|
|||
extension CollapseState {
|
||||
|
||||
func resolveFor(status: CollapseStateResolving, height: () -> CGFloat, textLength: Int? = nil) -> Bool {
|
||||
lazy var newHash = hashStatusProperties(status: status)
|
||||
let newHash = hashStatusProperties(status: status)
|
||||
guard unknown || statusPropertiesHash != newHash else {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ protocol Configurable {
|
|||
func configure(_ closure: (T) -> Void) -> T
|
||||
}
|
||||
extension Configurable where Self: UIView {
|
||||
@inline(__always)
|
||||
func configure(_ closure: (Self) -> Void) -> Self {
|
||||
closure(self)
|
||||
return self
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
|
||||
struct ImageGrayscalifier {
|
||||
static let queue = DispatchQueue(label: "ImageGrayscalifier", qos: .default)
|
||||
static let queue = DispatchQueue(label: "ImageGrayscalifier", qos: .userInitiated)
|
||||
|
||||
private static let context = CIContext()
|
||||
private static let cache = NSCache<NSURL, UIImage>()
|
||||
|
@ -24,6 +24,17 @@ 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? {
|
||||
if let url = url,
|
||||
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 },
|
||||
fetchAttachment: { @MainActor in await ImageCache.attachments.get($0).1 },
|
||||
fetchStatus: { mastodonController.persistentContainer.status(for: $0) },
|
||||
displayNameLabel: { AnyView(AccountDisplayNameLabel(account: $0, textStyle: $1, emojiSize: $2)) },
|
||||
displayNameLabel: { AnyView(AccountDisplayNameView(account: $0, textStyle: $1, emojiSize: $2)) },
|
||||
replyContentView: { AnyView(ComposeReplyContentView(status: $0, mastodonController: mastodonController, heightChanged: $1)) },
|
||||
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)
|
||||
}
|
||||
snapshot.appendItems(parentItems, toSection: .ancestors)
|
||||
snapshot.reloadItems([mainStatusItem])
|
||||
snapshot.reconfigureItems([mainStatusItem])
|
||||
|
||||
// convert sub-threads into items for section and add to snapshot
|
||||
self.addFlattenedChildThreadsToSnapshot(tree.descendants, mainStatus: mainStatus, snapshot: &snapshot)
|
||||
|
|
|
@ -15,7 +15,7 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
|||
@IBOutlet weak var headerImageView: UIImageView!
|
||||
@IBOutlet weak var avatarContainerView: UIView!
|
||||
@IBOutlet weak var avatarImageView: UIImageView!
|
||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
||||
@IBOutlet weak var noteTextView: StatusContentTextView!
|
||||
|
||||
var account: Account?
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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">
|
||||
<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">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -47,7 +47,7 @@
|
|||
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="voW-Is-1b2" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<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">
|
||||
<rect key="frame" x="76" y="72" width="316" height="24"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
||||
<nil key="textColor"/>
|
||||
|
|
|
@ -21,7 +21,7 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
|
|||
@IBOutlet weak var headerImageView: CachedImageView!
|
||||
@IBOutlet weak var avatarContainerView: UIView!
|
||||
@IBOutlet weak var avatarImageView: CachedImageView!
|
||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
||||
@IBOutlet weak var usernameLabel: UILabel!
|
||||
@IBOutlet weak var noteTextView: StatusContentTextView!
|
||||
@IBOutlet weak var suggestionSourceButton: UIButton!
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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">
|
||||
<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">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||
<capability name="System colors in document resources" 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"/>
|
||||
|
@ -44,7 +44,7 @@
|
|||
<constraint firstAttribute="width" constant="90" id="wav-YT-e4Y"/>
|
||||
</constraints>
|
||||
</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="EmojiLabel" 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="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="106" y="100" width="333" height="29"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
|
||||
<nil key="textColor"/>
|
||||
|
|
|
@ -65,7 +65,7 @@ struct MuteAccountView: View {
|
|||
)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
AccountDisplayNameLabel(account: account, textStyle: .headline, emojiSize: 17)
|
||||
AccountDisplayNameView(account: account, textStyle: .headline, emojiSize: 17)
|
||||
Text("@\(account.acct)")
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(.secondary)
|
||||
|
|
|
@ -55,7 +55,7 @@ struct ReportView: View {
|
|||
)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
AccountDisplayNameLabel(account: account, textStyle: .headline, emojiSize: 17)
|
||||
AccountDisplayNameView(account: account, textStyle: .headline, emojiSize: 17)
|
||||
Text("@\(account.acct)")
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(.secondary)
|
||||
|
|
|
@ -117,8 +117,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
|||
}
|
||||
|
||||
_ = state.resolveFor(status: edit, height: {
|
||||
layoutIfNeeded()
|
||||
return contentContainer.visibleSubviewHeight
|
||||
let width = self.bounds.width - 2*16
|
||||
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
|
||||
})
|
||||
collapseButton.isHidden = !state.collapsible!
|
||||
contentContainer.setCollapsed(state.collapsed!)
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class StatusEditPollView: UIStackView {
|
||||
class StatusEditPollView: UIStackView, StatusContentPollView {
|
||||
|
||||
private var titleLabels: [EmojiLabel] = []
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
@ -25,6 +27,7 @@ class StatusEditPollView: UIStackView {
|
|||
|
||||
func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) {
|
||||
arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
titleLabels = []
|
||||
|
||||
for option in poll?.options ?? [] {
|
||||
// the edit poll doesn't actually include the multiple value
|
||||
|
@ -33,6 +36,7 @@ class StatusEditPollView: UIStackView {
|
|||
let label = EmojiLabel()
|
||||
label.text = option.title
|
||||
label.setEmojis(emojis, identifier: Optional<String>.none)
|
||||
titleLabels.append(label)
|
||||
let stack = UIStackView(arrangedSubviews: [
|
||||
icon,
|
||||
label,
|
||||
|
@ -44,4 +48,14 @@ class StatusEditPollView: UIStackView {
|
|||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
@IBOutlet weak var avatarImageView: UIImageView!
|
||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
||||
@IBOutlet weak var usernameLabel: UILabel!
|
||||
@IBOutlet weak var noteLabel: EmojiLabel!
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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">
|
||||
<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">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -28,7 +28,7 @@
|
|||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Iif-9m-vM5">
|
||||
<rect key="frame" x="74" y="11" width="230" height="78"/>
|
||||
<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="EmojiLabel" 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="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="230" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
|
|
|
@ -12,7 +12,7 @@ import Pachyderm
|
|||
class LargeAccountDetailView: UIView {
|
||||
|
||||
var avatarImageView = UIImageView()
|
||||
var displayNameLabel = EmojiLabel()
|
||||
var displayNameLabel = AccountDisplayNameLabel()
|
||||
var usernameLabel = UILabel()
|
||||
|
||||
var avatarRequest: ImageCache.Request?
|
||||
|
|
|
@ -2,106 +2,31 @@
|
|||
// AccountDisplayNameLabel.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/7/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
// Created by Shadowfacts on 5/14/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import WebURLFoundationExtras
|
||||
|
||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||
class AccountDisplayNameLabel: EmojiLabel {
|
||||
|
||||
struct AccountDisplayNameLabel: View {
|
||||
let account: any AccountProtocol
|
||||
let textStyle: Font.TextStyle
|
||||
@ScaledMetric var emojiSize: CGFloat
|
||||
@State var text: Text
|
||||
@State var emojiRequests = [ImageCache.Request]()
|
||||
private var accountID: String?
|
||||
// store the display name, so that if it changes the label updates w/o changing the id
|
||||
private var accountDisplayName: String?
|
||||
|
||||
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))
|
||||
func updateForAccountDisplayName(account: some AccountProtocol) {
|
||||
guard accountID != account.id || accountDisplayName != account.displayName || Preferences.shared.hideCustomEmojiInUsernames == hasEmojis else {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
accountID = account.id
|
||||
accountDisplayName = account.displayName
|
||||
self.text = accountDisplayName
|
||||
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||
self.removeEmojis()
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
self.setEmojis(account.emojis, identifier: account.id)
|
||||
}
|
||||
}
|
||||
|
||||
//struct AccountDisplayNameLabel_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// AccountDisplayNameLabel()
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
// 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,10 +75,8 @@ class AttachmentView: GIFImageView {
|
|||
gifPlaybackModeChanged()
|
||||
|
||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||
ImageGrayscalifier.queue.async {
|
||||
self.displayImage()
|
||||
}
|
||||
}
|
||||
|
||||
if getBadges().isEmpty != Preferences.shared.showAttachmentBadges {
|
||||
createBadgesView(getBadges())
|
||||
|
@ -189,53 +187,59 @@ class AttachmentView: GIFImageView {
|
|||
|
||||
func loadImage() {
|
||||
let attachmentURL = attachment.url
|
||||
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
|
||||
guard let self = self, let data = data else { return }
|
||||
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, image) in
|
||||
guard let self = self,
|
||||
self.attachment.url == attachmentURL else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.attachmentRequest = nil
|
||||
}
|
||||
if self.attachment.url.pathExtension == "gif" {
|
||||
self.source = .gifData(attachmentURL, data)
|
||||
let controller = GIFController(gifData: data)
|
||||
DispatchQueue.main.async {
|
||||
controller.attach(to: self)
|
||||
|
||||
if attachmentURL.pathExtension == "gif",
|
||||
let data {
|
||||
self.source = .gifData(attachmentURL, data, image)
|
||||
if self.autoplayGifs {
|
||||
let controller = GIFController(gifData: data)
|
||||
controller.attach(to: self)
|
||||
controller.startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
if !self.autoplayGifs {
|
||||
self.displayImage()
|
||||
}
|
||||
|
||||
} else {
|
||||
self.source = .imageData(attachmentURL, data)
|
||||
self.displayImage()
|
||||
}
|
||||
} else if let image {
|
||||
self.source = .image(attachmentURL, image)
|
||||
self.displayImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadVideo() {
|
||||
if let previewURL = self.attachment.previewURL {
|
||||
attachmentRequest = ImageCache.attachments.get(previewURL, completion: { [weak self] (data, _ )in
|
||||
guard let self = self, let data = data else { return }
|
||||
attachmentRequest = ImageCache.attachments.get(previewURL, completion: { [weak self] (_, image) in
|
||||
guard let self, let image else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.attachmentRequest = nil
|
||||
self.source = .imageData(previewURL, data)
|
||||
self.source = .image(previewURL, image)
|
||||
self.displayImage()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let attachmentURL = self.attachment.url
|
||||
AttachmentView.queue.async {
|
||||
AttachmentView.queue.async { [weak self] in
|
||||
let asset = AVURLAsset(url: attachmentURL)
|
||||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
||||
self.source = .cgImage(attachmentURL, image)
|
||||
UIImage(cgImage: image).prepareForDisplay { [weak self] image in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self, let image else { return }
|
||||
self.source = .image(attachmentURL, image)
|
||||
self.displayImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
|
||||
playImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -276,9 +280,11 @@ class AttachmentView: GIFImageView {
|
|||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.source = .cgImage(attachmentURL, image)
|
||||
self.displayImage()
|
||||
}
|
||||
}
|
||||
|
||||
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
|
||||
self.gifvView = gifvView
|
||||
|
@ -295,33 +301,51 @@ class AttachmentView: GIFImageView {
|
|||
])
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func displayImage() {
|
||||
isGrayscale = Preferences.shared.grayscaleImages
|
||||
|
||||
let image: UIImage?
|
||||
|
||||
switch source {
|
||||
case nil:
|
||||
image = nil
|
||||
self.image = nil
|
||||
|
||||
case let .imageData(url, data), let .gifData(url, data):
|
||||
case let .image(url, sourceImage):
|
||||
if isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: url, data: data)
|
||||
ImageGrayscalifier.queue.async { [weak self] in
|
||||
let grayscale = ImageGrayscalifier.convert(url: url, image: sourceImage)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.image = grayscale
|
||||
}
|
||||
}
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
self.image = sourceImage
|
||||
}
|
||||
|
||||
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):
|
||||
if isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
|
||||
ImageGrayscalifier.queue.async { [weak self] in
|
||||
let grayscale = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.image = grayscale
|
||||
}
|
||||
}
|
||||
} else {
|
||||
image = UIImage(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
|
||||
private func createBadgesView(_ badges: Badges) {
|
||||
|
@ -409,8 +433,8 @@ class AttachmentView: GIFImageView {
|
|||
|
||||
fileprivate extension AttachmentView {
|
||||
enum Source {
|
||||
case imageData(URL, Data)
|
||||
case gifData(URL, Data)
|
||||
case image(URL, UIImage)
|
||||
case gifData(URL, Data, UIImage?)
|
||||
case cgImage(URL, CGImage)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,15 @@ class AttachmentsContainerView: UIView {
|
|||
let attachmentStacks: NSHashTable<UIStackView> = .weakObjects()
|
||||
var moreView: UIView?
|
||||
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 hideButtonView: UIVisualEffectView?
|
||||
|
@ -93,7 +102,8 @@ class AttachmentsContainerView: UIView {
|
|||
fillView(attachmentView)
|
||||
sendSubviewToBack(attachmentView)
|
||||
accessibilityElements.append(attachmentView)
|
||||
if let attachmentAspectRatio = attachmentView.attachmentAspectRatio {
|
||||
if Preferences.shared.showUncroppedMediaInline,
|
||||
let attachmentAspectRatio = attachmentView.attachmentAspectRatio {
|
||||
aspectRatio = attachmentAspectRatio
|
||||
}
|
||||
case 2:
|
||||
|
@ -266,18 +276,7 @@ class AttachmentsContainerView: UIView {
|
|||
accessibilityElements.append(moreView)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
self.aspectRatio = aspectRatio
|
||||
} else {
|
||||
self.isHidden = true
|
||||
}
|
||||
|
|
|
@ -40,6 +40,16 @@ extension BaseEmojiLabel {
|
|||
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>()
|
||||
var foundEmojis = false
|
||||
|
||||
|
@ -57,22 +67,36 @@ extension BaseEmojiLabel {
|
|||
foundEmojis = true
|
||||
|
||||
if let image = ImageCache.emojis.get(URL(emoji.url)!)?.image {
|
||||
// if the image is cached, add it immediately
|
||||
emojiImages[emoji.shortcode] = image
|
||||
// if the image is cached, add it immediately.
|
||||
// we generate the thumbnail on the main thread, because it's usually fast enough
|
||||
// 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 {
|
||||
// otherwise, perform the network request
|
||||
|
||||
group.enter()
|
||||
// todo: ImageCache.emojis.get here will re-check the memory and disk caches, there should be another method to force-refetch
|
||||
let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
|
||||
guard let image = image,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: image) else {
|
||||
let request = ImageCache.emojis.getFromSource(URL(emoji.url)!) { (_, image) in
|
||||
guard let image else {
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in
|
||||
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()
|
||||
}
|
||||
}
|
||||
if let request = request {
|
||||
emojiRequests.append(request)
|
||||
}
|
||||
|
@ -91,10 +115,8 @@ 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)
|
||||
// so, just ignore the warnings
|
||||
let emojiAttachments = emojiImages.withLock {
|
||||
let emojiFont = self.emojiFont
|
||||
let emojiTextColor = self.emojiTextColor
|
||||
return $0.mapValues { image in
|
||||
NSTextAttachment(emojiImage: image, in: emojiFont, with: emojiTextColor)
|
||||
NSTextAttachment(image: image)
|
||||
}
|
||||
}
|
||||
let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil
|
||||
|
|
|
@ -18,7 +18,7 @@ class CachedImageView: UIImageView {
|
|||
|
||||
override var image: UIImage? {
|
||||
didSet {
|
||||
fetchTask?.cancel()
|
||||
blurHashTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ class ConfirmReblogStatusPreviewView: UIView {
|
|||
vStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
hStack.addArrangedSubview(vStack)
|
||||
|
||||
let displayNameLabel = EmojiLabel()
|
||||
let displayNameLabel = AccountDisplayNameLabel()
|
||||
displayNameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1).addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]]), size: 0)
|
||||
displayNameLabel.adjustsFontSizeToFitWidth = true
|
||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
|
|
@ -38,24 +38,3 @@ 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,24 +11,27 @@ import Pachyderm
|
|||
|
||||
class PollOptionView: UIView {
|
||||
|
||||
private let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
|
||||
private static let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
|
||||
|
||||
let checkbox: PollOptionCheckboxView
|
||||
private(set) var label: EmojiLabel!
|
||||
private(set) var checkbox: PollOptionCheckboxView?
|
||||
|
||||
init(poll: Poll, option: Poll.Option, mastodonController: MastodonController) {
|
||||
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
let minHeight: CGFloat = 35
|
||||
layer.cornerRadius = 0.1 * minHeight
|
||||
layer.cornerCurve = .continuous
|
||||
backgroundColor = unselectedBackgroundColor
|
||||
backgroundColor = PollOptionView.unselectedBackgroundColor
|
||||
|
||||
checkbox.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(checkbox)
|
||||
let showCheckbox = poll.ownVotes?.isEmpty == false || !poll.effectiveExpired
|
||||
if showCheckbox {
|
||||
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
|
||||
checkbox!.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(checkbox!)
|
||||
}
|
||||
|
||||
let label = EmojiLabel()
|
||||
label = EmojiLabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.numberOfLines = 0
|
||||
label.font = .preferredFont(forTextStyle: .callout)
|
||||
|
@ -88,12 +91,8 @@ class PollOptionView: UIView {
|
|||
NSLayoutConstraint.activate([
|
||||
minHeightConstraint,
|
||||
|
||||
checkbox.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
checkbox.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||
|
||||
label.topAnchor.constraint(equalTo: topAnchor, 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),
|
||||
|
||||
percentLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||
|
@ -101,6 +100,16 @@ class PollOptionView: UIView {
|
|||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ class PollOptionsView: UIControl {
|
|||
var mastodonController: MastodonController!
|
||||
|
||||
var checkedOptionIndices: [Int] {
|
||||
options.enumerated().filter { $0.element.checkbox.isChecked }.map(\.offset)
|
||||
options.enumerated().filter { $0.element.checkbox?.isChecked == true }.map(\.offset)
|
||||
}
|
||||
var checkedOptionsChanged: (() -> Void)?
|
||||
|
||||
|
@ -32,7 +32,7 @@ class PollOptionsView: UIControl {
|
|||
|
||||
override var isEnabled: Bool {
|
||||
didSet {
|
||||
options.forEach { $0.checkbox.readOnly = !isEnabled }
|
||||
options.forEach { $0.checkbox?.readOnly = !isEnabled }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,9 +65,11 @@ class PollOptionsView: UIControl {
|
|||
|
||||
options = poll.options.enumerated().map { (index, opt) in
|
||||
let optionView = PollOptionView(poll: poll, option: opt, mastodonController: mastodonController)
|
||||
optionView.checkbox.readOnly = !isEnabled
|
||||
optionView.checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
|
||||
optionView.checkbox.voted = poll.voted ?? false
|
||||
if let checkbox = optionView.checkbox {
|
||||
checkbox.readOnly = !isEnabled
|
||||
checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
|
||||
checkbox.voted = poll.voted ?? false
|
||||
}
|
||||
stack.addArrangedSubview(optionView)
|
||||
return optionView
|
||||
}
|
||||
|
@ -75,15 +77,25 @@ class PollOptionsView: UIControl {
|
|||
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) {
|
||||
if poll.multiple {
|
||||
option.checkbox.isChecked.toggle()
|
||||
option.checkbox?.isChecked.toggle()
|
||||
} else {
|
||||
for opt in options {
|
||||
if opt === option {
|
||||
opt.checkbox.isChecked = true
|
||||
opt.checkbox?.isChecked = true
|
||||
} else {
|
||||
opt.checkbox.isChecked = false
|
||||
opt.checkbox?.isChecked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class StatusPollView: UIView {
|
||||
class StatusPollView: UIView, StatusContentPollView {
|
||||
|
||||
private static let formatter: DateComponentsFormatter = {
|
||||
let f = DateComponentsFormatter()
|
||||
|
@ -140,6 +140,11 @@ class StatusPollView: UIView {
|
|||
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() {
|
||||
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class ProfileHeaderView: UIView {
|
|||
@IBOutlet weak var avatarImageView: UIImageView!
|
||||
@IBOutlet weak var moreButton: ProfileHeaderButton!
|
||||
@IBOutlet weak var followButton: ProfileHeaderButton!
|
||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
||||
@IBOutlet weak var usernameLabel: UILabel!
|
||||
@IBOutlet weak var lockImageView: UIImageView!
|
||||
@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"/>
|
||||
</constraints>
|
||||
</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="EmojiLabel" 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="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="144" y="206" width="254" height="29"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
|
||||
<nil key="textColor"/>
|
||||
|
|
|
@ -39,7 +39,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
||||
}
|
||||
|
||||
let displayNameLabel = EmojiLabel().configure {
|
||||
let displayNameLabel = AccountDisplayNameLabel().configure {
|
||||
$0.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 24, weight: .semibold))
|
||||
$0.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
|
@ -191,13 +191,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var favoriteButton = UIButton().configure {
|
||||
private(set) lazy var favoriteButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
||||
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var reblogButton = UIButton().configure {
|
||||
private(set) lazy var reblogButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
||||
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
|
@ -348,24 +348,6 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
accountDetailAccessibilityElement.navigationDelegate = delegate
|
||||
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)
|
||||
if let application = status.applicationName {
|
||||
timestampAndClientText += " • \(application)"
|
||||
|
@ -376,7 +358,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
editTimestampButton.isHidden = false
|
||||
var config = UIButton.Configuration.plain()
|
||||
config.baseForegroundColor = .secondaryLabel
|
||||
config.attributedTitle = AttributedString("Edited on \(ConversationMainStatusCollectionViewCell.dateFormatter.string(from: editedAt))", attributes: metaButtonAttributes)
|
||||
config.attributedTitle = AttributedString("Edited on \(ConversationMainStatusCollectionViewCell.dateFormatter.string(from: editedAt))", attributes: AttributeContainer([
|
||||
.font: ConversationMainStatusCollectionViewCell.metaFont
|
||||
]))
|
||||
config.contentInsets = .zero
|
||||
editTimestampButton.configuration = config
|
||||
} else {
|
||||
|
@ -392,6 +376,33 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
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) {
|
||||
baseUpdateUIForPreferences(status: status)
|
||||
}
|
||||
|
|
|
@ -199,38 +199,6 @@ 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?) {
|
||||
hStack.backgroundColor = activeBackgroundColor
|
||||
setNeedsDisplay()
|
||||
|
|
|
@ -20,14 +20,14 @@ protocol StatusCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate,
|
|||
protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate {
|
||||
// MARK: Subviews
|
||||
var avatarImageView: CachedImageView { get }
|
||||
var displayNameLabel: EmojiLabel { get }
|
||||
var displayNameLabel: AccountDisplayNameLabel { get }
|
||||
var usernameLabel: UILabel { get }
|
||||
var contentWarningLabel: EmojiLabel { get }
|
||||
var collapseButton: StatusCollapseButton { get }
|
||||
var contentContainer: StatusContentContainer<StatusContentTextView, StatusPollView> { get }
|
||||
var replyButton: UIButton { get }
|
||||
var favoriteButton: UIButton { get }
|
||||
var reblogButton: UIButton { get }
|
||||
var favoriteButton: ToggleableButton { get }
|
||||
var reblogButton: ToggleableButton { get }
|
||||
var moreButton: UIButton { get }
|
||||
var prevThreadLinkView: UIView? { get set }
|
||||
var nextThreadLinkView: UIView? { get set }
|
||||
|
@ -45,6 +45,8 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
|||
var cancellables: Set<AnyCancellable> { get set }
|
||||
|
||||
func updateUIForPreferences(status: StatusMO)
|
||||
func updateStatusState(status: StatusMO)
|
||||
func estimateContentHeight() -> CGFloat
|
||||
}
|
||||
|
||||
// MARK: UI Configuration
|
||||
|
@ -58,8 +60,14 @@ extension StatusCollectionViewCell {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.filter { [unowned self] in $0 == self.statusID }
|
||||
.sink { [unowned self] _ in
|
||||
if let status = self.mastodonController.persistentContainer.status(for: self.statusID) {
|
||||
// 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)
|
||||
|
||||
mastodonController.persistentContainer.accountSubject
|
||||
|
@ -98,6 +106,7 @@ extension StatusCollectionViewCell {
|
|||
}
|
||||
|
||||
updateUIForPreferences(status: status)
|
||||
updateStatusState(status: status)
|
||||
|
||||
contentWarningLabel.text = status.spoilerText
|
||||
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
||||
|
@ -105,35 +114,17 @@ extension StatusCollectionViewCell {
|
|||
contentWarningLabel.setEmojis(status.emojis, identifier: statusID)
|
||||
}
|
||||
|
||||
replyButton.isEnabled = mastodonController.loggedIn
|
||||
|
||||
favoriteButton.isEnabled = mastodonController.loggedIn
|
||||
if status.favourited {
|
||||
favoriteButton.tintColor = UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)
|
||||
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
|
||||
} else {
|
||||
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")
|
||||
}
|
||||
replyButton.isEnabled = mastodonController.loggedIn
|
||||
favoriteButton.isEnabled = mastodonController.loggedIn
|
||||
|
||||
// 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
|
||||
}
|
||||
let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight)
|
||||
// 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
|
||||
// return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: )
|
||||
// }
|
||||
if didResolve {
|
||||
if statusState.collapsible! && showStatusAutomatically {
|
||||
statusState.collapsed = false
|
||||
|
@ -157,7 +148,9 @@ extension StatusCollectionViewCell {
|
|||
guard mastodonController.loggedIn else {
|
||||
return false
|
||||
}
|
||||
if status.visibility == .direct || status.visibility == .private {
|
||||
if status.visibility == .direct {
|
||||
return false
|
||||
} else if status.visibility == .private {
|
||||
if mastodonController.instanceFeatures.boostToOriginalAudience,
|
||||
status.account.id == mastodonController.account?.id {
|
||||
return true
|
||||
|
@ -212,6 +205,30 @@ extension StatusCollectionViewCell {
|
|||
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) {
|
||||
if prev {
|
||||
if let prevThreadLinkView {
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UIView {
|
||||
protocol StatusContentPollView: UIView {
|
||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat
|
||||
}
|
||||
|
||||
class StatusContentContainer<ContentView: ContentTextView, PollView: StatusContentPollView>: UIView {
|
||||
|
||||
private var useTopSpacer = false
|
||||
private let topSpacer = UIView().configure {
|
||||
|
@ -25,9 +29,10 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UI
|
|||
$0.isSelectable = false
|
||||
}
|
||||
|
||||
private static var cardViewHeight: CGFloat { 90 }
|
||||
let cardView = StatusCardView().configure {
|
||||
NSLayoutConstraint.activate([
|
||||
$0.heightAnchor.constraint(equalToConstant: 90),
|
||||
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight),
|
||||
])
|
||||
}
|
||||
|
||||
|
@ -125,11 +130,30 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: UIView>: UI
|
|||
}
|
||||
isCollapsed = collapsed
|
||||
|
||||
// ensure that we have a lastSubviewBottomConstraint
|
||||
updateConstraintsIfNeeded()
|
||||
// force unwrap because the content container should always have at least one view
|
||||
lastSubviewBottomConstraint!.isActive = !collapsed
|
||||
// don't call setNeedsUpdateConstraints b/c that destroys/recreates a bunch of other constraints
|
||||
// if there is no lastSubviewBottomConstraint, then we already need a constraint update, so we don't need to do anything here
|
||||
if let lastSubviewBottomConstraint {
|
||||
lastSubviewBottomConstraint.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
|
||||
}
|
||||
|
||||
let displayNameLabel = EmojiLabel().configure {
|
||||
let displayNameLabel = AccountDisplayNameLabel().configure {
|
||||
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
||||
.traits: [
|
||||
UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue,
|
||||
|
@ -238,13 +238,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var favoriteButton = UIButton().configure {
|
||||
private(set) lazy var favoriteButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
||||
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var reblogButton = UIButton().configure {
|
||||
private(set) lazy var reblogButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
|
||||
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
|
@ -619,10 +619,15 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
metaIndicatorsView.updateUI(status: status)
|
||||
|
||||
timelineReasonIcon.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.timelineReasonIconSize
|
||||
if let rebloggerID,
|
||||
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
||||
updateRebloggerLabel(reblogger: reblogger)
|
||||
}
|
||||
|
||||
func updateStatusState(status: StatusMO) {
|
||||
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() {
|
||||
|
@ -694,6 +699,11 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
|
||||
updateUIForPreferences(status: status)
|
||||
|
||||
if let rebloggerID,
|
||||
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
||||
updateRebloggerLabel(reblogger: reblogger)
|
||||
}
|
||||
|
||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||
updateGrayscaleableUI(status: status)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// 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