Compare commits

...

3 Commits

Author SHA1 Message Date
Shadowfacts 2c8ba878b7
Start converting UI to use CoreData backed objects instead of API
objects directly
2020-04-12 12:54:27 -04:00
Shadowfacts a0e95d4577
Remove unnecessary attachment decoding code
For some reason, creating a URL from a string decoded from the container
was producing URL objects that could not be round-tripped through
PropertyListEncoder/Decoder. Decoding a URL directly from the container
works correctly.
2020-04-12 12:52:51 -04:00
Shadowfacts 465aedd43f
Make account info username optional
Onboarding view controller needs to set the account info object on the
mastodon controller before calling getOwnAccount since getOwnAccount
will upsert the user's account into the persistent container, which
requires the account info to exist to create a unique-per-account
identifier.
2020-04-12 11:14:10 -04:00
25 changed files with 173 additions and 124 deletions

View File

@ -29,18 +29,10 @@ 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 = URL(string: try container.decode(String.self, forKey: .url))! self.url = try container.decode(URL.self, forKey: .url)
self.previewURL = URL(string: try container.decode(String.self, forKey: .previewURL))! self.previewURL = try container.decode(URL.self, forKey: .previewURL)
if let remote = try? container.decode(String.self, forKey: .remoteURL) { self.remoteURL = try? container.decode(URL.self, forKey: .remoteURL)
self.remoteURL = URL(string: remote)! self.textURL = try? container.decode(URL.self, forKey: .textURL)
} 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

@ -35,7 +35,7 @@ class MastodonController {
private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: self) private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: self)
let instanceURL: URL let instanceURL: URL
private(set) var accountInfo: LocalData.UserAccountInfo? 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.avatar self.avatar = account.avatarStatic // we don't animate avatars
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.header self.header = account.headerStatic // we don't animate headers
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,19 +37,33 @@ 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 {
if let statusMO = self.status(for: status.id, in: self.backgroundContext) { self.upsert(status: status)
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()
@ -62,17 +76,31 @@ 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 {
if let accountMO = self.account(for: account.id, in: self.backgroundContext) { self.upsert(account: account)
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,6 +36,7 @@ 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
@ -95,6 +96,7 @@ 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,6 +44,7 @@
<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"/>
@ -57,6 +58,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="388"/> <element name="Status" positionX="-63" positionY="-18" width="128" height="403"/>
</elements> </elements>
</model> </model>

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import Pachyderm import Pachyderm
extension Account { extension AccountMO {
var displayOrUserName: String { var displayOrUserName: String {
if displayName.isEmpty { if displayName.isEmpty {
@ -31,7 +31,7 @@ extension Account {
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 Account.customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "") return AccountMO.customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
} }
} }

View File

@ -11,6 +11,7 @@ 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,11 +44,10 @@ 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: username, accessToken: accessToken) return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: info["username"], accessToken: accessToken)
} }
} else { } else {
return [] return []
@ -56,15 +55,18 @@ class LocalData: ObservableObject {
} }
set { set {
objectWillChange.send() objectWillChange.send()
let array = newValue.map { (info) in let array = newValue.map { (info) -> [String: String] in
return [ var res = [
"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)
} }
@ -85,7 +87,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)
@ -97,6 +99,13 @@ 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 })
} }
@ -128,7 +137,7 @@ extension LocalData {
let instanceURL: URL let instanceURL: URL
let clientID: String let clientID: String
let clientSecret: String let clientSecret: String
let username: String fileprivate(set) var username: String!
let accessToken: String let accessToken: String
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {

View File

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

View File

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

View File

@ -68,10 +68,13 @@ 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
mastodonController.getOwnAccount { (account) in let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
DispatchQueue.main.async { mastodonController.accountInfo = accountInfo
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
mastodonController.getOwnAccount { (account) in
LocalData.shared.setUsername(for: accountInfo, username: account.username)
DispatchQueue.main.async {
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.cache.account(for: accountID) else { return } guard let accountID = accountID, let account = mastodonController.persistentContainer.account(for: accountID) else { return }
navigationItem.title = account.displayNameWithoutCustomEmoji navigationItem.title = account.displayNameWithoutCustomEmoji
} }

View File

@ -59,12 +59,15 @@ 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
DispatchQueue.main.async { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
self.tableView.reloadData() DispatchQueue.main.async {
self.tableView.reloadData()
}
} }
} }
} }
@ -101,13 +104,15 @@ 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) })
DispatchQueue.main.async { self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
UIView.performWithoutAnimation { DispatchQueue.main.async {
self.tableView.insertRows(at: newIndexPaths, with: .none) UIView.performWithoutAnimation {
self.tableView.insertRows(at: newIndexPaths, with: .none)
}
} }
} }
} }
@ -133,25 +138,27 @@ 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
} }
DispatchQueue.main.async { self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
let newIndexPaths = (0..<newStatuses.count).map { DispatchQueue.main.async {
IndexPath(row: $0, section: 0) let newIndexPaths = (0..<newStatuses.count).map {
} IndexPath(row: $0, section: 0)
UIView.performWithoutAnimation { }
self.tableView.insertRows(at: newIndexPaths, with: .automatic) UIView.performWithoutAnimation {
} self.tableView.insertRows(at: newIndexPaths, with: .automatic)
}
self.refreshControl?.endRefreshing() self.refreshControl?.endRefreshing()
// maintain the current position in the list (don't scroll to the top) // 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) self.tableView.scrollToRow(at: IndexPath(row: newStatuses.count, section: 0), at: .top, animated: false)
}
} }
} }
} }
@ -175,7 +182,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.cache.status(for: statusID(for: indexPath)) else { continue } guard let status = mastodonController.persistentContainer.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)
@ -185,7 +192,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.cache.status(for: statusID(for: indexPath)) else { continue } guard let status = mastodonController.persistentContainer.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,12 +32,13 @@ 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.displayOrUserName)" activity.title = "Send a message to \(mentioning.displayName)"
activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayOrUserName)" activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayName)"
} 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: Status) { func updateUI(status: StatusMO) {
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,6 +83,15 @@ 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,13 +138,14 @@ 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!.displayOrUserName peopleStr = people.first!.displayName
case 2: case 2:
peopleStr = people.first!.displayOrUserName + " and " + people.last!.displayOrUserName peopleStr = people.first!.displayName + " and " + people.last!.displayName
default: default:
peopleStr = people.dropLast().map { $0.displayOrUserName }.joined(separator: ", ") + ", and " + people.last!.displayOrUserName peopleStr = people.dropLast().map { $0.displayName }.joined(separator: ", ") + ", and " + people.last!.displayName
} }
actionLabel.text = "\(verb) by \(peopleStr)" actionLabel.text = "\(verb) by \(peopleStr)"
} }

View File

@ -72,15 +72,16 @@ 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!.displayOrUserName peopleStr = people.first!.displayName
case 2: case 2:
peopleStr = people.first!.displayOrUserName + " and " + people.last!.displayOrUserName peopleStr = people.first!.displayName + " and " + people.last!.displayName
default: default:
peopleStr = people.dropLast().map { $0.displayOrUserName }.joined(separator: ", ") + ", and " + people.last!.displayOrUserName peopleStr = people.dropLast().map { $0.displayName }.joined(separator: ", ") + ", and " + people.last!.displayName
} }
actionLabel.text = "Followed by \(peopleStr)" actionLabel.text = "Followed by \(peopleStr)"

View File

@ -52,12 +52,13 @@ 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.displayNameWithoutCustomEmoji)" actionLabel.text = "Request to follow from \(account.displayName)"
actionLabel.removeEmojis() actionLabel.removeEmojis()
} else { } else {
actionLabel.text = "Request to follow from \(account.displayOrUserName)" actionLabel.text = "Request to follow from \(account.displayName)"
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,25 +95,26 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
open func createObserversIfNecessary() { open func createObserversIfNecessary() {
if statusUpdater == nil { // todo: KVO on StatusMO for this?
statusUpdater = mastodonController.cache.statusSubject // if statusUpdater == nil {
.filter { [unowned self] in $0.id == self.statusID } // statusUpdater = mastodonController.cache.statusSubject
.receive(on: DispatchQueue.main) // .filter { [unowned self] in $0.id == self.statusID }
.sink { [unowned self] in self.updateStatusState(status: $0) } // .receive(on: DispatchQueue.main)
} // .sink { [unowned self] in self.updateStatusState(status: $0) }
// }
if accountUpdater == nil { //
accountUpdater = mastodonController.cache.accountSubject // if accountUpdater == nil {
.filter { [unowned self] in $0.id == self.accountID } // accountUpdater = mastodonController.cache.accountSubject
.receive(on: DispatchQueue.main) // .filter { [unowned self] in $0.id == self.accountID }
.sink { [unowned self] in self.updateUI(account: $0) } // .receive(on: DispatchQueue.main)
} // .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.cache.status(for: statusID) else { guard let status = mastodonController.persistentContainer.status(for: statusID) else {
fatalError("Missing cached status") fatalError("Missing cached status")
} }
self.statusID = statusID self.statusID = statusID
@ -161,9 +162,9 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
} }
func updateStatusState(status: Status) { func updateStatusState(status: StatusMO) {
favorited = status.favourited ?? false favorited = status.favourited
reblogged = status.reblogged ?? false reblogged = status.reblogged
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")
@ -177,22 +178,22 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
} }
func updateUI(account: Account) { func updateUI(account: AccountMO) {
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.cache.account(for: accountID) else { return } guard let mastodonController = mastodonController, let account = mastodonController.persistentContainer.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.cache.status(for: statusID)?.sensitive ?? false) attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
} }
override func prepareForReuse() { override func prepareForReuse() {
@ -311,7 +312,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.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.persistentContainer.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: Status) { override func updateStatusState(status: StatusMO) {
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: Account) { override func updateUI(account: AccountMO) {
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()
if rebloggerAccountUpdater == nil { // todo: use KVO on reblogger account?
rebloggerAccountUpdater = mastodonController.cache.accountSubject // if rebloggerAccountUpdater == nil {
.filter { [unowned self] in $0.id == self.rebloggerID } // rebloggerAccountUpdater = mastodonController.cache.accountSubject
.receive(on: DispatchQueue.main) // .filter { [unowned self] in $0.id == self.rebloggerID }
.sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) } // .receive(on: DispatchQueue.main)
} // .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.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard var status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
let realStatusID: String let realStatusID: String
if let rebloggedStatusID = status.reblog?.id, if let rebloggedStatus = status.reblog {
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 ?? false let pinned = status.pinned
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.cache.account(for: rebloggerID) { let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
updateRebloggerLabel(reblogger: reblogger) updateRebloggerLabel(reblogger: reblogger)
} }
} }
private func updateRebloggerLabel(reblogger: Account) { private func updateRebloggerLabel(reblogger: AccountMO) {
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.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.persistentContainer.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.cache.status(for: statusID) else { let status = mastodonController.persistentContainer.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,7 +314,8 @@ struct XCBActions {
DispatchQueue.main.async { DispatchQueue.main.async {
show(vc) show(vc)
} }
let alertController = UIAlertController(title: "Follow \(account.displayNameWithoutCustomEmoji)?", message: nil, preferredStyle: .alert) // todo: update to use managed objects
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)
})) }))