Compare commits
6 Commits
4fdafa893e
...
f4f2a5546c
Author | SHA1 | Date |
---|---|---|
Shadowfacts | f4f2a5546c | |
Shadowfacts | b220948e2b | |
Shadowfacts | 866edc472d | |
Shadowfacts | 88e4f52b5d | |
Shadowfacts | 98529ca5af | |
Shadowfacts | 6d8c5f632c |
|
@ -158,6 +158,7 @@
|
|||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */; };
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; };
|
||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
|
||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
|
||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
|
||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||
|
@ -454,6 +455,7 @@
|
|||
D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequestSpec.swift; sourceTree = "<group>"; };
|
||||
D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = "<group>"; };
|
||||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
|
||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
|
||||
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
|
||||
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
||||
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
||||
|
@ -942,6 +944,7 @@
|
|||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */,
|
||||
0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */,
|
||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */,
|
||||
);
|
||||
path = Preferences;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1743,6 +1746,7 @@
|
|||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
|
||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
||||
|
|
|
@ -13,15 +13,18 @@ import Combine
|
|||
|
||||
class MastodonCachePersistentStore: NSPersistentContainer {
|
||||
|
||||
private static let managedObjectModel: NSManagedObjectModel = {
|
||||
let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")!
|
||||
return NSManagedObjectModel(contentsOf: url)!
|
||||
}()
|
||||
|
||||
private(set) lazy var backgroundContext = newBackgroundContext()
|
||||
|
||||
let statusSubject = PassthroughSubject<String, Never>()
|
||||
let accountSubject = PassthroughSubject<String, Never>()
|
||||
|
||||
init(for controller: MastodonController) {
|
||||
let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")!
|
||||
let model = NSManagedObjectModel(contentsOf: url)!
|
||||
super.init(name: "\(controller.accountInfo!.id)_cache", managedObjectModel: model)
|
||||
super.init(name: "\(controller.accountInfo!.id)_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
||||
loadPersistentStores { (description, error) in
|
||||
if let error = error {
|
||||
fatalError("Unable to load persistent store: \(error)")
|
||||
|
|
|
@ -127,6 +127,33 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
// MARK: - Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
// see TimelineTableViewController.tableView(_:willDisplay:forRowAt:)
|
||||
if !isCurrentlyScrollingToTop, scrollViewDirection < 0 {
|
||||
let pageSize = 20
|
||||
if groups.count > 2 * pageSize,
|
||||
indexPath.row < groups.count - (2 * pageSize) {
|
||||
let groupsToRemove = groups[groups.count - pageSize..<groups.count]
|
||||
for group in groupsToRemove {
|
||||
for notification in group.notifications {
|
||||
// todo: reference count accounts
|
||||
// mastodonController.persistentContainer.account(for: notification.account.id)?.decrementReferenceCount()
|
||||
if let id = notification.status?.id {
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
let removedIndexPaths = (groups.count - 20..<groups.count).map { IndexPath(row: $0, section: 0) }
|
||||
|
||||
groups.removeLast(pageSize)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.deleteRows(at: removedIndexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if indexPath.row == groups.count - 1 {
|
||||
guard let older = older else { return }
|
||||
|
||||
|
|
|
@ -68,13 +68,16 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
|
|||
let authCode = item.value else { return }
|
||||
|
||||
mastodonController.authorize(authorizationCode: authCode) { (accessToken) in
|
||||
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
|
||||
mastodonController.accountInfo = accountInfo
|
||||
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch it's own account
|
||||
let tempAccountInfo = LocalData.UserAccountInfo(id: "temp", instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
|
||||
mastodonController.accountInfo = tempAccountInfo
|
||||
|
||||
mastodonController.getOwnAccount { (account) in
|
||||
LocalData.shared.setUsername(for: accountInfo, username: account.username)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// this needs to happen on the main thread because it publishes a new value for the ObservableObject
|
||||
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
|
||||
mastodonController.accountInfo = accountInfo
|
||||
|
||||
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// LocalAccountAvatarView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/10/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LocalAccountAvatarView: View {
|
||||
let localAccountInfo: LocalData.UserAccountInfo
|
||||
@State
|
||||
var avatarImage: UIImage? = nil
|
||||
|
||||
var body: some View {
|
||||
let image: Image
|
||||
if avatarImage == nil {
|
||||
image = Image(systemName: "person.crop.square")
|
||||
} else {
|
||||
image = Image(uiImage: self.avatarImage!).renderingMode(.original)
|
||||
}
|
||||
return image
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.onAppear(perform: self.loadImage)
|
||||
}
|
||||
|
||||
func loadImage() {
|
||||
let controller = MastodonController.getForAccount(localAccountInfo)
|
||||
controller.getOwnAccount { (account) in
|
||||
_ = ImageCache.avatars.get(account.avatar) { (data) in
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImage = image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct LocalAccountAvatarView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// LocalAccountAvatarView()
|
||||
// }
|
||||
//}
|
|
@ -21,8 +21,14 @@ struct PreferencesView: View {
|
|||
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
|
||||
}) {
|
||||
HStack {
|
||||
Text(account.username)
|
||||
.foregroundColor(.primary)
|
||||
LocalAccountAvatarView(localAccountInfo: account)
|
||||
VStack(alignment: .leading) {
|
||||
Text(verbatim: account.username)
|
||||
.foregroundColor(.primary)
|
||||
Text(verbatim: account.instanceURL.host!)
|
||||
.font(.caption)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
Spacer()
|
||||
if account == self.localData.getMostRecentAccount() {
|
||||
Image(systemName: "checkmark")
|
||||
|
|
|
@ -82,10 +82,11 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
|
|||
let request = actionType == .favorite ? Status.getFavourites(status.id) : Status.getReblogs(status.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(accounts, _) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addAll(accounts: accounts)
|
||||
DispatchQueue.main.async {
|
||||
self.accountIDs = accounts.map { $0.id }
|
||||
self.tableView.tableFooterView = nil
|
||||
self.mastodonController.persistentContainer.addAll(accounts: accounts) {
|
||||
DispatchQueue.main.async {
|
||||
self.accountIDs = accounts.map { $0.id }
|
||||
self.tableView.tableFooterView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
var newer: RequestRange?
|
||||
var older: RequestRange?
|
||||
|
||||
private var prevScrollViewContentOffset: CGPoint?
|
||||
private var scrollViewDirection: CGFloat = 0
|
||||
|
||||
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||
self.timeline = timeline
|
||||
self.mastodonController = mastodonController
|
||||
|
@ -109,8 +106,9 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
// MARK: - Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
// don't remove rows when jumping to the top, otherwise jumping back down might try to show removed rows
|
||||
// when scrolling upwards, decrement reference counts for old statuses, if necessary
|
||||
if scrollViewDirection < 0 {
|
||||
if !isCurrentlyScrollingToTop, scrollViewDirection < 0 {
|
||||
if indexPath.section <= timelineSegments.count - 2 {
|
||||
// decrement ref counts for all sections below the section below the current section
|
||||
// (e.g., there exist sections 0, 1, 2 and we're currently scrolling upwards in section 0, we want to remove section 2)
|
||||
|
@ -142,7 +140,7 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
for (id, _) in statusesToRemove {
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
timelineSegments[timelineSegments.count - 1].removeLast(20)
|
||||
timelineSegments[timelineSegments.count - 1].removeLast(pageSize)
|
||||
|
||||
let removedIndexPaths = (lastSection.count - 20..<lastSection.count).map { IndexPath(row: $0, section: timelineSegments.count - 1) }
|
||||
// Removing this DispatchQueue.main.async call causes things to break when scrolling
|
||||
|
@ -196,6 +194,8 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
||||
@objc func refreshStatuses(_ sender: Any) {
|
||||
guard let newer = newer else { return }
|
||||
|
||||
|
@ -226,15 +226,6 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark: Scroll View Delegate
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if let prev = prevScrollViewContentOffset {
|
||||
scrollViewDirection = scrollView.contentOffset.y - prev.y
|
||||
}
|
||||
prevScrollViewContentOffset = scrollView.contentOffset
|
||||
}
|
||||
|
||||
|
||||
@objc func composePressed(_ sender: Any) {
|
||||
compose()
|
||||
|
|
|
@ -11,32 +11,45 @@ import SafariServices
|
|||
|
||||
class EnhancedTableViewController: UITableViewController {
|
||||
|
||||
var prevScrollToTopOffset: CGPoint? = nil
|
||||
|
||||
private var topOffset: CGPoint {
|
||||
// when scrolled to top, the content offset is negative the height of the UI above the scroll view (i.e. the nav and status bars)
|
||||
let windowScene = view.window!.windowScene!
|
||||
let barOffset = -1 * (navigationController!.navigationBar.frame.height + windowScene.statusBarManager!.statusBarFrame.height)
|
||||
// add one so it's not technically all the way at the top, and scrollViewWShouldScrollToTop is still called to trigger undo
|
||||
return CGPoint(x: 0, y: barOffset + 1)
|
||||
}
|
||||
private var prevScrollToTopOffset: CGPoint? = nil
|
||||
private(set) var isCurrentlyScrollingToTop = false
|
||||
private var prevScrollViewContentOffset: CGPoint?
|
||||
private(set) var scrollViewDirection: CGFloat = 0
|
||||
|
||||
// MARK: Scroll View Delegate
|
||||
|
||||
override func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
|
||||
if let offset = prevScrollToTopOffset {
|
||||
tableView.setContentOffset(offset, animated: true)
|
||||
prevScrollToTopOffset = nil
|
||||
return false
|
||||
} else {
|
||||
prevScrollToTopOffset = tableView.contentOffset
|
||||
tableView.setContentOffset(topOffset, animated: true)
|
||||
isCurrentlyScrollingToTop = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
||||
isCurrentlyScrollingToTop = false
|
||||
// add one so it's not technically scrolled all the way to the top,
|
||||
// otherwise there's no way of detecting a status bar press to scroll back down
|
||||
tableView.contentOffset.y -= 0.5
|
||||
}
|
||||
|
||||
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
prevScrollToTopOffset = nil
|
||||
}
|
||||
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if let prev = prevScrollViewContentOffset {
|
||||
scrollViewDirection = scrollView.contentOffset.y - prev.y
|
||||
}
|
||||
prevScrollViewContentOffset = scrollView.contentOffset
|
||||
}
|
||||
|
||||
// MARK: Table View Delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if let cell = tableView.cellForRow(at: indexPath) as? SelectableTableViewCell {
|
||||
cell.didSelectCell()
|
||||
|
|
Loading…
Reference in New Issue