Compare commits

..

No commits in common. "2c8ba878b78d7680369f7db111ba1723dc0bf856" and "102fe6ed912165950295d8083aa435b540d6650b" have entirely different histories.

25 changed files with 124 additions and 173 deletions

View File

@ -29,10 +29,18 @@ public class Attachment: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id) self.id = try container.decode(String.self, forKey: .id)
self.kind = try container.decode(Kind.self, forKey: .kind) self.kind = try container.decode(Kind.self, forKey: .kind)
self.url = try container.decode(URL.self, forKey: .url) self.url = URL(string: try container.decode(String.self, forKey: .url))!
self.previewURL = try container.decode(URL.self, forKey: .previewURL) self.previewURL = URL(string: try container.decode(String.self, forKey: .previewURL))!
self.remoteURL = try? container.decode(URL.self, forKey: .remoteURL) if let remote = try? container.decode(String.self, forKey: .remoteURL) {
self.textURL = try? container.decode(URL.self, forKey: .textURL) self.remoteURL = URL(string: remote)!
} else {
self.remoteURL = nil
}
if let text = try? container.decode(String.self, forKey: .textURL) {
self.textURL = URL(string: text)!
} else {
self.textURL = nil
}
self.meta = try? container.decode(Metadata.self, forKey: .meta) self.meta = try? container.decode(Metadata.self, forKey: .meta)
self.description = try? container.decode(String.self, forKey: .description) self.description = try? container.decode(String.self, forKey: .description)
} }

View File

@ -33,9 +33,9 @@ class MastodonController {
private(set) lazy var cache = MastodonCache(mastodonController: self) private(set) lazy var cache = MastodonCache(mastodonController: self)
private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: self) private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: self)
let instanceURL: URL let instanceURL: URL
var accountInfo: LocalData.UserAccountInfo? private(set) var accountInfo: LocalData.UserAccountInfo?
let client: Client! let client: Client!

View File

@ -58,7 +58,7 @@ extension AccountMO {
} }
self.acct = account.acct self.acct = account.acct
self.avatar = account.avatarStatic // we don't animate avatars self.avatar = account.avatar
self.bot = account.bot ?? false self.bot = account.bot ?? false
self.createdAt = account.createdAt self.createdAt = account.createdAt
self.displayName = account.displayName self.displayName = account.displayName
@ -66,7 +66,7 @@ extension AccountMO {
self.fields = account.fields ?? [] self.fields = account.fields ?? []
self.followersCount = account.followersCount self.followersCount = account.followersCount
self.followingCount = account.followingCount self.followingCount = account.followingCount
self.header = account.headerStatic // we don't animate headers self.header = account.header
self.id = account.id self.id = account.id
self.locked = account.locked self.locked = account.locked
self.moved = account.moved ?? false self.moved = account.moved ?? false

View File

@ -37,33 +37,19 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
} }
private func upsert(status: Status) {
if let statusMO = self.status(for: status.id, in: self.backgroundContext) {
statusMO.updateFrom(apiStatus: status, container: self)
} else {
_ = StatusMO(apiStatus: status, container: self, context: self.backgroundContext)
}
}
func addOrUpdate(status: Status, save: Bool = true) { func addOrUpdate(status: Status, save: Bool = true) {
backgroundContext.perform { backgroundContext.perform {
self.upsert(status: status) if let statusMO = self.status(for: status.id, in: self.backgroundContext) {
statusMO.updateFrom(apiStatus: status, container: self)
} else {
_ = StatusMO(apiStatus: status, container: self, context: self.backgroundContext)
}
if save, self.backgroundContext.hasChanges { if save, self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
} }
} }
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
backgroundContext.perform {
statuses.forEach(self.upsert(status:))
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
completion?()
}
}
func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? { func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? {
let context = context ?? viewContext let context = context ?? viewContext
let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest() let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest()
@ -76,31 +62,17 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
} }
private func upsert(account: Account) {
if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self)
} else {
_ = AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
}
}
func addOrUpdate(account: Account, save: Bool = true) { func addOrUpdate(account: Account, save: Bool = true) {
backgroundContext.perform { backgroundContext.perform {
self.upsert(account: account) if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self)
} else {
_ = AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
}
if save, self.backgroundContext.hasChanges { if save, self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
} }
} }
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
backgroundContext.perform {
accounts.forEach(self.upsert(account:))
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
completion?()
}
}
} }

View File

@ -36,7 +36,6 @@ public final class StatusMO: NSManagedObject {
@NSManaged public var reblogged: Bool @NSManaged public var reblogged: Bool
@NSManaged public var reblogsCount: Int @NSManaged public var reblogsCount: Int
@NSManaged public var sensitive: Bool @NSManaged public var sensitive: Bool
@NSManaged public var spoilerText: String
@NSManaged public var uri: String // todo: are both uri and url necessary? @NSManaged public var uri: String // todo: are both uri and url necessary?
@NSManaged public var url: URL? @NSManaged public var url: URL?
@NSManaged private var visibilityString: String @NSManaged private var visibilityString: String
@ -96,7 +95,6 @@ extension StatusMO {
self.reblogged = status.reblogged ?? false self.reblogged = status.reblogged ?? false
self.reblogsCount = status.reblogsCount self.reblogsCount = status.reblogsCount
self.sensitive = status.sensitive self.sensitive = status.sensitive
self.spoilerText = status.spoilerText
self.uri = status.uri self.uri = status.uri
self.visibility = status.visibility self.visibility = status.visibility
self.account = container.account(for: status.account.id, in: context) ?? AccountMO(apiAccount: status.account, container: container, context: context) self.account = container.account(for: status.account.id, in: context) ?? AccountMO(apiAccount: status.account, container: container, context: context)

View File

@ -44,7 +44,6 @@
<attribute name="reblogged" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="reblogged" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="spoilerText" attributeType="String"/>
<attribute name="uri" attributeType="String"/> <attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/> <attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="visibilityString" attributeType="String"/> <attribute name="visibilityString" attributeType="String"/>
@ -58,6 +57,6 @@
</entity> </entity>
<elements> <elements>
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="313"/> <element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="313"/>
<element name="Status" positionX="-63" positionY="-18" width="128" height="403"/> <element name="Status" positionX="-63" positionY="-18" width="128" height="388"/>
</elements> </elements>
</model> </model>

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import Pachyderm import Pachyderm
extension AccountMO { extension Account {
var displayOrUserName: String { var displayOrUserName: String {
if displayName.isEmpty { if displayName.isEmpty {
@ -31,7 +31,7 @@ extension AccountMO {
private func stripCustomEmoji(from string: String) -> String { private func stripCustomEmoji(from string: String) -> String {
let range = NSRange(location: 0, length: string.utf16.count) let range = NSRange(location: 0, length: string.utf16.count)
return AccountMO.customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "") return Account.customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
} }
} }

View File

@ -11,7 +11,6 @@ import Foundation
private let decoder = PropertyListDecoder() private let decoder = PropertyListDecoder()
private let encoder = PropertyListEncoder() private let encoder = PropertyListEncoder()
// todo: invalidate cache on underlying data change using KVO?
@propertyWrapper @propertyWrapper
struct LazilyDecoding<Enclosing, Value: Codable> { struct LazilyDecoding<Enclosing, Value: Codable> {

View File

@ -44,10 +44,11 @@ class LocalData: ObservableObject {
let url = URL(string: instanceURL), let url = URL(string: instanceURL),
let clientId = info["clientID"], let clientId = info["clientID"],
let secret = info["clientSecret"], let secret = info["clientSecret"],
let username = info["username"],
let accessToken = info["accessToken"] else { let accessToken = info["accessToken"] else {
return nil return nil
} }
return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: info["username"], accessToken: accessToken) return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: username, accessToken: accessToken)
} }
} else { } else {
return [] return []
@ -55,18 +56,15 @@ class LocalData: ObservableObject {
} }
set { set {
objectWillChange.send() objectWillChange.send()
let array = newValue.map { (info) -> [String: String] in let array = newValue.map { (info) in
var res = [ return [
"id": info.id, "id": info.id,
"instanceURL": info.instanceURL.absoluteString, "instanceURL": info.instanceURL.absoluteString,
"clientID": info.clientID, "clientID": info.clientID,
"clientSecret": info.clientSecret, "clientSecret": info.clientSecret,
"username": info.username,
"accessToken": info.accessToken "accessToken": info.accessToken
] ]
if let username = info.username {
res["username"] = username
}
return res
} }
defaults.set(array, forKey: accountsKey) defaults.set(array, forKey: accountsKey)
} }
@ -87,7 +85,7 @@ class LocalData: ObservableObject {
return !accounts.isEmpty return !accounts.isEmpty
} }
func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String?, accessToken: String) -> UserAccountInfo { func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String, accessToken: String) -> UserAccountInfo {
var accounts = self.accounts var accounts = self.accounts
if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) { if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
accounts.remove(at: index) accounts.remove(at: index)
@ -99,13 +97,6 @@ class LocalData: ObservableObject {
return info return info
} }
func setUsername(for info: UserAccountInfo, username: String) {
var info = info
info.username = username
removeAccount(info)
accounts.append(info)
}
func removeAccount(_ info: UserAccountInfo) { func removeAccount(_ info: UserAccountInfo) {
accounts.removeAll(where: { $0.id == info.id }) accounts.removeAll(where: { $0.id == info.id })
} }
@ -137,7 +128,7 @@ extension LocalData {
let instanceURL: URL let instanceURL: URL
let clientID: String let clientID: String
let clientSecret: String let clientSecret: String
fileprivate(set) var username: String! let username: String
let accessToken: String let accessToken: String
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {

View File

@ -64,7 +64,12 @@ class MastodonCache {
func addAll(statuses: [Status]) { func addAll(statuses: [Status]) {
statuses.forEach(add) statuses.forEach(add)
mastodonController?.persistentContainer.addAll(statuses: statuses) if let container = mastodonController?.persistentContainer {
statuses.forEach { container.addOrUpdate(status: $0, save: false) }
container.backgroundContext.perform {
try! container.backgroundContext.save()
}
}
} }
// MARK: - Accounts // MARK: - Accounts
@ -99,7 +104,12 @@ class MastodonCache {
func addAll(accounts: [Account]) { func addAll(accounts: [Account]) {
accounts.forEach(add) accounts.forEach(add)
mastodonController?.persistentContainer.addAll(accounts: accounts) if let container = mastodonController?.persistentContainer {
accounts.forEach { container.addOrUpdate(account: $0, save: false) }
container.backgroundContext.perform {
try! container.backgroundContext.save()
}
}
} }
// MARK: - Relationships // MARK: - Relationships

View File

@ -213,8 +213,7 @@ class ComposeViewController: UIViewController {
replyAvatarImageViewTopConstraint!.isActive = true replyAvatarImageViewTopConstraint!.isActive = true
inReplyToContainer.isHidden = false inReplyToContainer.isHidden = false
// todo: update to use managed objects inReplyToLabel.text = "In reply to \(inReplyTo.account.displayOrUserName)"
inReplyToLabel.text = "In reply to \(inReplyTo.account.displayName)"
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {

View File

@ -68,13 +68,10 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
let authCode = item.value else { return } let authCode = item.value else { return }
mastodonController.authorize(authorizationCode: authCode) { (accessToken) in mastodonController.authorize(authorizationCode: authCode) { (accessToken) in
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
mastodonController.accountInfo = accountInfo
mastodonController.getOwnAccount { (account) in mastodonController.getOwnAccount { (account) in
LocalData.shared.setUsername(for: accountInfo, username: account.username)
DispatchQueue.main.async { DispatchQueue.main.async {
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo) self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
} }
} }

View File

@ -128,7 +128,7 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
guard let accountID = accountID, let account = mastodonController.persistentContainer.account(for: accountID) else { return } guard let accountID = accountID, let account = mastodonController.cache.account(for: accountID) else { return }
navigationItem.title = account.displayNameWithoutCustomEmoji navigationItem.title = account.displayNameWithoutCustomEmoji
} }

View File

@ -59,15 +59,12 @@ class TimelineTableViewController: EnhancedTableViewController {
let request = Client.getStatuses(timeline: timeline) let request = Client.getStatuses(timeline: timeline)
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(statuses, pagination) = response else { fatalError() } guard case let .success(statuses, pagination) = response else { fatalError() }
// self.mastodonController.cache.addAll(statuses: statuses) self.mastodonController.cache.addAll(statuses: statuses)
// todo: possible race condition here? we update the underlying data before waiting to reload the table view
self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0) self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0)
self.newer = pagination?.newer self.newer = pagination?.newer
self.older = pagination?.older self.older = pagination?.older
self.mastodonController.persistentContainer.addAll(statuses: statuses) { DispatchQueue.main.async {
DispatchQueue.main.async { self.tableView.reloadData()
self.tableView.reloadData()
}
} }
} }
} }
@ -104,15 +101,13 @@ class TimelineTableViewController: EnhancedTableViewController {
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.older = pagination?.older self.older = pagination?.older
// self.mastodonController.cache.addAll(statuses: newStatuses) self.mastodonController.cache.addAll(statuses: newStatuses)
let newRows = self.timelineSegments.last!.count..<(self.timelineSegments.last!.count + newStatuses.count) let newRows = self.timelineSegments.last!.count..<(self.timelineSegments.last!.count + newStatuses.count)
let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.timelineSegments.count - 1) } let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.timelineSegments.count - 1) }
self.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) }) self.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) { DispatchQueue.main.async {
DispatchQueue.main.async { UIView.performWithoutAnimation {
UIView.performWithoutAnimation { self.tableView.insertRows(at: newIndexPaths, with: .none)
self.tableView.insertRows(at: newIndexPaths, with: .none)
}
} }
} }
} }
@ -138,27 +133,25 @@ class TimelineTableViewController: EnhancedTableViewController {
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.newer = pagination?.newer self.newer = pagination?.newer
// self.mastodonController.cache.addAll(statuses: newStatuses) self.mastodonController.cache.addAll(statuses: newStatuses)
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
if let newer = pagination?.newer { if let newer = pagination?.newer {
self.newer = newer self.newer = newer
} }
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) { DispatchQueue.main.async {
DispatchQueue.main.async { let newIndexPaths = (0..<newStatuses.count).map {
let newIndexPaths = (0..<newStatuses.count).map { IndexPath(row: $0, section: 0)
IndexPath(row: $0, section: 0)
}
UIView.performWithoutAnimation {
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
}
self.refreshControl?.endRefreshing()
// maintain the current position in the list (don't scroll to the top)
self.tableView.scrollToRow(at: IndexPath(row: newStatuses.count, section: 0), at: .top, animated: false)
} }
UIView.performWithoutAnimation {
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
}
self.refreshControl?.endRefreshing()
// maintain the current position in the list (don't scroll to the top)
self.tableView.scrollToRow(at: IndexPath(row: newStatuses.count, section: 0), at: .top, animated: false)
} }
} }
} }
@ -182,7 +175,7 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
extension TimelineTableViewController: UITableViewDataSourcePrefetching { extension TimelineTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue } guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue }
_ = ImageCache.avatars.get(status.account.avatar, completion: nil) _ = ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments { for attachment in status.attachments {
_ = ImageCache.attachments.get(attachment.url, completion: nil) _ = ImageCache.attachments.get(attachment.url, completion: nil)
@ -192,7 +185,7 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue } guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue }
ImageCache.avatars.cancelWithoutCallback(status.account.avatar) ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.cancelWithoutCallback(attachment.url) ImageCache.attachments.cancelWithoutCallback(attachment.url)

View File

@ -32,13 +32,12 @@ class UserActivityManager {
// MARK: - New Post // MARK: - New Post
static func newPostActivity(mentioning: Account? = nil) -> NSUserActivity { static func newPostActivity(mentioning: Account? = nil) -> NSUserActivity {
// todo: update to use managed objects
let activity = NSUserActivity(type: .newPost) let activity = NSUserActivity(type: .newPost)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
if let mentioning = mentioning { if let mentioning = mentioning {
activity.userInfo = ["mentioning": mentioning.acct] activity.userInfo = ["mentioning": mentioning.acct]
activity.title = "Send a message to \(mentioning.displayName)" activity.title = "Send a message to \(mentioning.displayOrUserName)"
activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayName)" activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayOrUserName)"
} else { } else {
activity.userInfo = [:] activity.userInfo = [:]
activity.title = "New Post" activity.title = "New Post"

View File

@ -48,7 +48,7 @@ class AttachmentsContainerView: UIView {
// MARK: - User Interaface // MARK: - User Interaface
func updateUI(status: StatusMO) { func updateUI(status: Status) {
self.statusID = status.id self.statusID = status.id
attachments = status.attachments.filter { AttachmentsContainerView.supportedAttachmentTypes.contains($0.kind) } attachments = status.attachments.filter { AttachmentsContainerView.supportedAttachmentTypes.contains($0.kind) }

View File

@ -83,15 +83,6 @@ class EmojiLabel: UILabel {
extension EmojiLabel { extension EmojiLabel {
func updateForAccountDisplayName(account: Account) { 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 { if Preferences.shared.hideCustomEmojiInUsernames {
self.text = account.displayNameWithoutCustomEmoji self.text = account.displayNameWithoutCustomEmoji
self.removeEmojis() self.removeEmojis()

View File

@ -138,14 +138,13 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
let peopleStr: String let peopleStr: String
// todo: figure out how to localize this // todo: figure out how to localize this
// todo: update to use managed objects
switch people.count { switch people.count {
case 1: case 1:
peopleStr = people.first!.displayName peopleStr = people.first!.displayOrUserName
case 2: case 2:
peopleStr = people.first!.displayName + " and " + people.last!.displayName peopleStr = people.first!.displayOrUserName + " and " + people.last!.displayOrUserName
default: default:
peopleStr = people.dropLast().map { $0.displayName }.joined(separator: ", ") + ", and " + people.last!.displayName peopleStr = people.dropLast().map { $0.displayOrUserName }.joined(separator: ", ") + ", and " + people.last!.displayOrUserName
} }
actionLabel.text = "\(verb) by \(peopleStr)" actionLabel.text = "\(verb) by \(peopleStr)"
} }

View File

@ -72,16 +72,15 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
} }
func updateActionLabel(people: [Account]) { func updateActionLabel(people: [Account]) {
// todo: update to use managed objects
// todo: figure out how to localize this // todo: figure out how to localize this
let peopleStr: String let peopleStr: String
switch people.count { switch people.count {
case 1: case 1:
peopleStr = people.first!.displayName peopleStr = people.first!.displayOrUserName
case 2: case 2:
peopleStr = people.first!.displayName + " and " + people.last!.displayName peopleStr = people.first!.displayOrUserName + " and " + people.last!.displayOrUserName
default: default:
peopleStr = people.dropLast().map { $0.displayName }.joined(separator: ", ") + ", and " + people.last!.displayName peopleStr = people.dropLast().map { $0.displayOrUserName }.joined(separator: ", ") + ", and " + people.last!.displayOrUserName
} }
actionLabel.text = "Followed by \(peopleStr)" actionLabel.text = "Followed by \(peopleStr)"

View File

@ -52,13 +52,12 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
} }
func updateUI(account: Account) { func updateUI(account: Account) {
// todo: update to use managed objects
self.account = account self.account = account
if Preferences.shared.hideCustomEmojiInUsernames { if Preferences.shared.hideCustomEmojiInUsernames {
actionLabel.text = "Request to follow from \(account.displayName)" actionLabel.text = "Request to follow from \(account.displayNameWithoutCustomEmoji)"
actionLabel.removeEmojis() actionLabel.removeEmojis()
} else { } else {
actionLabel.text = "Request to follow from \(account.displayName)" actionLabel.text = "Request to follow from \(account.displayOrUserName)"
actionLabel.setEmojis(account.emojis, identifier: account.id) actionLabel.setEmojis(account.emojis, identifier: account.id)
} }
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in

View File

@ -95,26 +95,25 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
open func createObserversIfNecessary() { open func createObserversIfNecessary() {
// todo: KVO on StatusMO for this? if statusUpdater == nil {
// if statusUpdater == nil { statusUpdater = mastodonController.cache.statusSubject
// statusUpdater = mastodonController.cache.statusSubject .filter { [unowned self] in $0.id == self.statusID }
// .filter { [unowned self] in $0.id == self.statusID } .receive(on: DispatchQueue.main)
// .receive(on: DispatchQueue.main) .sink { [unowned self] in self.updateStatusState(status: $0) }
// .sink { [unowned self] in self.updateStatusState(status: $0) } }
// }
// if accountUpdater == nil {
// if accountUpdater == nil { accountUpdater = mastodonController.cache.accountSubject
// accountUpdater = mastodonController.cache.accountSubject .filter { [unowned self] in $0.id == self.accountID }
// .filter { [unowned self] in $0.id == self.accountID } .receive(on: DispatchQueue.main)
// .receive(on: DispatchQueue.main) .sink { [unowned self] in self.updateUI(account: $0) }
// .sink { [unowned self] in self.updateUI(account: $0) } }
// }
} }
func updateUI(statusID: String, state: StatusState) { func updateUI(statusID: String, state: StatusState) {
createObserversIfNecessary() createObserversIfNecessary()
guard let status = mastodonController.persistentContainer.status(for: statusID) else { guard let status = mastodonController.cache.status(for: statusID) else {
fatalError("Missing cached status") fatalError("Missing cached status")
} }
self.statusID = statusID self.statusID = statusID
@ -162,9 +161,9 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
} }
func updateStatusState(status: StatusMO) { func updateStatusState(status: Status) {
favorited = status.favourited favorited = status.favourited ?? false
reblogged = status.reblogged reblogged = status.reblogged ?? false
if favorited { if favorited {
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label") favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
@ -178,22 +177,22 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
} }
func updateUI(account: AccountMO) { func updateUI(account: Account) {
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
avatarImageView.image = nil avatarImageView.image = nil
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
guard let self = self, let data = data, self.accountID == account.id else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
guard let self = self, let data = data, self.accountID == account.id else { return }
self.avatarImageView.image = UIImage(data: data) self.avatarImageView.image = UIImage(data: data)
} }
} }
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
guard let mastodonController = mastodonController, let account = mastodonController.persistentContainer.account(for: accountID) else { return } guard let mastodonController = mastodonController, let account = mastodonController.cache.account(for: accountID) else { return }
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
displayNameLabel.updateForAccountDisplayName(account: account) displayNameLabel.updateForAccountDisplayName(account: account)
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false) attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.cache.status(for: statusID)?.sensitive ?? false)
} }
override func prepareForReuse() { override func prepareForReuse() {
@ -312,7 +311,7 @@ class BaseStatusTableViewCell: UITableViewCell {
extension BaseStatusTableViewCell: AttachmentViewDelegate { extension BaseStatusTableViewCell: AttachmentViewDelegate {
func attachmentViewGallery(startingAt index: Int) -> UIViewController { func attachmentViewGallery(startingAt index: Int) -> UIViewController {
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
return delegate!.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index) return delegate!.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
} }

View File

@ -49,7 +49,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
timestampAndClientLabel.text = timestampAndClientText timestampAndClientLabel.text = timestampAndClientText
} }
override func updateStatusState(status: StatusMO) { override func updateStatusState(status: Status) {
super.updateStatusState(status: status) super.updateStatusState(status: status)
// todo: localize me // todo: localize me
@ -57,7 +57,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
totalReblogsButton.setTitle("\(status.reblogsCount) Reblog\(status.reblogsCount == 1 ? "" : "s")", for: .normal) totalReblogsButton.setTitle("\(status.reblogsCount) Reblog\(status.reblogsCount == 1 ? "" : "s")", for: .normal)
} }
override func updateUI(account: AccountMO) { override func updateUI(account: Account) {
super.updateUI(account: account) super.updateUI(account: account)
profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji

View File

@ -47,20 +47,20 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
override func createObserversIfNecessary() { override func createObserversIfNecessary() {
super.createObserversIfNecessary() super.createObserversIfNecessary()
// todo: use KVO on reblogger account? if rebloggerAccountUpdater == nil {
// if rebloggerAccountUpdater == nil { rebloggerAccountUpdater = mastodonController.cache.accountSubject
// rebloggerAccountUpdater = mastodonController.cache.accountSubject .filter { [unowned self] in $0.id == self.rebloggerID }
// .filter { [unowned self] in $0.id == self.rebloggerID } .receive(on: DispatchQueue.main)
// .receive(on: DispatchQueue.main) .sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) }
// .sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) } }
// }
} }
override func updateUI(statusID: String, state: StatusState) { override func updateUI(statusID: String, state: StatusState) {
guard var status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard var status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
let realStatusID: String let realStatusID: String
if let rebloggedStatus = status.reblog { if let rebloggedStatusID = status.reblog?.id,
let rebloggedStatus = mastodonController.cache.status(for: rebloggedStatusID) {
reblogStatusID = statusID reblogStatusID = statusID
rebloggerID = status.account.id rebloggerID = status.account.id
status = rebloggedStatus status = rebloggedStatus
@ -77,7 +77,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
updateTimestamp() updateTimestamp()
let pinned = status.pinned let pinned = status.pinned ?? false
pinImageView.isHidden = !(pinned && showPinned) pinImageView.isHidden = !(pinned && showPinned)
timestampLabel.isHidden = !pinImageView.isHidden timestampLabel.isHidden = !pinImageView.isHidden
} }
@ -85,12 +85,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
@objc override func updateUIForPreferences() { @objc override func updateUIForPreferences() {
super.updateUIForPreferences() super.updateUIForPreferences()
if let rebloggerID = rebloggerID, if let rebloggerID = rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) { let reblogger = mastodonController.cache.account(for: rebloggerID) {
updateRebloggerLabel(reblogger: reblogger) updateRebloggerLabel(reblogger: reblogger)
} }
} }
private func updateRebloggerLabel(reblogger: AccountMO) { private func updateRebloggerLabel(reblogger: Account) {
if Preferences.shared.hideCustomEmojiInUsernames { if Preferences.shared.hideCustomEmojiInUsernames {
reblogLabel.text = "Reblogged by \(reblogger.displayNameWithoutCustomEmoji)" reblogLabel.text = "Reblogged by \(reblogger.displayNameWithoutCustomEmoji)"
reblogLabel.removeEmojis() reblogLabel.removeEmojis()
@ -104,7 +104,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated // if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated
// so we bail out immediately, since there's nothing to update // so we bail out immediately, since there's nothing to update
guard let mastodonController = mastodonController else { return } guard let mastodonController = mastodonController else { return }
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
timestampLabel.text = status.createdAt.timeAgoString() timestampLabel.text = status.createdAt.timeAgoString()
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date()) timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())

View File

@ -15,7 +15,7 @@ class StatusContentTextView: ContentTextView {
didSet { didSet {
guard let statusID = statusID else { return } guard let statusID = statusID else { return }
guard let mastodonController = mastodonController, guard let mastodonController = mastodonController,
let status = mastodonController.persistentContainer.status(for: statusID) else { let status = mastodonController.cache.status(for: statusID) else {
fatalError("Can't set StatusContentTextView text without cached status for \(statusID)") fatalError("Can't set StatusContentTextView text without cached status for \(statusID)")
} }
setTextFromHtml(status.content) setTextFromHtml(status.content)

View File

@ -314,8 +314,7 @@ struct XCBActions {
DispatchQueue.main.async { DispatchQueue.main.async {
show(vc) show(vc)
} }
// todo: update to use managed objects let alertController = UIAlertController(title: "Follow \(account.displayNameWithoutCustomEmoji)?", message: nil, preferredStyle: .alert)
let alertController = UIAlertController(title: "Follow \(account.displayName)?", message: nil, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
performAction(account) performAction(account)
})) }))