Compare commits
No commits in common. "26c99a1a35a81d7f53689f1cc493f444a17130df" and "32204368939d534e28df7570cd7558805b5a73bd" have entirely different histories.
26c99a1a35
...
3220436893
|
@ -81,7 +81,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
||||||
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
guard var components = URLComponents(url: request.baseURL ?? baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
||||||
components.path = request.path
|
components.path = request.path
|
||||||
components.queryItems = request.queryParameters.queryItems
|
components.queryItems = request.queryParameters.queryItems
|
||||||
guard let url = components.url else { return nil }
|
guard let url = components.url else { return nil }
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Foundation
|
||||||
public enum Timeline {
|
public enum Timeline {
|
||||||
case home
|
case home
|
||||||
case `public`(local: Bool)
|
case `public`(local: Bool)
|
||||||
|
case instance(instanceURL: URL)
|
||||||
case tag(hashtag: String)
|
case tag(hashtag: String)
|
||||||
case list(id: String)
|
case list(id: String)
|
||||||
case direct
|
case direct
|
||||||
|
@ -21,7 +22,7 @@ extension Timeline {
|
||||||
switch self {
|
switch self {
|
||||||
case .home:
|
case .home:
|
||||||
return "/api/v1/timelines/home"
|
return "/api/v1/timelines/home"
|
||||||
case .public:
|
case .public, .instance(_):
|
||||||
return "/api/v1/timelines/public"
|
return "/api/v1/timelines/public"
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
return "/api/v1/timelines/tag/\(hashtag)"
|
return "/api/v1/timelines/tag/\(hashtag)"
|
||||||
|
@ -33,7 +34,12 @@ extension Timeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
func request(range: RequestRange) -> Request<[Status]> {
|
func request(range: RequestRange) -> Request<[Status]> {
|
||||||
var request: Request<[Status]> = Request<[Status]>(method: .get, path: endpoint)
|
var request: Request<[Status]>
|
||||||
|
if case let .instance(instanceURL) = self {
|
||||||
|
request = Request<[Status]>(method: .get, baseURL: instanceURL, path: endpoint)
|
||||||
|
} else {
|
||||||
|
request = Request<[Status]>(method: .get, path: endpoint)
|
||||||
|
}
|
||||||
if case .public(true) = self {
|
if case .public(true) = self {
|
||||||
request.queryParameters.append("local" => true)
|
request.queryParameters.append("local" => true)
|
||||||
}
|
}
|
||||||
|
@ -51,6 +57,8 @@ extension Timeline: Codable {
|
||||||
self = .home
|
self = .home
|
||||||
case "public":
|
case "public":
|
||||||
self = .public(local: try container.decode(Bool.self, forKey: .local))
|
self = .public(local: try container.decode(Bool.self, forKey: .local))
|
||||||
|
case "instanceURL":
|
||||||
|
self = .instance(instanceURL: try container.decode(URL.self, forKey: .instanceURL))
|
||||||
case "tag":
|
case "tag":
|
||||||
self = .tag(hashtag: try container.decode(String.self, forKey: .hashtag))
|
self = .tag(hashtag: try container.decode(String.self, forKey: .hashtag))
|
||||||
case "list":
|
case "list":
|
||||||
|
@ -70,6 +78,9 @@ extension Timeline: Codable {
|
||||||
case let .public(local):
|
case let .public(local):
|
||||||
try container.encode("public", forKey: .type)
|
try container.encode("public", forKey: .type)
|
||||||
try container.encode(local, forKey: .local)
|
try container.encode(local, forKey: .local)
|
||||||
|
case let .instance(instanceURL):
|
||||||
|
try container.encode("instanceURL", forKey: .type)
|
||||||
|
try container.encode(instanceURL, forKey: .instanceURL)
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
try container.encode("tag", forKey: .type)
|
try container.encode("tag", forKey: .type)
|
||||||
try container.encode(hashtag, forKey: .hashtag)
|
try container.encode(hashtag, forKey: .hashtag)
|
||||||
|
@ -84,6 +95,7 @@ extension Timeline: Codable {
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case type
|
case type
|
||||||
case local
|
case local
|
||||||
|
case instanceURL
|
||||||
case hashtag
|
case hashtag
|
||||||
case listID
|
case listID
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,14 @@ import Foundation
|
||||||
|
|
||||||
public struct Request<ResultType: Decodable> {
|
public struct Request<ResultType: Decodable> {
|
||||||
let method: Method
|
let method: Method
|
||||||
|
let baseURL: URL?
|
||||||
let path: String
|
let path: String
|
||||||
let body: Body
|
let body: Body
|
||||||
var queryParameters: [Parameter]
|
var queryParameters: [Parameter]
|
||||||
|
|
||||||
init(method: Method, path: String, body: Body = .empty, queryParameters: [Parameter] = []) {
|
init(method: Method, baseURL: URL? = nil, path: String, body: Body = .empty, queryParameters: [Parameter] = []) {
|
||||||
self.method = method
|
self.method = method
|
||||||
|
self.baseURL = baseURL
|
||||||
self.path = path
|
self.path = path
|
||||||
self.body = body
|
self.body = body
|
||||||
self.queryParameters = queryParameters
|
self.queryParameters = queryParameters
|
||||||
|
|
|
@ -12,13 +12,13 @@ import Cache
|
||||||
class ImageCache {
|
class ImageCache {
|
||||||
|
|
||||||
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24))
|
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24))
|
||||||
static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24))
|
||||||
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
||||||
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
||||||
|
|
||||||
let cache: Cache<Data>
|
let cache: Cache<Data>
|
||||||
|
|
||||||
var requests = [URL: RequestGroup]()
|
var requests = [URL: Request]()
|
||||||
|
|
||||||
init(name: String, memoryExpiry expiry: Expiry) {
|
init(name: String, memoryExpiry expiry: Expiry) {
|
||||||
let storage = MemoryStorage<Data>(config: MemoryConfig(expiry: expiry))
|
let storage = MemoryStorage<Data>(config: MemoryConfig(expiry: expiry))
|
||||||
|
@ -36,22 +36,20 @@ class ImageCache {
|
||||||
self.cache = .hybrid(HybridStorage(memoryStorage: memory, diskStorage: disk))
|
self.cache = .hybrid(HybridStorage(memoryStorage: memory, diskStorage: disk))
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(_ url: URL, completion: ((Data?) -> Void)?) -> Request? {
|
func get(_ url: URL, completion: ((Data?) -> Void)?) {
|
||||||
let key = url.absoluteString
|
let key = url.absoluteString
|
||||||
if (try? cache.existsObject(forKey: key)) ?? false,
|
if (try? cache.existsObject(forKey: key)) ?? false,
|
||||||
let data = try? cache.object(forKey: key) {
|
let data = try? cache.object(forKey: key) {
|
||||||
completion?(data)
|
completion?(data)
|
||||||
return nil
|
|
||||||
} else {
|
} else {
|
||||||
if let completion = completion, let group = requests[url] {
|
if let completion = completion, let request = requests[url] {
|
||||||
return group.addCallback(completion)
|
request.callbacks.append(completion)
|
||||||
} else {
|
} else {
|
||||||
let group = RequestGroup(url: url)
|
let request = Request(url: url, completion: completion)
|
||||||
let request = group.addCallback(completion)
|
requests[url] = request
|
||||||
group.run { (data) in
|
request.run { (data) in
|
||||||
try? self.cache.setObject(data, forKey: key)
|
try? self.cache.setObject(data, forKey: key)
|
||||||
}
|
}
|
||||||
return request
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,21 +58,22 @@ class ImageCache {
|
||||||
return try? cache.object(forKey: url.absoluteString)
|
return try? cache.object(forKey: url.absoluteString)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelWithoutCallback(_ url: URL) {
|
func cancel(_ url: URL) {
|
||||||
requests[url]?.cancelWithoutCallback()
|
requests[url]?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestGroup {
|
class Request {
|
||||||
let url: URL
|
let url: URL
|
||||||
private var task: URLSessionDataTask?
|
var task: URLSessionDataTask?
|
||||||
private var requests = [Request]()
|
var callbacks: [(Data?) -> Void]
|
||||||
|
|
||||||
init(url: URL) {
|
init(url: URL, completion: ((Data?) -> Void)?) {
|
||||||
self.url = url
|
if let completion = completion {
|
||||||
|
self.callbacks = [completion]
|
||||||
|
} else {
|
||||||
|
self.callbacks = []
|
||||||
}
|
}
|
||||||
|
self.url = url
|
||||||
deinit {
|
|
||||||
task?.cancel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(cache: @escaping (Data) -> Void) {
|
func run(cache: @escaping (Data) -> Void) {
|
||||||
|
@ -89,56 +88,13 @@ class ImageCache {
|
||||||
task!.resume()
|
task!.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updatePriority() {
|
func cancel() {
|
||||||
task?.priority = max(1.0, URLSessionTask.defaultPriority + 0.1 * Float(requests.filter { !$0.cancelled }.count))
|
|
||||||
}
|
|
||||||
|
|
||||||
func addCallback(_ completion: ((Data?) -> Void)?) -> Request {
|
|
||||||
let request = Request(callback: completion)
|
|
||||||
requests.append(request)
|
|
||||||
updatePriority()
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancelWithoutCallback() {
|
|
||||||
if let request = requests.first(where: { $0.callback == nil && !$0.cancelled }) {
|
|
||||||
request.cancel()
|
|
||||||
updatePriority()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func requestCancelled() {
|
|
||||||
let remaining = requests.filter { !$0.cancelled }.count
|
|
||||||
if remaining <= 0 {
|
|
||||||
task?.cancel()
|
task?.cancel()
|
||||||
complete(with: nil)
|
complete(with: nil)
|
||||||
} else {
|
|
||||||
updatePriority()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func complete(with data: Data?) {
|
func complete(with data: Data?) {
|
||||||
requests.filter { !$0.cancelled }.forEach {
|
callbacks.forEach { $0(data) }
|
||||||
if let callback = $0.callback {
|
|
||||||
callback(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Request {
|
|
||||||
weak var group: RequestGroup?
|
|
||||||
private(set) var callback: ((Data?) -> Void)?
|
|
||||||
private(set) var cancelled: Bool = false
|
|
||||||
|
|
||||||
init(callback: ((Data?) -> Void)?) {
|
|
||||||
self.callback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancel() {
|
|
||||||
cancelled = true
|
|
||||||
callback = nil
|
|
||||||
group?.requestCancelled()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ extension Timeline {
|
||||||
return "Home"
|
return "Home"
|
||||||
case let .public(local):
|
case let .public(local):
|
||||||
return local ? "Local" : "Federated"
|
return local ? "Local" : "Federated"
|
||||||
|
case let .instance(instance):
|
||||||
|
return instance.host!
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
return "#\(hashtag)"
|
return "#\(hashtag)"
|
||||||
case .list:
|
case .list:
|
||||||
|
@ -35,6 +37,8 @@ extension Timeline {
|
||||||
} else {
|
} else {
|
||||||
return UIImage(systemName: "globe")
|
return UIImage(systemName: "globe")
|
||||||
}
|
}
|
||||||
|
case .instance(_):
|
||||||
|
return UIImage(systemName: "globe")
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,6 @@ class AttachmentViewController: UIViewController {
|
||||||
var largeImageVC: LargeImageViewController?
|
var largeImageVC: LargeImageViewController?
|
||||||
var loadingVC: LoadingViewController?
|
var loadingVC: LoadingViewController?
|
||||||
|
|
||||||
var attachmentRequest: ImageCache.Request?
|
|
||||||
|
|
||||||
private var initialControlsVisible: Bool = true
|
private var initialControlsVisible: Bool = true
|
||||||
var controlsVisible: Bool {
|
var controlsVisible: Bool {
|
||||||
get {
|
get {
|
||||||
|
@ -53,25 +51,15 @@ class AttachmentViewController: UIViewController {
|
||||||
} else {
|
} else {
|
||||||
loadingVC = LoadingViewController()
|
loadingVC = LoadingViewController()
|
||||||
embedChild(loadingVC!)
|
embedChild(loadingVC!)
|
||||||
attachmentRequest = ImageCache.attachments.get(attachment.url) { [weak self] (data) in
|
ImageCache.attachments.get(attachment.url) { [weak self] (data) in
|
||||||
guard let self = self else { return }
|
|
||||||
self.attachmentRequest = nil
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.loadingVC?.removeViewAndController()
|
self?.loadingVC?.removeViewAndController()
|
||||||
self.createLargeImage(data: data!)
|
self?.createLargeImage(data: data!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didMove(toParent parent: UIViewController?) {
|
|
||||||
super.didMove(toParent: parent)
|
|
||||||
|
|
||||||
if parent == nil {
|
|
||||||
attachmentRequest?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createLargeImage(data: Data) {
|
func createLargeImage(data: Data) {
|
||||||
guard let image = UIImage(data: data) else { return }
|
guard let image = UIImage(data: data) else { return }
|
||||||
largeImageVC = LargeImageViewController(image: image, description: attachment.description, sourceInfo: nil)
|
largeImageVC = LargeImageViewController(image: image, description: attachment.description, sourceInfo: nil)
|
||||||
|
|
|
@ -15,7 +15,13 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
var statuses: [(id: String, state: StatusState)] = []
|
var statuses: [(id: String, state: StatusState)] = [] {
|
||||||
|
didSet {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var newer: RequestRange?
|
var newer: RequestRange?
|
||||||
var older: RequestRange?
|
var older: RequestRange?
|
||||||
|
@ -49,10 +55,6 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
||||||
self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) })
|
self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) })
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userActivity = UserActivityManager.bookmarksActivity()
|
userActivity = UserActivityManager.bookmarksActivity()
|
||||||
|
@ -88,14 +90,7 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
||||||
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 newIndexPaths = (self.statuses.count..<(self.statuses.count + newStatuses.count)).map {
|
|
||||||
IndexPath(row: $0, section: 0)
|
|
||||||
}
|
|
||||||
self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,9 +159,9 @@ extension BookmarksTableViewController: 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: statuses[indexPath.row].id) else { continue }
|
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
|
||||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
for attachment in status.attachments where attachment.kind == .image {
|
||||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
ImageCache.attachments.get(attachment.url, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,9 +169,9 @@ extension BookmarksTableViewController: 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: statuses[indexPath.row].id) else { continue }
|
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
ImageCache.avatars.cancel(status.account.avatar)
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
for attachment in status.attachments where attachment.kind == .image {
|
||||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
ImageCache.attachments.cancel(attachment.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,9 +170,9 @@ extension ConversationTableViewController: 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: statuses[indexPath.row].id) else { continue }
|
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,9 +180,9 @@ extension ConversationTableViewController: 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: statuses[indexPath.row].id) else { continue }
|
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
ImageCache.avatars.cancel(status.account.avatar)
|
||||||
for attachment in status.attachments {
|
for attachment in status.attachments {
|
||||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
ImageCache.attachments.cancel(attachment.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,13 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
||||||
let excludedTypes: [Pachyderm.Notification.Kind]
|
let excludedTypes: [Pachyderm.Notification.Kind]
|
||||||
let groupTypes = [Notification.Kind.favourite, .reblog, .follow]
|
let groupTypes = [Notification.Kind.favourite, .reblog, .follow]
|
||||||
|
|
||||||
var groups: [NotificationGroup] = []
|
var groups: [NotificationGroup] = [] {
|
||||||
|
didSet {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var newer: RequestRange?
|
var newer: RequestRange?
|
||||||
var older: RequestRange?
|
var older: RequestRange?
|
||||||
|
@ -67,10 +73,6 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,9 +133,6 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
let newIndexPaths = (self.groups.count..<(self.groups.count + groups.count)).map {
|
|
||||||
IndexPath(row: $0, section: 0)
|
|
||||||
}
|
|
||||||
self.groups.append(contentsOf: groups)
|
self.groups.append(contentsOf: groups)
|
||||||
|
|
||||||
self.mastodonController.cache.addAll(notifications: newNotifications)
|
self.mastodonController.cache.addAll(notifications: newNotifications)
|
||||||
|
@ -141,10 +140,6 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
||||||
self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account })
|
self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account })
|
||||||
|
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,11 +216,6 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let newIndexPaths = (0..<groups.count).map {
|
|
||||||
IndexPath(row: $0, section: 0)
|
|
||||||
}
|
|
||||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
|
||||||
|
|
||||||
self.refreshControl?.endRefreshing()
|
self.refreshControl?.endRefreshing()
|
||||||
|
|
||||||
// maintain the current position in the list (don't scroll to top)
|
// maintain the current position in the list (don't scroll to top)
|
||||||
|
@ -250,7 +240,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
for notificationID in groups[indexPath.row].notificationIDs {
|
for notificationID in groups[indexPath.row].notificationIDs {
|
||||||
guard let notification = mastodonController.cache.notification(for: notificationID) else { continue }
|
guard let notification = mastodonController.cache.notification(for: notificationID) else { continue }
|
||||||
_ = ImageCache.avatars.get(notification.account.avatar, completion: nil)
|
ImageCache.avatars.get(notification.account.avatar, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,7 +249,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
for notificationID in groups[indexPath.row].notificationIDs {
|
for notificationID in groups[indexPath.row].notificationIDs {
|
||||||
guard let notification = mastodonController.cache.notification(for: notificationID) else { continue }
|
guard let notification = mastodonController.cache.notification(for: notificationID) else { continue }
|
||||||
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
|
ImageCache.avatars.cancel(notification.account.avatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@ class MyProfileTableViewController: ProfileTableViewController {
|
||||||
mastodonController.getOwnAccount { (account) in
|
mastodonController.getOwnAccount { (account) in
|
||||||
self.accountID = account.id
|
self.accountID = account.id
|
||||||
|
|
||||||
_ = ImageCache.avatars.get(account.avatar, completion: { [weak self] (data) in
|
ImageCache.avatars.get(account.avatar, completion: { (data) in
|
||||||
guard let self = self, let data = data, let image = UIImage(data: data) else { return }
|
guard let data = data, let image = UIImage(data: data) else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let size = CGSize(width: 30, height: 30)
|
let size = CGSize(width: 30, height: 30)
|
||||||
let tabBarImage = UIGraphicsImageRenderer(size: size).image { (_) in
|
let tabBarImage = UIGraphicsImageRenderer(size: size).image { (_) in
|
||||||
|
|
|
@ -294,9 +294,9 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching {
|
||||||
for indexPath in indexPaths where indexPath.section > 1 {
|
for indexPath in indexPaths where indexPath.section > 1 {
|
||||||
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
|
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
|
||||||
guard let status = mastodonController.cache.status(for: statusID) else { continue }
|
guard let status = mastodonController.cache.status(for: statusID) 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -305,9 +305,9 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching {
|
||||||
for indexPath in indexPaths where indexPath.section > 1 {
|
for indexPath in indexPaths where indexPath.section > 1 {
|
||||||
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
|
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
|
||||||
guard let status = mastodonController.cache.status(for: statusID) else { continue }
|
guard let status = mastodonController.cache.status(for: statusID) else { continue }
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
ImageCache.avatars.cancel(status.account.avatar)
|
||||||
for attachment in status.attachments {
|
for attachment in status.attachments {
|
||||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
ImageCache.attachments.cancel(attachment.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,10 +39,7 @@ class InstanceTimelineViewController: TimelineTableViewController {
|
||||||
// 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)
|
||||||
|
|
||||||
super.init(for: .public(local: true), mastodonController: instanceMastodonController)
|
super.init(for: .instance(instanceURL: url), mastodonController: instanceMastodonController)
|
||||||
|
|
||||||
title = url.host!
|
|
||||||
userActivity = nil // todo: activity for instance-specific timelines
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
@ -77,27 +74,6 @@ class InstanceTimelineViewController: TimelineTableViewController {
|
||||||
// no-op, we don't currently support viewing whole conversations from other instances
|
// no-op, we don't currently support viewing whole conversations from other instances
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
// don't show other screens or actions for other instances
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
// don't show swipe actions for other instances
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
// only show more actions for other instances
|
|
||||||
let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in
|
|
||||||
completion(true)
|
|
||||||
self.showMoreOptions(forStatus: self.timelineSegments[indexPath.section][indexPath.row].id, sourceView: tableView.cellForRow(at: indexPath))
|
|
||||||
}
|
|
||||||
more.image = UIImage(systemName: "ellipsis.circle.fill")
|
|
||||||
more.backgroundColor = .lightGray
|
|
||||||
return UISwipeActionsConfiguration(actions: [more])
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
@objc func toggleSaveButtonPressed() {
|
@objc func toggleSaveButtonPressed() {
|
||||||
if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) {
|
if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) {
|
||||||
|
|
|
@ -14,7 +14,13 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
var timeline: Timeline!
|
var timeline: Timeline!
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
var timelineSegments: [[(id: String, state: StatusState)]] = []
|
var timelineSegments: [[(id: String, state: StatusState)]] = [] {
|
||||||
|
didSet {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var newer: RequestRange?
|
var newer: RequestRange?
|
||||||
var older: RequestRange?
|
var older: RequestRange?
|
||||||
|
@ -63,9 +69,6 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
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.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,12 +105,7 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
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 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.tableView.insertRows(at: newIndexPaths, with: .none)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,11 +137,6 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let newIndexPaths = (0..<newStatuses.count).map {
|
|
||||||
IndexPath(row: $0, section: 0)
|
|
||||||
}
|
|
||||||
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)
|
||||||
|
@ -172,9 +165,9 @@ 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.cache.status(for: statusID(for: indexPath)) else { continue }
|
||||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||||
for attachment in status.attachments {
|
for attachment in status.attachments {
|
||||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
ImageCache.attachments.get(attachment.url, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,9 +175,9 @@ 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.cache.status(for: statusID(for: indexPath)) else { continue }
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
ImageCache.avatars.cancel(status.account.avatar)
|
||||||
for attachment in status.attachments {
|
for attachment in status.attachments {
|
||||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
ImageCache.attachments.cancel(attachment.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,9 @@ class UserActivityManager {
|
||||||
case .public(local: false):
|
case .public(local: false):
|
||||||
activity.title = NSLocalizedString("Show Federated Timeline", comment: "federated timeline shortcut title")
|
activity.title = NSLocalizedString("Show Federated Timeline", comment: "federated timeline shortcut title")
|
||||||
activity.suggestedInvocationPhrase = NSLocalizedString("Show my federated timeline", comment: "federated timeline invocation phrase")
|
activity.suggestedInvocationPhrase = NSLocalizedString("Show my federated timeline", comment: "federated timeline invocation phrase")
|
||||||
|
case let .instance(instance):
|
||||||
|
activity.title = String(format: NSLocalizedString("Show %@", comment: "show instance timeline shortcut title"), instance.host!)
|
||||||
|
activity.suggestedInvocationPhrase = String(format: NSLocalizedString("Show the instance %@", comment: "instance timeline shortcut invocation phrase"), instance.host!)
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
activity.title = String(format: NSLocalizedString("Show #%@", comment: "show hashtag shortcut title"), hashtag)
|
activity.title = String(format: NSLocalizedString("Show #%@", comment: "show hashtag shortcut title"), hashtag)
|
||||||
activity.suggestedInvocationPhrase = String(format: NSLocalizedString("Show the %@ hashtag", comment: "hashtag shortcut invocation phrase"), hashtag)
|
activity.suggestedInvocationPhrase = String(format: NSLocalizedString("Show the %@ hashtag", comment: "hashtag shortcut invocation phrase"), hashtag)
|
||||||
|
|
|
@ -19,7 +19,7 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
|
||||||
var avatarRequest: ImageCache.Request?
|
var avatarURL: URL?
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
@ -44,9 +44,9 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
fatalError("Missing cached account \(accountID)")
|
fatalError("Missing cached account \(accountID)")
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
self.avatarURL = account.avatar
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
ImageCache.avatars.get(account.avatar) { (data) in
|
||||||
self.avatarRequest = nil
|
guard let data = data, self.avatarURL == account.avatar else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = UIImage(data: data)
|
self.avatarImageView.image = UIImage(data: data)
|
||||||
}
|
}
|
||||||
|
@ -57,12 +57,6 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
updateUIForPrefrences()
|
updateUIForPrefrences()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
|
||||||
super.prepareForReuse()
|
|
||||||
|
|
||||||
avatarRequest?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountTableViewCell: SelectableTableViewCell {
|
extension AccountTableViewCell: SelectableTableViewCell {
|
||||||
|
|
|
@ -15,8 +15,6 @@ class LargeAccountDetailView: UIView {
|
||||||
var displayNameLabel = UILabel()
|
var displayNameLabel = UILabel()
|
||||||
var usernameLabel = UILabel()
|
var usernameLabel = UILabel()
|
||||||
|
|
||||||
var avatarRequest: ImageCache.Request?
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
@ -51,10 +49,6 @@ class LargeAccountDetailView: UIView {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
avatarRequest?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
@ -69,9 +63,8 @@ class LargeAccountDetailView: UIView {
|
||||||
displayNameLabel.text = account.realDisplayName
|
displayNameLabel.text = account.realDisplayName
|
||||||
usernameLabel.text = "@\(account.acct)"
|
usernameLabel.text = "@\(account.acct)"
|
||||||
|
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
ImageCache.avatars.get(account.avatar) { (data) in
|
||||||
guard let self = self, let data = data else { return }
|
guard let data = data else { return }
|
||||||
self.avatarRequest = nil
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = UIImage(data: data)
|
self.avatarImageView.image = UIImage(data: data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,6 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
var attachment: Attachment!
|
var attachment: Attachment!
|
||||||
var index: Int!
|
var index: Int!
|
||||||
|
|
||||||
private var attachmentRequest: ImageCache.Request?
|
|
||||||
|
|
||||||
var gifData: Data?
|
var gifData: Data?
|
||||||
|
|
||||||
public lazy var animator: Animator? = Animator(withDelegate: self)
|
public lazy var animator: Animator? = Animator(withDelegate: self)
|
||||||
|
@ -39,10 +37,6 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
loadAttachment()
|
loadAttachment()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
attachmentRequest?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
commonInit()
|
commonInit()
|
||||||
|
@ -77,9 +71,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadImage() {
|
func loadImage() {
|
||||||
attachmentRequest = ImageCache.attachments.get(attachment.url) { [weak self] (data) in
|
ImageCache.attachments.get(attachment.url) { [weak self] (data) in
|
||||||
guard let self = self, let data = data else { return }
|
guard let self = self, let data = data else { return }
|
||||||
self.attachmentRequest = nil
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.attachment.url.pathExtension == "gif" {
|
if self.attachment.url.pathExtension == "gif" {
|
||||||
self.animate(withGIFData: data)
|
self.animate(withGIFData: data)
|
||||||
|
@ -94,14 +87,12 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadVideo() {
|
func loadVideo() {
|
||||||
let attachmentURL = self.attachment.url
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
let asset = AVURLAsset(url: attachmentURL)
|
let asset = AVURLAsset(url: self.attachment.url)
|
||||||
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: CMTime(seconds: 0, preferredTimescale: 1), actualTime: nil) else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
guard self.attachment.url == attachmentURL else { return }
|
|
||||||
self.image = UIImage(cgImage: image)
|
self.image = UIImage(cgImage: image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,10 @@ class ComposeStatusReplyView: UIView {
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
@IBOutlet weak var statusContentTextView: StatusContentTextView!
|
@IBOutlet weak var statusContentTextView: StatusContentTextView!
|
||||||
|
|
||||||
var avatarRequest: ImageCache.Request?
|
|
||||||
|
|
||||||
static func create() -> ComposeStatusReplyView {
|
static func create() -> ComposeStatusReplyView {
|
||||||
return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeStatusReplyView
|
return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeStatusReplyView
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
avatarRequest?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
@ -45,8 +39,8 @@ class ComposeStatusReplyView: UIView {
|
||||||
statusContentTextView.overrideMastodonController = mastodonController
|
statusContentTextView.overrideMastodonController = mastodonController
|
||||||
statusContentTextView.statusID = status.id
|
statusContentTextView.statusID = status.id
|
||||||
|
|
||||||
avatarRequest = ImageCache.avatars.get(status.account.avatar) { [weak self] (data) in
|
ImageCache.avatars.get(status.account.avatar) { (data) in
|
||||||
guard let self = self, let data = data else { return }
|
guard let data = data else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = UIImage(data: data)
|
self.avatarImageView.image = UIImage(data: data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ class ContentTextView: LinkTextView {
|
||||||
|
|
||||||
for emoji in emojis {
|
for emoji in emojis {
|
||||||
group.enter()
|
group.enter()
|
||||||
_ = ImageCache.emojis.get(emoji.url) { (data) in
|
ImageCache.emojis.get(emoji.url) { (data) in
|
||||||
defer { group.leave() }
|
defer { group.leave() }
|
||||||
guard let data = data, let image = UIImage(data: data) else {
|
guard let data = data, let image = UIImage(data: data) else {
|
||||||
return
|
return
|
||||||
|
|
|
@ -20,7 +20,6 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
var selectorInstance: InstanceSelector.Instance?
|
var selectorInstance: InstanceSelector.Instance?
|
||||||
|
|
||||||
var thumbnailURL: URL?
|
var thumbnailURL: URL?
|
||||||
var thumbnailRequest: ImageCache.Request?
|
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
@ -60,21 +59,12 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
private func updateThumbnail(url: URL) {
|
private func updateThumbnail(url: URL) {
|
||||||
thumbnailImageView.image = nil
|
thumbnailImageView.image = nil
|
||||||
thumbnailURL = url
|
thumbnailURL = url
|
||||||
thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (data) in
|
ImageCache.attachments.get(url) { (data) in
|
||||||
guard let self = self, self.thumbnailURL == url, let data = data, let image = UIImage(data: data) else { return }
|
guard self.thumbnailURL == url, let data = data, let image = UIImage(data: data) else { return }
|
||||||
self.thumbnailRequest = nil
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.thumbnailImageView.image = image
|
self.thumbnailImageView.image = image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
|
||||||
super.prepareForReuse()
|
|
||||||
|
|
||||||
thumbnailRequest?.cancel()
|
|
||||||
instance = nil
|
|
||||||
selectorInstance = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
var group: NotificationGroup!
|
var group: NotificationGroup!
|
||||||
var statusID: String!
|
var statusID: String!
|
||||||
|
|
||||||
var avatarRequests = [String: ImageCache.Request]()
|
var authorAvatarURL: URL?
|
||||||
var updateTimestampWorkItem: DispatchWorkItem?
|
var updateTimestampWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
@ -75,10 +75,9 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.layer.masksToBounds = true
|
imageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
ImageCache.avatars.get(account.avatar) { (data) in
|
||||||
guard let self = self, let data = data, self.group.id == group.id else { return }
|
guard let data = data, self.group.id == group.id else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarRequests.removeValue(forKey: account.id)
|
|
||||||
imageView.image = UIImage(data: data)
|
imageView.image = UIImage(data: data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,7 +149,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
avatarRequests.values.forEach { $0.cancel() }
|
if let authorAvatarURL = authorAvatarURL {
|
||||||
|
ImageCache.avatars.cancel(authorAvatarURL)
|
||||||
|
}
|
||||||
|
|
||||||
updateTimestampWorkItem?.cancel()
|
updateTimestampWorkItem?.cancel()
|
||||||
updateTimestampWorkItem = nil
|
updateTimestampWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
var group: NotificationGroup!
|
var group: NotificationGroup!
|
||||||
|
|
||||||
var avatarRequests = [String: ImageCache.Request]()
|
|
||||||
var updateTimestampWorkItem: DispatchWorkItem?
|
var updateTimestampWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
@ -56,10 +55,9 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.layer.masksToBounds = true
|
imageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
ImageCache.avatars.get(account.avatar) { (data) in
|
||||||
guard let self = self, let data = data, self.group.id == group.id else { return }
|
guard let data = data, self.group.id == group.id else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarRequests.removeValue(forKey: account.id)
|
|
||||||
imageView.image = UIImage(data: data)
|
imageView.image = UIImage(data: data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +114,6 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
avatarRequests.values.forEach { $0.cancel() }
|
|
||||||
updateTimestampWorkItem?.cancel()
|
updateTimestampWorkItem?.cancel()
|
||||||
updateTimestampWorkItem = nil
|
updateTimestampWorkItem = nil
|
||||||
}
|
}
|
||||||
|
@ -142,19 +139,15 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
|
||||||
|
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
guard let mastodonController = mastodonController else { return nil }
|
guard let mastodonController = mastodonController else { return nil }
|
||||||
let accountIDs = self.group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id }
|
|
||||||
return (content: {
|
return (content: {
|
||||||
|
let accountIDs = self.group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id }
|
||||||
if accountIDs.count == 1 {
|
if accountIDs.count == 1 {
|
||||||
return ProfileTableViewController(accountID: accountIDs.first!, mastodonController: mastodonController)
|
return ProfileTableViewController(accountID: accountIDs.first!, mastodonController: mastodonController)
|
||||||
} else {
|
} else {
|
||||||
return AccountListTableViewController(accountIDs: accountIDs, mastodonController: mastodonController)
|
return AccountListTableViewController(accountIDs: accountIDs, mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
}, actions: {
|
}, actions: {
|
||||||
if accountIDs.count == 1 {
|
|
||||||
return self.actionsForProfile(accountID: accountIDs.first!, sourceView: self)
|
|
||||||
} else {
|
|
||||||
return []
|
return []
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
var notification: Pachyderm.Notification?
|
var notification: Pachyderm.Notification?
|
||||||
var account: Account!
|
var account: Account!
|
||||||
|
|
||||||
var avatarRequest: ImageCache.Request?
|
|
||||||
var updateTimestampWorkItem: DispatchWorkItem?
|
var updateTimestampWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
@ -54,9 +53,8 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
func updateUI(account: Account) {
|
func updateUI(account: Account) {
|
||||||
self.account = account
|
self.account = account
|
||||||
actionLabel.text = "Request to follow from \(account.realDisplayName)"
|
actionLabel.text = "Request to follow from \(account.realDisplayName)"
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
ImageCache.avatars.get(account.avatar) { (data) in
|
||||||
guard let self = self, self.account == account, let data = data, let image = UIImage(data: data) else { return }
|
guard self.account == account, let data = data, let image = UIImage(data: data) else { return }
|
||||||
self.avatarRequest = nil
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = image
|
self.avatarImageView.image = image
|
||||||
}
|
}
|
||||||
|
@ -90,7 +88,6 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
avatarRequest?.cancel()
|
|
||||||
updateTimestampWorkItem?.cancel()
|
updateTimestampWorkItem?.cancel()
|
||||||
updateTimestampWorkItem = nil
|
updateTimestampWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,8 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
|
||||||
var avatarRequest: ImageCache.Request?
|
var avatarURL: URL?
|
||||||
var headerRequest: ImageCache.Request?
|
var headerURL: URL?
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
avatarContainerView.layer.masksToBounds = true
|
avatarContainerView.layer.masksToBounds = true
|
||||||
|
@ -63,19 +63,19 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
usernameLabel.text = "@\(account.acct)"
|
usernameLabel.text = "@\(account.acct)"
|
||||||
|
|
||||||
avatarImageView.image = nil
|
avatarImageView.image = nil
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
avatarURL = account.avatar
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
ImageCache.avatars.get(account.avatar) { (data) in
|
||||||
self.avatarRequest = nil
|
guard let data = data else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = UIImage(data: data)
|
self.avatarImageView.image = UIImage(data: data)
|
||||||
|
self.avatarURL = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
headerImageView.image = nil
|
ImageCache.headers.get(account.header) { (data) in
|
||||||
headerRequest = ImageCache.headers.get(account.header) { [weak self] (data) in
|
guard let data = data else { return }
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
|
||||||
self.headerRequest = nil
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.headerImageView.image = UIImage(data: data)
|
self.headerImageView.image = UIImage(data: data)
|
||||||
|
self.headerURL = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +131,12 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
avatarRequest?.cancel()
|
if let url = avatarURL {
|
||||||
headerRequest?.cancel()
|
ImageCache.avatars.cancel(url)
|
||||||
|
}
|
||||||
|
if let url = headerURL {
|
||||||
|
ImageCache.headers.cancel(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func morePressed() {
|
@objc func morePressed() {
|
||||||
|
|
|
@ -64,7 +64,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
var showStatusAutomatically = false
|
var showStatusAutomatically = false
|
||||||
|
|
||||||
private var avatarRequest: ImageCache.Request?
|
var avatarURL: URL?
|
||||||
|
var attachmentDataTasks: [URLSessionDataTask] = []
|
||||||
|
|
||||||
private var statusUpdater: Cancellable?
|
private var statusUpdater: Cancellable?
|
||||||
private var accountUpdater: Cancellable?
|
private var accountUpdater: Cancellable?
|
||||||
|
@ -179,10 +180,12 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
func updateUI(account: Account) {
|
func updateUI(account: Account) {
|
||||||
usernameLabel.text = "@\(account.acct)"
|
usernameLabel.text = "@\(account.acct)"
|
||||||
avatarImageView.image = nil
|
avatarImageView.image = nil
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
avatarURL = account.avatar
|
||||||
guard let self = self, let data = data, self.accountID == account.id else { return }
|
ImageCache.avatars.get(account.avatar) { (data) in
|
||||||
|
guard let data = data, self.avatarURL == account.avatar else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = UIImage(data: data)
|
self.avatarImageView.image = UIImage(data: data)
|
||||||
|
self.avatarURL = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,7 +200,9 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
avatarRequest?.cancel()
|
if let avatarURL = avatarURL {
|
||||||
|
ImageCache.avatars.cancel(avatarURL)
|
||||||
|
}
|
||||||
attachmentsView.attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
|
attachmentsView.attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
|
||||||
showStatusAutomatically = false
|
showStatusAutomatically = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTimestamp() {
|
func updateTimestamp() {
|
||||||
guard let mastodonController = mastodonController, let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||||
|
|
||||||
timestampLabel.text = status.createdAt.timeAgoString()
|
timestampLabel.text = status.createdAt.timeAgoString()
|
||||||
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
|
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
|
||||||
|
@ -236,8 +236,8 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
||||||
completion(true)
|
completion(true)
|
||||||
self.delegate?.showMoreOptions(forStatus: self.statusID, sourceView: self)
|
self.delegate?.showMoreOptions(forStatus: self.statusID, sourceView: self)
|
||||||
}
|
}
|
||||||
more.image = UIImage(systemName: "ellipsis.circle.fill")
|
more.image = UIImage(systemName: "ellipsis")
|
||||||
more.backgroundColor = .lightGray
|
more.backgroundColor = .gray
|
||||||
return UISwipeActionsConfiguration(actions: [reply, more])
|
return UISwipeActionsConfiguration(actions: [reply, more])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue