Compare commits

..

4 Commits

4 changed files with 44 additions and 47 deletions

View File

@ -26,10 +26,17 @@ public class Hashtag: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name) self.name = try container.decode(String.self, forKey: .name)
// pixelfed (possibly others) don't fully escape special characters in the hashtag url // pixelfed (possibly others) don't fully escape special characters in the hashtag url
if let url = URL(try container.decode(WebURL.self, forKey: .url)) { do {
let webURL = try container.decode(WebURL.self, forKey: .url)
if let url = URL(webURL) {
self.url = url self.url = url
} else { } else {
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "unable to convert WebURL to URL") let s = try? container.decode(String.self, forKey: .url)
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "unable to convert WebURL \(s?.debugDescription ?? "nil") to URL")
}
} catch {
let s = try? container.decode(String.self, forKey: .url)
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "unable to decode WebURL from \(s?.debugDescription ?? "nil")")
} }
self.history = try container.decodeIfPresent([History].self, forKey: .history) self.history = try container.decodeIfPresent([History].self, forKey: .history)
} }

View File

@ -78,6 +78,14 @@ class ImageCache {
} }
} }
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
return await withCheckedContinuation { continuation in
_ = get(url, loadOriginal: loadOriginal) { data, image in
continuation.resume(returning: (data, image))
}
}
}
func fetchIfNotCached(_ url: URL) { func fetchIfNotCached(_ url: URL) {
// if caching is disabled, don't bother fetching since nothing will be done with the result // if caching is disabled, don't bother fetching since nothing will be done with the result
guard !ImageCache.disableCaching else { return } guard !ImageCache.disableCaching else { return }

View File

@ -74,7 +74,7 @@ class PostService: ObservableObject {
throw Error.attachmentData(index: index, cause: error) throw Error.attachmentData(index: index, cause: error)
} }
do { do {
let uploaded = try await uploadAttachment(data: data, mimeType: mimeType, description: attachment.description) let uploaded = try await uploadAttachment(data: data, mimeType: mimeType, description: attachment.attachmentDescription)
attachments.append(uploaded) attachments.append(uploaded)
currentStep += 1 currentStep += 1
} catch let error as Client.Error { } catch let error as Client.Error {

View File

@ -25,7 +25,6 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
var group: NotificationGroup! var group: NotificationGroup!
var statusID: String! var statusID: String!
private var avatarRequests = [String: ImageCache.Request]()
private var updateTimestampWorkItem: DispatchWorkItem? private var updateTimestampWorkItem: DispatchWorkItem?
private var isGrayscale = false private var isGrayscale = false
@ -47,7 +46,9 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
if isGrayscale != Preferences.shared.grayscaleImages { if isGrayscale != Preferences.shared.grayscaleImages {
updateGrayscaleableUI() Task {
await updateGrayscaleableUI()
}
} }
} }
@ -74,40 +75,26 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
isGrayscale = Preferences.shared.grayscaleImages isGrayscale = Preferences.shared.grayscaleImages
updateTimestamp()
let timestampLabelSize = timestampLabel.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: timestampLabel.bounds.height))
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) } let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
var imageViews = [UIImageView]() var imageViews = [UIImageView]()
for account in people { for _ in people {
let imageView = UIImageView() let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
if let avatarURL = account.avatar {
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self else { return }
guard let image = image,
self.group.id == group.id,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
}
return
}
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
imageView.image = transformedImage
}
}
}
actionAvatarStackView.addArrangedSubview(imageView) actionAvatarStackView.addArrangedSubview(imageView)
imageViews.append(imageView) imageViews.append(imageView)
// don't add more avatars if they would overflow or squeeze the timestamp label // don't add more avatars if they would overflow or squeeze the timestamp label
let avatarViewsWidth = 30 * CGFloat(imageViews.count) let avatarViewsWidth = 30 * CGFloat(imageViews.count)
let avatarMarginsWidth = 4 * CGFloat(max(0, imageViews.count - 1)) let avatarMarginsWidth = 4 * CGFloat(max(0, imageViews.count - 1))
let maxAvatarStackWidth = verticalStackView.bounds.width - timestampLabel.bounds.width - 8 // todo: when the cell is first created, verticalStackView.bounds.width is not correct
let maxAvatarStackWidth = verticalStackView.bounds.width - timestampLabelSize.width - 8
let remainingWidth = maxAvatarStackWidth - avatarViewsWidth - avatarMarginsWidth let remainingWidth = maxAvatarStackWidth - avatarViewsWidth - avatarMarginsWidth
if remainingWidth < 34 { if remainingWidth < 34 {
break break
@ -115,43 +102,39 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
NSLayoutConstraint.activate(imageViews.map { $0.widthAnchor.constraint(equalTo: $0.heightAnchor) }) NSLayoutConstraint.activate(imageViews.map { $0.widthAnchor.constraint(equalTo: $0.heightAnchor) })
updateTimestamp() Task {
await updateGrayscaleableUI()
}
actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id) actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id)
let doc = try! SwiftSoup.parse(status.content) let doc = try! SwiftSoup.parse(status.content)
statusContentLabel.text = try! doc.text() statusContentLabel.text = try! doc.text()
} }
private func updateGrayscaleableUI() { @MainActor
private func updateGrayscaleableUI() async {
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) } let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
let groupID = group.id let groupID = group.id
for (index, account) in people.enumerated() { for (index, account) in people.enumerated() {
guard actionAvatarStackView.arrangedSubviews.count > index, guard actionAvatarStackView.arrangedSubviews.count > index,
let imageView = actionAvatarStackView.arrangedSubviews[index] as? UIImageView else { let imageView = actionAvatarStackView.arrangedSubviews[index] as? UIImageView,
let avatarURL = account.avatar else {
continue continue
} }
if let avatarURL = account.avatar { Task {
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in let (_, image) = await ImageCache.avatars.get(avatarURL)
guard let self = self else { return }
guard let image = image, guard let image = image,
self.group.id == groupID, self.group.id == groupID,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
}
return return
} }
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
imageView.image = transformedImage imageView.image = transformedImage
} }
} }
} }
}
}
private func updateTimestamp() { private func updateTimestamp() {
guard let notification = group.notifications.first else { guard let notification = group.notifications.first else {
@ -217,7 +200,6 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
avatarRequests.values.forEach { $0.cancel() }
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil updateTimestampWorkItem = nil
} }