Compare commits

..

6 Commits

Author SHA1 Message Date
Shadowfacts 1c9b1b9ac3
Add support (sort of) for gifv attachments
See #98
2020-05-12 21:46:08 -04:00
Shadowfacts 82ad3b9fc4
Add reference counting for accounts
Closes #97
2020-05-11 22:03:17 -04:00
Shadowfacts 0a89dd3041
Don't double update accounts
Adding a status to the cache will also cache the status' account
2020-05-11 18:27:54 -04:00
Shadowfacts 40863ef130
Fix crash when opening more options for status in instance public timeline 2020-05-11 17:58:43 -04:00
Shadowfacts cd78287a87
Fix crash when viewing instance public timelines
Use a CoreData in-memory store for public timelines.
2020-05-11 17:57:50 -04:00
Shadowfacts 04496aca1d
Apply avatar style to local account avatar images 2020-05-10 19:30:19 -04:00
19 changed files with 238 additions and 41 deletions

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public final class Status: StatusProtocol, Decodable { public final class Status: /*StatusProtocol,*/ Decodable {
public let id: String public let id: String
public let uri: String public let uri: String
public let url: URL? public let url: URL?
@ -23,8 +23,8 @@ public final class Status: StatusProtocol, Decodable {
// public let repliesCount: Int // public let repliesCount: Int
public let reblogsCount: Int public let reblogsCount: Int
public let favouritesCount: Int public let favouritesCount: Int
public let reblogged: Bool public let reblogged: Bool?
public let favourited: Bool public let favourited: Bool?
public let muted: Bool? public let muted: Bool?
public let sensitive: Bool public let sensitive: Bool
public let spoilerText: String public let spoilerText: String

View File

@ -134,6 +134,8 @@
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64D8CA92463B494006B0BAA /* CachedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* CachedDictionary.swift */; }; D64D8CA92463B494006B0BAA /* CachedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* CachedDictionary.swift */; };
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; }; D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; };
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; }; D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
@ -428,6 +430,8 @@
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64D8CA82463B494006B0BAA /* CachedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedDictionary.swift; sourceTree = "<group>"; }; D64D8CA82463B494006B0BAA /* CachedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedDictionary.swift; sourceTree = "<group>"; };
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; }; D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = "<group>"; };
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; }; D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -597,6 +601,7 @@
children = ( children = (
04D14BAE22B34A2800642648 /* GalleryViewController.swift */, 04D14BAE22B34A2800642648 /* GalleryViewController.swift */,
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */, D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */,
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */,
); );
path = "Attachment Gallery"; path = "Attachment Gallery";
sourceTree = "<group>"; sourceTree = "<group>";
@ -1216,6 +1221,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */, D6C94D882139E6EC00CB5196 /* AttachmentView.swift */,
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */,
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */, D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */,
); );
path = Attachments; path = Attachments;
@ -1650,6 +1656,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */, D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */,
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */,
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */, D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */, D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
@ -1702,6 +1709,7 @@
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */, D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */, D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */, D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */, D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */, D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,

View File

@ -30,7 +30,8 @@ class MastodonController {
} }
} }
private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: self) private let transient: Bool
private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient)
let instanceURL: URL let instanceURL: URL
var accountInfo: LocalData.UserAccountInfo? var accountInfo: LocalData.UserAccountInfo?
@ -40,10 +41,11 @@ class MastodonController {
var account: Account! var account: Account!
var instance: Instance! var instance: Instance!
init(instanceURL: URL) { init(instanceURL: URL, transient: Bool = false) {
self.instanceURL = instanceURL self.instanceURL = instanceURL
self.accountInfo = nil self.accountInfo = nil
self.client = Client(baseURL: instanceURL) self.client = Client(baseURL: instanceURL)
self.transient = transient
} }
func run<Result>(_ request: Request<Result>, completion: @escaping Client.Callback<Result>) { func run<Result>(_ request: Request<Result>, completion: @escaping Client.Callback<Result>) {
@ -82,11 +84,19 @@ class MastodonController {
run(request) { response in run(request) { response in
guard case let .success(account, _) = response else { fatalError() } guard case let .success(account, _) = response else { fatalError() }
self.account = account self.account = account
self.persistentContainer.addOrUpdate(account: account) self.persistentContainer.backgroundContext.perform {
if let accountMO = self.persistentContainer.account(for: account.id, in: self.persistentContainer.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self.persistentContainer)
} else {
// the first time the user's account is added to the store,
// increment its reference count so that it's never removed
self.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true)
}
completion?(account) completion?(account)
} }
} }
} }
}
func getOwnInstance(completion: ((Instance) -> Void)? = nil) { func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
if let instance = self.instance { if let instance = self.instance {

View File

@ -32,6 +32,7 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
@NSManaged public var locked: Bool @NSManaged public var locked: Bool
@NSManaged public var movedCD: Bool @NSManaged public var movedCD: Bool
@NSManaged public var note: String @NSManaged public var note: String
@NSManaged public var referenceCount: Int
@NSManaged public var statusesCount: Int @NSManaged public var statusesCount: Int
@NSManaged public var url: URL @NSManaged public var url: URL
@NSManaged public var username: String @NSManaged public var username: String
@ -46,12 +47,30 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
public var bot: Bool? { botCD } public var bot: Bool? { botCD }
public var moved: Bool? { movedCD } public var moved: Bool? { movedCD }
func incrementReferenceCount() {
referenceCount += 1
}
func decrementReferenceCount() {
referenceCount -= 1
if referenceCount <= 0 {
managedObjectContext!.delete(self)
}
}
public override func prepareForDeletion() {
super.prepareForDeletion()
movedTo?.decrementReferenceCount()
}
} }
extension AccountMO { extension AccountMO {
convenience init(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore, context: NSManagedObjectContext) { convenience init(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
self.init(context: context) self.init(context: context)
self.updateFrom(apiAccount: account, container: container) self.updateFrom(apiAccount: account, container: container)
movedTo?.incrementReferenceCount()
} }
func updateFrom(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore) { func updateFrom(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore) {

View File

@ -23,8 +23,17 @@ class MastodonCachePersistentStore: NSPersistentContainer {
let statusSubject = PassthroughSubject<String, Never>() let statusSubject = PassthroughSubject<String, Never>()
let accountSubject = PassthroughSubject<String, Never>() let accountSubject = PassthroughSubject<String, Never>()
init(for controller: MastodonController) { init(for accountInfo: LocalData.UserAccountInfo?, transient: Bool = false) {
super.init(name: "\(controller.accountInfo!.id)_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel) if transient {
super.init(name: "transient_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
let storeDescription = NSPersistentStoreDescription()
storeDescription.type = NSInMemoryStoreType
persistentStoreDescriptions = [storeDescription]
} else {
super.init(name: "\(accountInfo!.id)_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
}
loadPersistentStores { (description, error) in loadPersistentStores { (description, error) in
if let error = error { if let error = error {
fatalError("Unable to load persistent store: \(error)") fatalError("Unable to load persistent store: \(error)")
@ -96,18 +105,25 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
@discardableResult @discardableResult
private func upsert(account: Account) -> AccountMO { private func upsert(account: Account, incrementReferenceCount: Bool) -> AccountMO {
if let accountMO = self.account(for: account.id, in: self.backgroundContext) { if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self) accountMO.updateFrom(apiAccount: account, container: self)
if incrementReferenceCount {
accountMO.incrementReferenceCount()
}
return accountMO return accountMO
} else { } else {
return AccountMO(apiAccount: account, container: self, context: self.backgroundContext) let accountMO = AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
if incrementReferenceCount {
accountMO.incrementReferenceCount()
}
return accountMO
} }
} }
func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) { func addOrUpdate(account: Account, incrementReferenceCount: Bool, completion: ((AccountMO) -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
let accountMO = self.upsert(account: account) let accountMO = self.upsert(account: account, incrementReferenceCount: incrementReferenceCount)
if self.backgroundContext.hasChanges { if self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
@ -118,7 +134,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
func addAll(accounts: [Account], completion: (() -> Void)? = nil) { func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
accounts.forEach { self.upsert(account: $0) } accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
if self.backgroundContext.hasChanges { if self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
@ -130,9 +146,11 @@ class MastodonCachePersistentStore: NSPersistentContainer {
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) { func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
let statuses = notifications.compactMap { $0.status } let statuses = notifications.compactMap { $0.status }
let accounts = notifications.map { $0.account } // filter out mentions, otherwise we would double increment the reference count of those accounts
// since the status has the same account as the notification
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) } statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
accounts.forEach { self.upsert(account: $0) } accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
if self.backgroundContext.hasChanges { if self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
@ -148,7 +166,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
var updatedStatuses = [String]() var updatedStatuses = [String]()
block(self.backgroundContext, { (accounts) in block(self.backgroundContext, { (accounts) in
accounts.forEach { self.upsert(account: $0) } accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
updatedAccounts.append(contentsOf: accounts.map { $0.id }) updatedAccounts.append(contentsOf: accounts.map { $0.id })
}, { (statuses) in }, { (statuses) in
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) } statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }

View File

@ -82,6 +82,7 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
public override func prepareForDeletion() { public override func prepareForDeletion() {
super.prepareForDeletion() super.prepareForDeletion()
reblog?.decrementReferenceCount() reblog?.decrementReferenceCount()
account.decrementReferenceCount()
} }
} }
@ -92,6 +93,7 @@ extension StatusMO {
self.updateFrom(apiStatus: status, container: container) self.updateFrom(apiStatus: status, container: container)
reblog?.incrementReferenceCount() reblog?.incrementReferenceCount()
account.incrementReferenceCount()
} }
func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) { func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) {
@ -106,7 +108,7 @@ extension StatusMO {
self.content = status.content self.content = status.content
self.createdAt = status.createdAt self.createdAt = status.createdAt
self.emojis = status.emojis self.emojis = status.emojis
self.favourited = status.favourited self.favourited = status.favourited ?? false
self.favouritesCount = status.favouritesCount self.favouritesCount = status.favouritesCount
self.hashtags = status.hashtags self.hashtags = status.hashtags
self.inReplyToAccountID = status.inReplyToAccountID self.inReplyToAccountID = status.inReplyToAccountID
@ -115,7 +117,7 @@ extension StatusMO {
self.mentions = status.mentions self.mentions = status.mentions
self.muted = status.muted ?? false self.muted = status.muted ?? false
self.pinnedInternal = status.pinned ?? false self.pinnedInternal = status.pinned ?? false
self.reblogged = status.reblogged 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.spoilerText = status.spoilerText

View File

@ -15,6 +15,7 @@
<attribute name="locked" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="locked" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="movedCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="movedCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="note" attributeType="String"/> <attribute name="note" attributeType="String"/>
<attribute name="referenceCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="url" attributeType="URI"/> <attribute name="url" attributeType="URI"/>
<attribute name="username" attributeType="String"/> <attribute name="username" attributeType="String"/>
@ -49,7 +50,6 @@
<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"/>
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/> <relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/> <relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
<uniquenessConstraints> <uniquenessConstraints>
@ -59,7 +59,7 @@
</uniquenessConstraints> </uniquenessConstraints>
</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="328"/>
<element name="Status" positionX="-63" positionY="-18" width="128" height="418"/> <element name="Status" positionX="-63" positionY="-18" width="128" height="418"/>
</elements> </elements>
</model> </model>

View File

@ -75,6 +75,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
let vc = AVPlayerViewController() let vc = AVPlayerViewController()
vc.player = AVPlayer(url: $0.url) vc.player = AVPlayer(url: $0.url)
return vc return vc
case .gifv:
return GifvAttachmentViewController(attachment: $0)
default: default:
fatalError() fatalError()
} }

View File

@ -0,0 +1,33 @@
//
// GifvAttachmentViewController.swift
// Tusker
//
// Created by Shadowfacts on 5/12/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import AVFoundation
class GifvAttachmentViewController: UIViewController {
private let attachment: Attachment
init(attachment: Attachment) {
precondition(attachment.kind == .gifv)
self.attachment = attachment
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let asset = AVURLAsset(url: attachment.url)
self.view = GifvAttachmentView(asset: asset, gravity: .resizeAspect)
}
}

View File

@ -10,19 +10,27 @@ import SwiftUI
struct LocalAccountAvatarView: View { struct LocalAccountAvatarView: View {
let localAccountInfo: LocalData.UserAccountInfo let localAccountInfo: LocalData.UserAccountInfo
@State @State var avatarImage: UIImage? = nil
var avatarImage: UIImage? = nil @ObservedObject var preferences = Preferences.shared
var body: some View { var body: some View {
let image: Image let image: Image
if avatarImage == nil { if avatarImage == nil {
image = Image(systemName: "person.crop.square") let imageName: String
switch preferences.avatarStyle {
case .circle:
imageName = "person.crop.circle"
case .roundRect:
imageName = "person.crop.square"
}
image = Image(systemName: imageName).resizable()
} else { } else {
image = Image(uiImage: self.avatarImage!).renderingMode(.original) image = Image(uiImage: self.avatarImage!).renderingMode(.original)
} }
return image return image
.resizable() .resizable()
.frame(width: 30, height: 30) .frame(width: 30, height: 30)
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 30)
.onAppear(perform: self.loadImage) .onAppear(perform: self.loadImage)
} }

View File

@ -61,6 +61,15 @@ class ProfileTableViewController: EnhancedTableViewController {
fatalError("init(coder:) has not been implemeneted") fatalError("init(coder:) has not been implemeneted")
} }
deinit {
if let id = accountID {
let container = mastodonController.persistentContainer
container.backgroundContext.perform {
container.account(for: id, in: container.backgroundContext)?.decrementReferenceCount()
}
}
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -90,7 +99,7 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
return return
} }
self.mastodonController.persistentContainer.addOrUpdate(account: account) { (_) in self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (_) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateAccountUI() self.updateAccountUI()
self.tableView.reloadData() self.tableView.reloadData()

View File

@ -134,11 +134,10 @@ class SearchResultsViewController: EnhancedTableViewController {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
// todo: reference count accounts oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in
// oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in guard case let .account(id) = item else { return }
// guard case let .account(id) = item else { return } self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount()
// self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount() }
// }
oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in
guard case let .status(id, _) = item else { return } guard case let .status(id, _) = item else { return }
@ -158,7 +157,6 @@ class SearchResultsViewController: EnhancedTableViewController {
snapshot.appendSections([.statuses]) snapshot.appendSections([.statuses])
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses) snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
addStatuses(results.statuses) addStatuses(results.statuses)
addAccounts(results.statuses.map { $0.account })
} }
}, completion: { }, completion: {
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@ -58,6 +58,17 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
if let accountIDs = self.accountIDs {
let container = self.mastodonController.persistentContainer
container.backgroundContext.perform {
for id in accountIDs {
container.account(for: id, in: container.backgroundContext)?.decrementReferenceCount()
}
}
}
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -71,7 +82,11 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude))
if accountIDs == nil { if let accountIDs = accountIDs {
accountIDs.forEach { (id) in
self.mastodonController.persistentContainer.account(for: id)?.incrementReferenceCount()
}
} else {
// account IDs haven't been set, so perform a request to load them // account IDs haven't been set, so perform a request to load them
guard let status = mastodonController.persistentContainer.status(for: statusID) else { guard let status = mastodonController.persistentContainer.status(for: statusID) else {
fatalError("Missing cached status \(statusID)") fatalError("Missing cached status \(statusID)")

View File

@ -37,7 +37,7 @@ class InstanceTimelineViewController: TimelineTableViewController {
self.instanceURL = url self.instanceURL = url
// the timeline VC only stores a weak reference to it, so we need to store a strong reference to make sure it's not released immediately // the timeline VC only stores a weak reference to it, so we need to store a strong reference to make sure it's not released immediately
instanceMastodonController = MastodonController(instanceURL: url) instanceMastodonController = MastodonController(instanceURL: url, transient: true)
super.init(for: .public(local: true), mastodonController: instanceMastodonController) super.init(for: .public(local: true), mastodonController: instanceMastodonController)

View File

@ -210,7 +210,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
let bookmarked = status.bookmarked ?? false let bookmarked = status.bookmarked ?? false
customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0) customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0)
if status.account.id == apiController.account.id { if apiController.account != nil, status.account.id == apiController.account.id {
let pinned = status.pinned ?? false let pinned = status.pinned ?? false
customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1) customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1)
} }

View File

@ -81,6 +81,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
loadVideo() loadVideo()
case .audio: case .audio:
loadAudio() loadAudio()
case .gifv:
loadGifv()
default: default:
preconditionFailure("invalid attachment type") preconditionFailure("invalid attachment type")
} }
@ -111,9 +113,9 @@ class AttachmentView: UIImageView, GIFAnimatable {
let asset = AVURLAsset(url: attachmentURL) let asset = AVURLAsset(url: attachmentURL)
let generator = AVAssetImageGenerator(asset: asset) let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true generator.appliesPreferredTrackTransform = true
guard let image = try? generator.copyCGImage(at: CMTime(seconds: 0, preferredTimescale: 1), actualTime: nil) else { return } guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
DispatchQueue.main.async { DispatchQueue.main.async { [weak self] in
guard self.attachment.url == attachmentURL else { return } guard let self = self, self.attachment.url == attachmentURL else { return }
self.image = UIImage(cgImage: image) self.image = UIImage(cgImage: image)
} }
} }
@ -150,6 +152,30 @@ class AttachmentView: UIImageView, GIFAnimatable {
]) ])
} }
func loadGifv() {
let attachmentURL = self.attachment.url
let asset = AVURLAsset(url: attachmentURL)
DispatchQueue.global(qos: .userInitiated).async {
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
DispatchQueue.main.async { [weak self] in
guard let self = self, self.attachment.url == attachmentURL else { return }
self.image = UIImage(cgImage: image)
}
}
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
gifvView.translatesAutoresizingMaskIntoConstraints = false
addSubview(gifvView)
NSLayoutConstraint.activate([
gifvView.leadingAnchor.constraint(equalTo: leadingAnchor),
gifvView.trailingAnchor.constraint(equalTo: trailingAnchor),
gifvView.topAnchor.constraint(equalTo: topAnchor),
gifvView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
override func display(_ layer: CALayer) { override func display(_ layer: CALayer) {
updateImageIfNeeded() updateImageIfNeeded()
} }

View File

@ -11,7 +11,7 @@ import Pachyderm
class AttachmentsContainerView: UIView { class AttachmentsContainerView: UIView {
static let supportedAttachmentTypes = [Attachment.Kind.image, .video, .audio] static let supportedAttachmentTypes = [Attachment.Kind.image, .video, .audio, .gifv]
weak var delegate: AttachmentViewDelegate? weak var delegate: AttachmentViewDelegate?

View File

@ -0,0 +1,49 @@
//
// GifvAttachmentView.swift
// Tusker
//
// Created by Shadowfacts on 5/12/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import AVFoundation
class GifvAttachmentView: UIView {
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
private var playerLayer: AVPlayerLayer {
layer as! AVPlayerLayer
}
private let item: AVPlayerItem
private let player: AVPlayer
init(asset: AVAsset, gravity: AVLayerVideoGravity) {
item = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: item)
super.init(frame: .zero)
playerLayer.player = player
playerLayer.videoGravity = gravity
player.play()
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func restartItem() {
item.seek(to: .zero) { (success) in
guard success else { return }
self.player.play()
}
}
}

View File

@ -261,7 +261,7 @@ class BaseStatusTableViewCell: UITableViewCell {
mastodonController.run(request) { response in mastodonController.run(request) { response in
DispatchQueue.main.async { DispatchQueue.main.async {
if case let .success(newStatus, _) = response { if case let .success(newStatus, _) = response {
self.favorited = newStatus.favourited self.favorited = newStatus.favourited ?? false
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false) self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
} else { } else {
@ -286,7 +286,7 @@ class BaseStatusTableViewCell: UITableViewCell {
mastodonController.run(request) { response in mastodonController.run(request) { response in
DispatchQueue.main.async { DispatchQueue.main.async {
if case let .success(newStatus, _) = response { if case let .success(newStatus, _) = response {
self.reblogged = newStatus.reblogged self.reblogged = newStatus.reblogged ?? false
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false) self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
} else { } else {