Compare commits
No commits in common. "dbdf1d39bd579ff1c72e95f6ca5ea4d1ee00dd20" and "0f6e9c97ccfc2ad01647085eab4806189cc9859c" have entirely different histories.
dbdf1d39bd
...
0f6e9c97cc
|
@ -1,11 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2022.1 (31)
|
|
||||||
Bugfixes:
|
|
||||||
- Fix not being able to post attachments with descriptions
|
|
||||||
- Fix potential crash when displaying certain notifications
|
|
||||||
- More detailed error message when decoding invalid URLs
|
|
||||||
|
|
||||||
## 2022.1 (30)
|
## 2022.1 (30)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Add fast account switching on iPad
|
- Add fast account switching on iPad
|
||||||
|
|
|
@ -26,17 +26,10 @@ 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
|
||||||
do {
|
if let url = URL(try container.decode(WebURL.self, forKey: .url)) {
|
||||||
let webURL = try container.decode(WebURL.self, forKey: .url)
|
self.url = url
|
||||||
if let url = URL(webURL) {
|
} else {
|
||||||
self.url = url
|
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "unable to convert WebURL to URL")
|
||||||
} else {
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2198,7 +2198,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 31;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2229,7 +2229,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 31;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2339,7 +2339,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 31;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2366,7 +2366,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 31;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
|
|
@ -78,14 +78,6 @@ 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 }
|
||||||
|
|
|
@ -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.attachmentDescription)
|
let uploaded = try await uploadAttachment(data: data, mimeType: mimeType, description: attachment.description)
|
||||||
attachments.append(uploaded)
|
attachments.append(uploaded)
|
||||||
currentStep += 1
|
currentStep += 1
|
||||||
} catch let error as Client.Error {
|
} catch let error as Client.Error {
|
||||||
|
|
|
@ -25,6 +25,7 @@ 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
|
||||||
|
|
||||||
|
@ -46,9 +47,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
Task {
|
updateGrayscaleableUI()
|
||||||
await updateGrayscaleableUI()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,63 +74,81 @@ 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 _ in people {
|
for account 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))
|
||||||
// todo: when the cell is first created, verticalStackView.bounds.width is not correct
|
let maxAvatarStackWidth = verticalStackView.bounds.width - timestampLabel.bounds.width - 8
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NSLayoutConstraint.activate(imageViews.map { $0.widthAnchor.constraint(equalTo: $0.heightAnchor) })
|
NSLayoutConstraint.activate(imageViews.map { $0.widthAnchor.constraint(equalTo: $0.heightAnchor) })
|
||||||
|
|
||||||
Task {
|
|
||||||
await updateGrayscaleableUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
updateTimestamp()
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
private func updateGrayscaleableUI() {
|
||||||
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,
|
let imageView = actionAvatarStackView.arrangedSubviews[index] as? UIImageView else {
|
||||||
let avatarURL = account.avatar else {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
if let avatarURL = account.avatar {
|
||||||
let (_, image) = await ImageCache.avatars.get(avatarURL)
|
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||||
guard let image = image,
|
guard let self = self else { return }
|
||||||
self.group.id == groupID,
|
guard let image = image,
|
||||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
self.group.id == groupID,
|
||||||
return
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
imageView.image = transformedImage
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,6 +217,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue