Compare commits

...

7 Commits

12 changed files with 96 additions and 43 deletions

View File

@ -1,5 +1,13 @@
# Changelog
## 2022.1 (38)
This is a hotfix for the previous build. Its changelog is included below.
Bugfixes:
- Fix sensitive attachments not being hidden on the timeline
- Fix timeline descriptions appearing repeatedly
- iPadOS: Fix occasional crash when hovering over text
## 2022.1 (37)
This is the first build with the rewritten/rearchitected timeline screen. In future builds, this will roll out to the notifications and profile screens as well, but for now it's only used in the home tab. If you encounter crashes or errors, please report them. If you see a blue error bubble pop up, you can long-press it to send an error report.

View File

@ -2213,7 +2213,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2242,7 +2242,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2352,7 +2352,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2379,7 +2379,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;

View File

@ -10,6 +10,9 @@ import Foundation
import CoreData
import Pachyderm
import Combine
import OSLog
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
class MastodonCachePersistentStore: NSPersistentContainer {
@ -49,7 +52,8 @@ class MastodonCachePersistentStore: NSPersistentContainer {
loadPersistentStores { (description, error) in
if let error = error {
fatalError("Unable to load persistent store: \(error)")
logger.error("Unable to load persistent store: \(String(describing: error), privacy: .public)")
fatalError("Unable to load persistent store")
}
}
@ -58,6 +62,18 @@ class MastodonCachePersistentStore: NSPersistentContainer {
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: viewContext)
}
func save(context: NSManagedObjectContext) {
guard context.hasChanges else {
return
}
do {
try context.save()
} catch {
logger.error("Unable to save managed object context: \(String(describing: error), privacy: .public)")
fatalError("Unable to save managed object context")
}
}
func status(for id: String, in context: NSManagedObjectContext? = nil) -> StatusMO? {
let context = context ?? viewContext
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
@ -84,9 +100,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
let context = context ?? backgroundContext
context.perform {
let statusMO = self.upsert(status: status, context: context)
if context.hasChanges {
try! context.save()
}
self.save(context: context)
completion?(statusMO)
self.statusSubject.send(status.id)
}
@ -95,9 +109,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
@MainActor
func addOrUpdateOnViewContext(status: Status) -> StatusMO {
let statusMO = self.upsert(status: status, context: viewContext)
if viewContext.hasChanges {
try! viewContext.save()
}
self.save(context: viewContext)
statusSubject.send(status.id)
return statusMO
}
@ -105,9 +117,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
backgroundContext.perform {
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
self.save(context: self.backgroundContext)
statuses.forEach { self.statusSubject.send($0.id) }
completion?()
}
@ -146,9 +156,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) {
backgroundContext.perform {
let accountMO = self.upsert(account: account)
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
self.save(context: self.backgroundContext)
completion?(accountMO)
self.accountSubject.send(account.id)
}
@ -180,9 +188,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
func addOrUpdate(relationship: Relationship, completion: ((RelationshipMO) -> Void)? = nil) {
backgroundContext.perform {
let relationshipMO = self.upsert(relationship: relationship)
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
self.save(context: self.backgroundContext)
completion?(relationshipMO)
self.relationshipSubject.send(relationship.id)
}
@ -191,9 +197,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
backgroundContext.perform {
accounts.forEach { self.upsert(account: $0) }
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
self.save(context: self.backgroundContext)
completion?()
accounts.forEach { self.accountSubject.send($0.id) }
}
@ -207,9 +211,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
accounts.forEach { self.upsert(account: $0) }
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
self.save(context: self.backgroundContext)
completion?()
statuses.forEach { self.statusSubject.send($0.id) }
accounts.forEach { self.accountSubject.send($0.id) }
@ -232,9 +234,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
updatedAccounts.forEach(self.accountSubject.send)
updatedStatuses.forEach(self.statusSubject.send)
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
self.save(context: self.backgroundContext)
completion?()
}
}

View File

@ -4,7 +4,7 @@
<dict>
<key>OSLogPreferences</key>
<dict>
<key>$(PRODUCT_BUNDLE_IDENTIFIER)</key>
<key>space.vaccor.Tusker</key>
<dict>
<key>DEFAULT-OPTIONS</key>
<dict>

View File

@ -58,7 +58,7 @@ class HashtagTimelineViewController: TimelineTableViewController {
} else {
_ = SavedHashtag(hashtag: hashtag, context: context)
}
try! context.save()
mastodonController.persistentContainer.save(context: context)
}
}

View File

@ -94,7 +94,7 @@ class InstanceTimelineViewController: TimelineTableViewController {
_ = SavedInstance(url: instanceURL, context: context)
delegate?.didSaveInstance(url: instanceURL)
}
try? context.save()
mastodonController.persistentContainer.save(context: context)
}
}

View File

@ -124,7 +124,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
private func applyInitialSnapshot() {
if case .public(let local) = timeline,
(local && !Preferences.shared.hasShownLocalTimelineDescription) ||
(!local && Preferences.shared.hasShownFederatedTimelineDescription) {
(!local && !Preferences.shared.hasShownFederatedTimelineDescription) {
var snapshot = dataSource.snapshot()
snapshot.appendSections([.header])
snapshot.appendItems([.publicTimelineDescription], toSection: .header)
@ -144,6 +144,25 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if isShowingTimelineDescription,
case .public(let local) = timeline {
if local {
Preferences.shared.hasShownLocalTimelineDescription = true
} else {
Preferences.shared.hasShownFederatedTimelineDescription = true
}
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
pruneOffscreenRows()
}
private func removeTimelineDescriptionCell() {
var snapshot = dataSource.snapshot()
snapshot.deleteSections([.header])
@ -151,6 +170,25 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
isShowingTimelineDescription = false
}
private func pruneOffscreenRows() {
guard let lastVisibleIndexPath = collectionView.indexPathsForVisibleItems.last else {
return
}
var snapshot = dataSource.snapshot()
let items = snapshot.itemIdentifiers(inSection: .statuses)
let pageSize = 20
let numberOfPagesToPrune = (items.count - lastVisibleIndexPath.row - 1) / pageSize
if numberOfPagesToPrune > 0 {
let itemsToRemove = Array(items.suffix(numberOfPagesToPrune * pageSize))
snapshot.deleteItems(itemsToRemove)
}
dataSource.apply(snapshot, animatingDifferences: false)
if case .status(id: let id, state: _) = snapshot.itemIdentifiers(inSection: .statuses).last {
older = .before(id: id, count: nil)
}
}
@objc func refresh() {
Task {
await controller.loadNewer()

View File

@ -110,7 +110,7 @@ extension MenuActionProvider {
} else {
_ = SavedHashtag(hashtag: hashtag, context: context)
}
try! context.save()
mastodonController.persistentContainer.save(context: context)
})
]
} else {

View File

@ -33,9 +33,17 @@ class AttachmentsContainerView: UIView {
}
}
override func awakeFromNib() {
super.awakeFromNib()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
self.isUserInteractionEnabled = true
self.layer.cornerRadius = 5
self.layer.masksToBounds = true

View File

@ -221,7 +221,9 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
let charIndex = lineFragment.characterIndex(for: pointInLineFragment)
var range = NSRange()
guard let link = lineFragment.attributedString.attribute(.link, at: charIndex, longestEffectiveRange: &range, in: lineFragment.attributedString.fullRange) as? URL else {
// sometimes characterIndex(for:) returns NSNotFound even for points that are in the line fragment's typographic bounds (see #183), so we check just in case
guard charIndex != NSNotFound,
let link = lineFragment.attributedString.attribute(.link, at: charIndex, longestEffectiveRange: &range, in: lineFragment.attributedString.fullRange) as? URL else {
return nil
}
// lineFragment.attributedString is the NSTextLayoutFragment's string, and so range is in its index space

View File

@ -170,9 +170,7 @@ class StatusPollView: UIView {
return
}
status.poll = poll
if container.viewContext.hasChanges {
try! container.viewContext.save()
}
container.save(context: container.viewContext)
container.statusSubject.send(status.id)
}
}

View File

@ -85,13 +85,12 @@ extension StatusCollectionViewCell {
contentContainer.contentTextView.setTextFrom(status: status)
contentContainer.attachmentsView.delegate = self
contentContainer.attachmentsView.updateUI(status: status)
contentContainer.cardView.updateUI(status: status)
contentContainer.cardView.isHidden = status.card == nil
contentContainer.cardView.navigationDelegate = delegate
contentContainer.cardView.actionProvider = delegate
contentContainer.attachmentsView.updateUI(status: status)
updateStatusState(status: status)
contentWarningLabel.text = status.spoilerText