Compare commits
No commits in common. "0f6e9c97ccfc2ad01647085eab4806189cc9859c" and "8c888906c92a457bc325654e658bebed5c772ba7" have entirely different histories.
0f6e9c97cc
...
8c888906c9
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -1,33 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2022.1 (30)
|
|
||||||
Features/Improvements:
|
|
||||||
- Add fast account switching on iPad
|
|
||||||
- Add "Add Account" option to fast account switcher
|
|
||||||
- Show "# more replies" indicator in conversation in more circumstances
|
|
||||||
- When refreshing notifications, new ones are grouped with existing notifications
|
|
||||||
- Add subtitles to explain post visibility options
|
|
||||||
- Improve error messages when posting a video fails
|
|
||||||
- Display error messages instead of crashing when certain actions fail
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix Shortcuts actions not working in some circumstances
|
|
||||||
- Fix CW field growing wider than the screen
|
|
||||||
- Fix saved hashtags not persisting
|
|
||||||
- Fix not being able to long-press error-message bubbles
|
|
||||||
- Fix follow context menu item not updating after following
|
|
||||||
- Fix not being able to log in to certain Pixelfed instances
|
|
||||||
- Fix crash when closing the app
|
|
||||||
- Fix crash when loading profile screen
|
|
||||||
- Fix crash when refreshing polls
|
|
||||||
- Fix crash when poll voting fails
|
|
||||||
- Fix crash when accepting/rejecting a follow request fails
|
|
||||||
- Fix saved hashtags being sorted with case-sensitivity
|
|
||||||
- Fix multiple lines of text with emojis getting squashed together
|
|
||||||
- iPad: Fix Shortcuts actions showing wrong window type
|
|
||||||
- Mac: Fix Cmd+N shortcut not opening Compose window
|
|
||||||
- iPad/Mac: Fix Send Message action not mentioning account
|
|
||||||
|
|
||||||
## 2022.1 (27)
|
## 2022.1 (27)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Add emoji picker button to Compose screen toolbar
|
- Add emoji picker button to Compose screen toolbar
|
||||||
|
|
|
@ -49,12 +49,7 @@ public final class Account: AccountProtocol, Decodable {
|
||||||
self.avatarStatic = try? container.decode(URL.self, forKey: .avatarStatic)
|
self.avatarStatic = try? container.decode(URL.self, forKey: .avatarStatic)
|
||||||
self.header = try? container.decode(URL.self, forKey: .header)
|
self.header = try? container.decode(URL.self, forKey: .header)
|
||||||
self.headerStatic = try? container.decode(URL.self, forKey: .headerStatic)
|
self.headerStatic = try? container.decode(URL.self, forKey: .headerStatic)
|
||||||
// even up-to-date pixelfed instances sometimes lack this, for reasons unclear
|
self.emojis = try container.decode([Emoji].self, forKey: .emojis)
|
||||||
if let emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) {
|
|
||||||
self.emojis = emojis
|
|
||||||
} else {
|
|
||||||
self.emojis = []
|
|
||||||
}
|
|
||||||
self.fields = (try? container.decode([Field].self, forKey: .fields)) ?? []
|
self.fields = (try? container.decode([Field].self, forKey: .fields)) ?? []
|
||||||
self.bot = try? container.decode(Bool.self, forKey: .bot)
|
self.bot = try? container.decode(Bool.self, forKey: .bot)
|
||||||
|
|
||||||
|
|
|
@ -34,13 +34,6 @@ public class Hashtag: Codable {
|
||||||
self.history = try container.decodeIfPresent([History].self, forKey: .history)
|
self.history = try container.decodeIfPresent([History].self, forKey: .history)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(name, forKey: .name)
|
|
||||||
try container.encode(url.absoluteString, forKey: .url)
|
|
||||||
try container.encodeIfPresent(history, forKey: .history)
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case name
|
case name
|
||||||
case url
|
case url
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct NotificationGroup: Identifiable, Hashable {
|
public class NotificationGroup: Identifiable, Hashable {
|
||||||
public private(set) var notifications: [Notification]
|
public let notifications: [Notification]
|
||||||
public let id: String
|
public let id: String
|
||||||
public let kind: Notification.Kind
|
public let kind: Notification.Kind
|
||||||
public let statusState: StatusState?
|
public let statusState: StatusState?
|
||||||
|
@ -27,91 +27,34 @@ public struct NotificationGroup: Identifiable, Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool {
|
public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool {
|
||||||
guard lhs.notifications.count == rhs.notifications.count else {
|
return lhs.id == rhs.id
|
||||||
return false
|
|
||||||
}
|
|
||||||
for (a, b) in zip(lhs.notifications, rhs.notifications) where a.id != b.id {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
for notification in notifications {
|
hasher.combine(id)
|
||||||
hasher.combine(notification.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private mutating func append(_ notification: Notification) {
|
|
||||||
notifications.append(notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
private mutating func append(group: NotificationGroup) {
|
|
||||||
notifications.append(contentsOf: group.notifications)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
||||||
var groups = [NotificationGroup]()
|
var groups = [[Notification]]()
|
||||||
for notification in notifications {
|
for notification in notifications {
|
||||||
if allowedTypes.contains(notification.kind) {
|
if allowedTypes.contains(notification.kind) {
|
||||||
if let lastGroup = groups.last, canMerge(notification: notification, into: lastGroup) {
|
if let lastGroup = groups.last, let firstNotification = lastGroup.first, firstNotification.kind == notification.kind, firstNotification.status?.id == notification.status?.id {
|
||||||
groups[groups.count - 1].append(notification)
|
groups[groups.count - 1].append(notification)
|
||||||
continue
|
continue
|
||||||
} else if groups.count >= 2 {
|
} else if groups.count >= 2 {
|
||||||
let secondToLastGroup = groups[groups.count - 2]
|
let secondToLastGroup = groups[groups.count - 2]
|
||||||
if allowedTypes.contains(groups[groups.count - 1].kind), canMerge(notification: notification, into: secondToLastGroup) {
|
if allowedTypes.contains(groups[groups.count - 1][0].kind), let firstNotification = secondToLastGroup.first, firstNotification.kind == notification.kind, firstNotification.status?.id == notification.status?.id {
|
||||||
groups[groups.count - 2].append(notification)
|
groups[groups.count - 2].append(notification)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.append(NotificationGroup(notifications: [notification])!)
|
groups.append([notification])
|
||||||
}
|
}
|
||||||
return groups
|
return groups.map {
|
||||||
}
|
NotificationGroup(notifications: $0)!
|
||||||
|
|
||||||
private static func canMerge(notification: Notification, into group: NotificationGroup) -> Bool {
|
|
||||||
return notification.kind == group.kind && notification.status?.id == group.notifications.first!.status?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func mergeGroups(first: [NotificationGroup], second: [NotificationGroup], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
|
||||||
guard !first.isEmpty else {
|
|
||||||
return second
|
|
||||||
}
|
}
|
||||||
guard !second.isEmpty else {
|
|
||||||
return first
|
|
||||||
}
|
|
||||||
|
|
||||||
var merged = first
|
|
||||||
var second = second
|
|
||||||
merged.reserveCapacity(second.count)
|
|
||||||
while let firstGroupFromSecond = second.first,
|
|
||||||
allowedTypes.contains(firstGroupFromSecond.kind) {
|
|
||||||
|
|
||||||
second.removeFirst()
|
|
||||||
|
|
||||||
guard let lastGroup = merged.last,
|
|
||||||
allowedTypes.contains(lastGroup.kind) else {
|
|
||||||
merged.append(firstGroupFromSecond)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if canMerge(notification: firstGroupFromSecond.notifications.first!, into: lastGroup) {
|
|
||||||
merged[merged.count - 1].append(group: firstGroupFromSecond)
|
|
||||||
} else if merged.count >= 2 {
|
|
||||||
let secondToLastGroup = merged[merged.count - 2]
|
|
||||||
if allowedTypes.contains(secondToLastGroup.kind), canMerge(notification: firstGroupFromSecond.notifications.first!, into: secondToLastGroup) {
|
|
||||||
merged[merged.count - 2].append(group: firstGroupFromSecond)
|
|
||||||
} else {
|
|
||||||
merged.append(firstGroupFromSecond)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
merged.append(firstGroupFromSecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
merged.append(contentsOf: second)
|
|
||||||
return merged
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,268 +0,0 @@
|
||||||
//
|
|
||||||
// NotificationGroupTests.swift
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/26/22.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
@testable import Pachyderm
|
|
||||||
|
|
||||||
class NotificationGroupTests: XCTestCase {
|
|
||||||
|
|
||||||
let decoder: JSONDecoder = {
|
|
||||||
let d = JSONDecoder()
|
|
||||||
d.dateDecodingStrategy = .iso8601
|
|
||||||
return d
|
|
||||||
}()
|
|
||||||
|
|
||||||
let statusA = """
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"created_at": "2019-11-23T07:28:34Z",
|
|
||||||
"account": {
|
|
||||||
"id": "2",
|
|
||||||
"username": "bar",
|
|
||||||
"acct": "bar",
|
|
||||||
"display_name": "bar",
|
|
||||||
"locked": false,
|
|
||||||
"created_at": "2019-11-01T01:01:01Z",
|
|
||||||
"followers_count": 0,
|
|
||||||
"following_count": 0,
|
|
||||||
"statuses_count": 0,
|
|
||||||
"note": "",
|
|
||||||
"url": "https://example.com/@bar",
|
|
||||||
"uri": "https://example.com/@bar",
|
|
||||||
},
|
|
||||||
"url": "https://example.com/@bar/1",
|
|
||||||
"uri": "https://example.com/@bar/1",
|
|
||||||
"content": "",
|
|
||||||
"emojis": [],
|
|
||||||
"reblogs_count": 0,
|
|
||||||
"favourites_count": 0,
|
|
||||||
"sensitive": false,
|
|
||||||
"spoiler_text": "",
|
|
||||||
"visibility": "public",
|
|
||||||
"media_attachments": [],
|
|
||||||
"mentions": [],
|
|
||||||
"tags": [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
lazy var likeA1Data = """
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"type": "favourite",
|
|
||||||
"created_at": "2019-11-23T07:29:18Z",
|
|
||||||
"account": {
|
|
||||||
"id": "1",
|
|
||||||
"username": "foo",
|
|
||||||
"acct": "foo",
|
|
||||||
"display_name": "foo",
|
|
||||||
"locked": false,
|
|
||||||
"created_at": "2019-11-01T01:01:01Z",
|
|
||||||
"followers_count": 0,
|
|
||||||
"following_count": 0,
|
|
||||||
"statuses_count": 0,
|
|
||||||
"note": "",
|
|
||||||
"url": "https://example.com/@foo",
|
|
||||||
"uri": "https://example.com/@foo",
|
|
||||||
},
|
|
||||||
"status": \(statusA)
|
|
||||||
""".data(using: .utf8)!
|
|
||||||
lazy var likeA1 = try! decoder.decode(Notification.self, from: likeA1Data)
|
|
||||||
lazy var likeA2Data = """
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"type": "favourite",
|
|
||||||
"created_at": "2019-11-23T07:30:00Z",
|
|
||||||
"account": {
|
|
||||||
"id": "2",
|
|
||||||
"username": "baz",
|
|
||||||
"acct": "baz",
|
|
||||||
"display_name": "baz",
|
|
||||||
"locked": false,
|
|
||||||
"created_at": "2019-11-01T01:01:01Z",
|
|
||||||
"followers_count": 0,
|
|
||||||
"following_count": 0,
|
|
||||||
"statuses_count": 0,
|
|
||||||
"note": "",
|
|
||||||
"url": "https://example.com/@baz",
|
|
||||||
"uri": "https://example.com/@baz",
|
|
||||||
},
|
|
||||||
"status": \(statusA)
|
|
||||||
""".data(using: .utf8)!
|
|
||||||
lazy var likeA2 = try! decoder.decode(Notification.self, from: likeA2Data)
|
|
||||||
let statusB = """
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"created_at": "2019-11-23T07:28:34Z",
|
|
||||||
"account": {
|
|
||||||
"id": "2",
|
|
||||||
"username": "bar",
|
|
||||||
"acct": "bar",
|
|
||||||
"display_name": "bar",
|
|
||||||
"locked": false,
|
|
||||||
"created_at": "2019-11-01T01:01:01Z",
|
|
||||||
"followers_count": 0,
|
|
||||||
"following_count": 0,
|
|
||||||
"statuses_count": 0,
|
|
||||||
"note": "",
|
|
||||||
"url": "https://example.com/@bar",
|
|
||||||
"uri": "https://example.com/@bar",
|
|
||||||
},
|
|
||||||
"url": "https://example.com/@bar/1",
|
|
||||||
"uri": "https://example.com/@bar/1",
|
|
||||||
"content": "",
|
|
||||||
"emojis": [],
|
|
||||||
"reblogs_count": 0,
|
|
||||||
"favourites_count": 0,
|
|
||||||
"sensitive": false,
|
|
||||||
"spoiler_text": "",
|
|
||||||
"visibility": "public",
|
|
||||||
"media_attachments": [],
|
|
||||||
"mentions": [],
|
|
||||||
"tags": [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
lazy var likeBData = """
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"type": "favourite",
|
|
||||||
"created_at": "2019-11-23T07:29:18Z",
|
|
||||||
"account": {
|
|
||||||
"id": "1",
|
|
||||||
"username": "foo",
|
|
||||||
"acct": "foo",
|
|
||||||
"display_name": "foo",
|
|
||||||
"locked": false,
|
|
||||||
"created_at": "2019-11-01T01:01:01Z",
|
|
||||||
"followers_count": 0,
|
|
||||||
"following_count": 0,
|
|
||||||
"statuses_count": 0,
|
|
||||||
"note": "",
|
|
||||||
"url": "https://example.com/@foo",
|
|
||||||
"uri": "https://example.com/@foo",
|
|
||||||
},
|
|
||||||
"status": \(statusB)
|
|
||||||
""".data(using: .utf8)!
|
|
||||||
lazy var likeB = try! decoder.decode(Notification.self, from: likeBData)
|
|
||||||
lazy var likeB2Data = """
|
|
||||||
{
|
|
||||||
"id": "4",
|
|
||||||
"type": "favourite",
|
|
||||||
"created_at": "2019-11-23T07:29:18Z",
|
|
||||||
"account": {
|
|
||||||
"id": "2",
|
|
||||||
"username": "bar",
|
|
||||||
"acct": "bar",
|
|
||||||
"display_name": "bar",
|
|
||||||
"locked": false,
|
|
||||||
"created_at": "2019-11-02T01:01:01Z",
|
|
||||||
"followers_count": 0,
|
|
||||||
"following_count": 0,
|
|
||||||
"statuses_count": 0,
|
|
||||||
"note": "",
|
|
||||||
"url": "https://example.com/@bar",
|
|
||||||
"uri": "https://example.com/@bar",
|
|
||||||
},
|
|
||||||
"status": \(statusB)
|
|
||||||
""".data(using: .utf8)!
|
|
||||||
lazy var likeB2 = try! decoder.decode(Notification.self, from: likeB2Data)
|
|
||||||
lazy var mentionBData = """
|
|
||||||
{
|
|
||||||
"id": "5",
|
|
||||||
"type": "mention",
|
|
||||||
"created_at": "2019-11-23T07:29:18Z",
|
|
||||||
"account": {
|
|
||||||
"id": "1",
|
|
||||||
"username": "foo",
|
|
||||||
"acct": "foo",
|
|
||||||
"display_name": "foo",
|
|
||||||
"locked": false,
|
|
||||||
"created_at": "2019-11-01T01:01:01Z",
|
|
||||||
"followers_count": 0,
|
|
||||||
"following_count": 0,
|
|
||||||
"statuses_count": 0,
|
|
||||||
"note": "",
|
|
||||||
"url": "https://example.com/@foo",
|
|
||||||
"uri": "https://example.com/@foo",
|
|
||||||
},
|
|
||||||
"status": \(statusB)
|
|
||||||
""".data(using: .utf8)!
|
|
||||||
lazy var mentionB = try! decoder.decode(Notification.self, from: mentionBData)
|
|
||||||
|
|
||||||
|
|
||||||
func testGroupSimple() {
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeA2], only: [.favourite])
|
|
||||||
XCTAssertEqual(groups, [NotificationGroup(notifications: [likeA1, likeA2])!])
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGroupWithOtherGroupableInBetween() {
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeB, likeA2], only: [.favourite])
|
|
||||||
XCTAssertEqual(groups, [
|
|
||||||
NotificationGroup(notifications: [likeA1, likeA2])!,
|
|
||||||
NotificationGroup(notifications: [likeB])!,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDontGroupWithUngroupableInBetween() {
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, mentionB, likeA2], only: [.favourite])
|
|
||||||
XCTAssertEqual(groups, [
|
|
||||||
NotificationGroup(notifications: [likeA1])!,
|
|
||||||
NotificationGroup(notifications: [mentionB])!,
|
|
||||||
NotificationGroup(notifications: [likeA2])!,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMergeSimpleGroups() {
|
|
||||||
let group1 = NotificationGroup(notifications: [likeA1])!
|
|
||||||
let group2 = NotificationGroup(notifications: [likeA2])!
|
|
||||||
let merged = NotificationGroup.mergeGroups(first: [group1], second: [group2], only: [.favourite])
|
|
||||||
XCTAssertEqual(merged, [
|
|
||||||
NotificationGroup(notifications: [likeA1, likeA2])!
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMergeGroupsWithOtherGroupableInBetween() {
|
|
||||||
let group1 = NotificationGroup(notifications: [likeA1])!
|
|
||||||
let group2 = NotificationGroup(notifications: [likeB])!
|
|
||||||
let group3 = NotificationGroup(notifications: [likeA2])!
|
|
||||||
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
|
||||||
XCTAssertEqual(merged, [
|
|
||||||
NotificationGroup(notifications: [likeA1, likeA2])!,
|
|
||||||
NotificationGroup(notifications: [likeB])!,
|
|
||||||
])
|
|
||||||
|
|
||||||
let merged2 = NotificationGroup.mergeGroups(first: [group1], second: [group2, group3], only: [.favourite])
|
|
||||||
XCTAssertEqual(merged2, [
|
|
||||||
NotificationGroup(notifications: [likeA1, likeA2])!,
|
|
||||||
NotificationGroup(notifications: [likeB])!,
|
|
||||||
])
|
|
||||||
|
|
||||||
let group4 = NotificationGroup(notifications: [likeB2])!
|
|
||||||
let group5 = NotificationGroup(notifications: [mentionB])!
|
|
||||||
let merged3 = NotificationGroup.mergeGroups(first: [group1, group5, group2], second: [group4, group3], only: [.favourite])
|
|
||||||
print(merged3.count)
|
|
||||||
XCTAssertEqual(merged3, [
|
|
||||||
group1,
|
|
||||||
group5,
|
|
||||||
NotificationGroup(notifications: [likeB, likeB2]),
|
|
||||||
group3
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDontMergeWithUngroupableInBetween() {
|
|
||||||
let group1 = NotificationGroup(notifications: [likeA1])!
|
|
||||||
let group2 = NotificationGroup(notifications: [mentionB])!
|
|
||||||
let group3 = NotificationGroup(notifications: [likeA2])!
|
|
||||||
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
|
||||||
XCTAssertEqual(merged, [
|
|
||||||
NotificationGroup(notifications: [likeA1])!,
|
|
||||||
NotificationGroup(notifications: [mentionB])!,
|
|
||||||
NotificationGroup(notifications: [likeA2])!,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -235,11 +235,6 @@
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */; };
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */; };
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; };
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; };
|
||||||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
||||||
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */; };
|
|
||||||
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366A281EE77E00237D0E /* PollVoteButton.swift */; };
|
|
||||||
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366C2828444F00237D0E /* SavedInstance.swift */; };
|
|
||||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366E2828452F00237D0E /* SavedHashtag.swift */; };
|
|
||||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */; };
|
|
||||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */; };
|
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */; };
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */; };
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */; };
|
||||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; };
|
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; };
|
||||||
|
@ -285,14 +280,13 @@
|
||||||
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
|
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
|
||||||
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
|
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
|
||||||
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
|
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
|
||||||
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; };
|
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; };
|
||||||
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; };
|
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; };
|
||||||
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
|
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
|
||||||
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
||||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
|
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
|
||||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
|
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
|
||||||
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; };
|
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
||||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
||||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
||||||
|
@ -580,11 +574,6 @@
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableViewController.swift; sourceTree = "<group>"; };
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
||||||
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarMyProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
D6B9366A281EE77E00237D0E /* PollVoteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteButton.swift; sourceTree = "<group>"; };
|
|
||||||
D6B9366C2828444F00237D0E /* SavedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstance.swift; sourceTree = "<group>"; };
|
|
||||||
D6B9366E2828452F00237D0E /* SavedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtag.swift; sourceTree = "<group>"; };
|
|
||||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Helpers.swift"; sourceTree = "<group>"; };
|
|
||||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; };
|
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPageViewController.swift; sourceTree = "<group>"; };
|
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPageViewController.swift; sourceTree = "<group>"; };
|
||||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
|
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -646,7 +635,6 @@
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiImageView.swift; sourceTree = "<group>"; };
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiImageView.swift; sourceTree = "<group>"; };
|
||||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
||||||
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = "<group>"; };
|
|
||||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
|
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
|
||||||
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
||||||
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -757,7 +745,6 @@
|
||||||
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */,
|
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */,
|
||||||
D623A5402635FB3C0095BD04 /* PollOptionView.swift */,
|
D623A5402635FB3C0095BD04 /* PollOptionView.swift */,
|
||||||
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */,
|
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */,
|
||||||
D6B9366A281EE77E00237D0E /* PollVoteButton.swift */,
|
|
||||||
);
|
);
|
||||||
path = Poll;
|
path = Poll;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -853,10 +840,7 @@
|
||||||
D60E2F232442372B005F8713 /* StatusMO.swift */,
|
D60E2F232442372B005F8713 /* StatusMO.swift */,
|
||||||
D60E2F252442372B005F8713 /* AccountMO.swift */,
|
D60E2F252442372B005F8713 /* AccountMO.swift */,
|
||||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
|
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
|
||||||
D6B9366E2828452F00237D0E /* SavedHashtag.swift */,
|
|
||||||
D6B9366C2828444F00237D0E /* SavedInstance.swift */,
|
|
||||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
|
||||||
);
|
);
|
||||||
path = CoreData;
|
path = CoreData;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -907,7 +891,6 @@
|
||||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
||||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
||||||
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */,
|
|
||||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Main;
|
path = Main;
|
||||||
|
@ -1383,7 +1366,6 @@
|
||||||
D61959D2241E846D00A37B8E /* Models */,
|
D61959D2241E846D00A37B8E /* Models */,
|
||||||
D663626021360A9600C9CBA2 /* Preferences */,
|
D663626021360A9600C9CBA2 /* Preferences */,
|
||||||
D641C780213DD7C4004B4513 /* Screens */,
|
D641C780213DD7C4004B4513 /* Screens */,
|
||||||
D6E9CDA6281A426700BBC98E /* Services */,
|
|
||||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||||
D6BED1722126661300F02DA0 /* Views */,
|
D6BED1722126661300F02DA0 /* Views */,
|
||||||
|
@ -1440,14 +1422,6 @@
|
||||||
path = OpenInTusker;
|
path = OpenInTusker;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D6E9CDA6281A426700BBC98E /* Services */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
D6E9CDA7281A427800BBC98E /* PostService.swift */,
|
|
||||||
);
|
|
||||||
path = Services;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1802,8 +1776,6 @@
|
||||||
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */,
|
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */,
|
||||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||||
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */,
|
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */,
|
||||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
|
||||||
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */,
|
|
||||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||||
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
||||||
|
@ -1855,10 +1827,8 @@
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
||||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
|
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||||
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
|
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
|
||||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
||||||
|
@ -1873,7 +1843,6 @@
|
||||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
||||||
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||||
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */,
|
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
||||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
||||||
|
@ -1945,7 +1914,6 @@
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
||||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
|
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
|
||||||
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */,
|
|
||||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||||
|
@ -2034,7 +2002,6 @@
|
||||||
};
|
};
|
||||||
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = {
|
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
platformFilter = ios;
|
|
||||||
target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */;
|
target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */;
|
||||||
targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */;
|
targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
@ -2198,7 +2165,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 30;
|
CURRENT_PROJECT_VERSION = 27;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2229,7 +2196,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 30;
|
CURRENT_PROJECT_VERSION = 27;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2339,7 +2306,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 30;
|
CURRENT_PROJECT_VERSION = 27;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2366,7 +2333,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 30;
|
CURRENT_PROJECT_VERSION = 27;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
|
|
@ -28,16 +28,6 @@
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
|
||||||
skipped = "NO">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "PachydermTests"
|
|
||||||
BuildableName = "PachydermTests"
|
|
||||||
BlueprintName = "PachydermTests"
|
|
||||||
ReferencedContainer = "container:Pachyderm">
|
|
||||||
</BuildableReference>
|
|
||||||
</TestableReference>
|
|
||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
|
|
|
@ -29,7 +29,7 @@ class BookmarkStatusActivity: StatusActivity {
|
||||||
let request = Status.bookmark(status.id)
|
let request = Status.bookmark(status.id)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
if case let .success(status, _) = response {
|
if case let .success(status, _) = response {
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: status)
|
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
} else {
|
} else {
|
||||||
// todo: display error message
|
// todo: display error message
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
|
|
|
@ -28,7 +28,7 @@ class MuteConversationActivity: StatusActivity {
|
||||||
let request = Status.muteConversation(status.id)
|
let request = Status.muteConversation(status.id)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
if case let .success(status, _) = response {
|
if case let .success(status, _) = response {
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: status)
|
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
} else {
|
} else {
|
||||||
// todo: display error message
|
// todo: display error message
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
|
|
|
@ -28,7 +28,7 @@ class PinStatusActivity: StatusActivity {
|
||||||
let request = Status.pin(status.id)
|
let request = Status.pin(status.id)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
if case let .success(status, _) = response {
|
if case let .success(status, _) = response {
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: status)
|
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
} else {
|
} else {
|
||||||
// todo: display error message
|
// todo: display error message
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
|
|
|
@ -29,7 +29,7 @@ class UnbookmarkStatusActivity: StatusActivity {
|
||||||
let request = Status.unbookmark(status.id)
|
let request = Status.unbookmark(status.id)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
if case let .success(status, _) = response {
|
if case let .success(status, _) = response {
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: status)
|
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
} else {
|
} else {
|
||||||
// todo: display error message
|
// todo: display error message
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
|
|
|
@ -28,7 +28,7 @@ class UnmuteConversationActivity: StatusActivity {
|
||||||
let request = Status.unmuteConversation(status.id)
|
let request = Status.unmuteConversation(status.id)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
if case let .success(status, _) = response {
|
if case let .success(status, _) = response {
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: status)
|
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
} else {
|
} else {
|
||||||
// todo: display error message
|
// todo: display error message
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
|
|
|
@ -28,7 +28,7 @@ class UnpinStatusActivity: StatusActivity {
|
||||||
let request = Status.unpin(status.id)
|
let request = Status.unpin(status.id)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
if case let .success(status, _) = response {
|
if case let .success(status, _) = response {
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: status)
|
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
} else {
|
} else {
|
||||||
// todo: display error message
|
// todo: display error message
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
|
|
|
@ -8,10 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import CrashReporter
|
import CrashReporter
|
||||||
import CoreData
|
|
||||||
import OSLog
|
|
||||||
|
|
||||||
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
|
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
@ -31,24 +27,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
AudioSessionHelper.setDefault()
|
AudioSessionHelper.setDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let oldSavedData = SavedDataManager.load() {
|
|
||||||
do {
|
|
||||||
for account in oldSavedData.accountIDs {
|
|
||||||
guard let account = LocalData.shared.getAccount(id: account) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let controller = MastodonController.getForAccount(account)
|
|
||||||
try oldSavedData.migrateToCoreData(accountID: account.id, context: controller.persistentContainer.viewContext)
|
|
||||||
if controller.persistentContainer.viewContext.hasChanges {
|
|
||||||
try controller.persistentContainer.viewContext.save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try SavedDataManager.destroy()
|
|
||||||
} catch {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +45,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func buildMenu(with builder: UIMenuBuilder) {
|
override func buildMenu(with builder: UIMenuBuilder) {
|
||||||
|
super.buildMenu(with: builder)
|
||||||
|
|
||||||
if builder.system == .main {
|
if builder.system == .main {
|
||||||
MenuController.buildMainMenu(builder: builder)
|
MenuController.buildMainMenu(builder: builder)
|
||||||
|
@ -95,22 +74,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
.bookmarks,
|
.bookmarks,
|
||||||
.myProfile,
|
.myProfile,
|
||||||
.showProfile:
|
.showProfile:
|
||||||
if activity.displaysAuxiliaryScene {
|
return "auxiliary"
|
||||||
stateRestorationLogger.info("Using auxiliary scene for \(type.rawValue, privacy: .public)")
|
|
||||||
return "auxiliary"
|
|
||||||
} else {
|
|
||||||
return "main-scene"
|
|
||||||
}
|
|
||||||
|
|
||||||
case .newPost:
|
case .newPost:
|
||||||
return "compose"
|
return "compose"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func closeWindow() {
|
|
||||||
guard let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ class MastodonController: ObservableObject {
|
||||||
} else {
|
} else {
|
||||||
// the first time the user's account is added to the store,
|
// the first time the user's account is added to the store,
|
||||||
// increment its reference count so that it's never removed
|
// increment its reference count so that it's never removed
|
||||||
self.persistentContainer.addOrUpdate(account: account)
|
self.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true)
|
||||||
}
|
}
|
||||||
completion?(.success(account))
|
completion?(.success(account))
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,21 +52,20 @@ struct MenuController {
|
||||||
static let prevSubTabCommand = UIKeyCommand(title: "Previous Sub Tab", action: #selector(TabbedPageViewController.selectPrevPage), input: "[", modifierFlags: [.command, .shift])
|
static let prevSubTabCommand = UIKeyCommand(title: "Previous Sub Tab", action: #selector(TabbedPageViewController.selectPrevPage), input: "[", modifierFlags: [.command, .shift])
|
||||||
|
|
||||||
static func buildMainMenu(builder: UIMenuBuilder) {
|
static func buildMainMenu(builder: UIMenuBuilder) {
|
||||||
builder.replace(menu: .file, with: buildFileMenu())
|
builder.insertChild(buildFileMenu(), atStartOfMenu: .file)
|
||||||
builder.insertChild(buildSubTabMenu(), atStartOfMenu: .view)
|
builder.insertChild(buildSubTabMenu(), atStartOfMenu: .view)
|
||||||
builder.insertChild(buildSidebarShortcuts(), atStartOfMenu: .view)
|
builder.insertChild(buildSidebarShortcuts(), atStartOfMenu: .view)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func buildFileMenu() -> UIMenu {
|
private static func buildFileMenu() -> UIMenu {
|
||||||
return UIMenu(
|
return UIMenu(
|
||||||
title: "File",
|
title: "",
|
||||||
image: nil,
|
image: nil,
|
||||||
identifier: nil,
|
identifier: nil,
|
||||||
options: [],
|
options: .displayInline,
|
||||||
children: [
|
children: [
|
||||||
composeCommand,
|
composeCommand,
|
||||||
refreshCommand(discoverabilityTitle: nil),
|
refreshCommand(discoverabilityTitle: nil),
|
||||||
UIKeyCommand(title: "Close", action: #selector(AppDelegate.closeWindow), input: "w", modifierFlags: .command),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,10 +32,10 @@ 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
|
||||||
@NSManaged public var lastFetchedAt: Date?
|
|
||||||
@NSManaged public var movedTo: AccountMO?
|
@NSManaged public var movedTo: AccountMO?
|
||||||
|
|
||||||
@LazilyDecoding(arrayFrom: \AccountMO.emojisData)
|
@LazilyDecoding(arrayFrom: \AccountMO.emojisData)
|
||||||
|
@ -47,21 +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 }
|
||||||
|
|
||||||
public override func awakeFromFetch() {
|
func incrementReferenceCount() {
|
||||||
super.awakeFromFetch()
|
referenceCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
managedObjectContext?.perform {
|
func decrementReferenceCount() {
|
||||||
self.lastFetchedAt = Date()
|
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)
|
||||||
self.lastFetchedAt = Date()
|
|
||||||
|
movedTo?.incrementReferenceCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateFrom(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore) {
|
func updateFrom(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore) {
|
||||||
|
|
|
@ -50,8 +50,6 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
fatalError("Unable to load persistent store: \(error)")
|
fatalError("Unable to load persistent store: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: viewContext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func status(for id: String, in context: NSManagedObjectContext? = nil) -> StatusMO? {
|
func status(for id: String, in context: NSManagedObjectContext? = nil) -> StatusMO? {
|
||||||
|
@ -67,19 +65,26 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func upsert(status: Status, context: NSManagedObjectContext) -> StatusMO {
|
private func upsert(status: Status, incrementReferenceCount: Bool, context: NSManagedObjectContext) -> StatusMO {
|
||||||
if let statusMO = self.status(for: status.id, in: context) {
|
if let statusMO = self.status(for: status.id, in: context) {
|
||||||
statusMO.updateFrom(apiStatus: status, container: self)
|
statusMO.updateFrom(apiStatus: status, container: self)
|
||||||
|
if incrementReferenceCount {
|
||||||
|
statusMO.incrementReferenceCount()
|
||||||
|
}
|
||||||
return statusMO
|
return statusMO
|
||||||
} else {
|
} else {
|
||||||
return StatusMO(apiStatus: status, container: self, context: context)
|
let statusMO = StatusMO(apiStatus: status, container: self, context: context)
|
||||||
|
if incrementReferenceCount {
|
||||||
|
statusMO.incrementReferenceCount()
|
||||||
|
}
|
||||||
|
return statusMO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addOrUpdate(status: Status, context: NSManagedObjectContext? = nil, completion: ((StatusMO) -> Void)? = nil) {
|
func addOrUpdate(status: Status, incrementReferenceCount: Bool, context: NSManagedObjectContext? = nil, completion: ((StatusMO) -> Void)? = nil) {
|
||||||
let context = context ?? backgroundContext
|
let context = context ?? backgroundContext
|
||||||
context.perform {
|
context.perform {
|
||||||
let statusMO = self.upsert(status: status, context: context)
|
let statusMO = self.upsert(status: status, incrementReferenceCount: incrementReferenceCount, context: context)
|
||||||
if context.hasChanges {
|
if context.hasChanges {
|
||||||
try! context.save()
|
try! context.save()
|
||||||
}
|
}
|
||||||
|
@ -88,19 +93,9 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func addOrUpdateOnViewContext(status: Status) -> StatusMO {
|
|
||||||
let statusMO = self.upsert(status: status, context: viewContext)
|
|
||||||
if viewContext.hasChanges {
|
|
||||||
try! viewContext.save()
|
|
||||||
}
|
|
||||||
statusSubject.send(status.id)
|
|
||||||
return statusMO
|
|
||||||
}
|
|
||||||
|
|
||||||
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
|
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
backgroundContext.perform {
|
||||||
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true, context: self.backgroundContext) }
|
||||||
if self.backgroundContext.hasChanges {
|
if self.backgroundContext.hasChanges {
|
||||||
try! self.backgroundContext.save()
|
try! self.backgroundContext.save()
|
||||||
}
|
}
|
||||||
|
@ -109,14 +104,6 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAll(statuses: [Status]) async {
|
|
||||||
return await withCheckedContinuation { continuation in
|
|
||||||
addAll(statuses: statuses) {
|
|
||||||
continuation.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? {
|
func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? {
|
||||||
let context = context ?? viewContext
|
let context = context ?? viewContext
|
||||||
let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest()
|
let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest()
|
||||||
|
@ -130,18 +117,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()
|
||||||
}
|
}
|
||||||
|
@ -186,7 +180,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()
|
||||||
}
|
}
|
||||||
|
@ -201,8 +195,8 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
// filter out mentions, otherwise we would double increment the reference count of those accounts
|
// filter out mentions, otherwise we would double increment the reference count of those accounts
|
||||||
// since the status has the same account as the notification
|
// since the status has the same account as the notification
|
||||||
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
||||||
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true, context: self.backgroundContext) }
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
@ -218,10 +212,10 @@ 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, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true, context: self.backgroundContext) }
|
||||||
updatedStatuses.append(contentsOf: statuses.map { $0.id })
|
updatedStatuses.append(contentsOf: statuses.map { $0.id })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -235,43 +229,4 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
|
||||||
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
|
||||||
if changes.hashtags {
|
|
||||||
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
|
|
||||||
}
|
|
||||||
if changes.instances {
|
|
||||||
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hasChangedSavedHashtagsOrInstances(_ notification: Foundation.Notification) -> (hashtags: Bool, instances: Bool) {
|
|
||||||
var changes: (hashtags: Bool, instances: Bool) = (false, false)
|
|
||||||
if let inserted = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> {
|
|
||||||
for object in inserted {
|
|
||||||
if object is SavedHashtag {
|
|
||||||
changes.hashtags = true
|
|
||||||
} else if object is SavedInstance {
|
|
||||||
changes.instances = true
|
|
||||||
}
|
|
||||||
if changes.hashtags && changes.instances {
|
|
||||||
return changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let deleted = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> {
|
|
||||||
for object in deleted {
|
|
||||||
if object is SavedHashtag {
|
|
||||||
changes.hashtags = true
|
|
||||||
} else if object is SavedInstance {
|
|
||||||
changes.instances = true
|
|
||||||
}
|
|
||||||
if changes.hashtags && changes.instances {
|
|
||||||
return changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changes
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
//
|
|
||||||
// NSManagedObjectContext+Helpers.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/9/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import CoreData
|
|
||||||
|
|
||||||
extension NSManagedObjectContext {
|
|
||||||
func objectExists<T: NSFetchRequestResult>(for request: NSFetchRequest<T>) -> Bool {
|
|
||||||
switch try? count(for: request) {
|
|
||||||
case nil, 0, NSNotFound:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
//
|
|
||||||
// SavedHashtag.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/8/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CoreData
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
@objc(SavedHashtag)
|
|
||||||
public final class SavedHashtag: NSManagedObject {
|
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<SavedHashtag> {
|
|
||||||
return NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
|
|
||||||
}
|
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest(name: String) -> NSFetchRequest<SavedHashtag> {
|
|
||||||
let req = NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
|
|
||||||
req.predicate = NSPredicate(format: "name = %@", name)
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
@NSManaged public var name: String
|
|
||||||
@NSManaged public var url: URL
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SavedHashtag {
|
|
||||||
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
|
||||||
self.init(context: context)
|
|
||||||
self.name = hashtag.name
|
|
||||||
self.url = hashtag.url
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
//
|
|
||||||
// SavedInstance.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/8/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CoreData
|
|
||||||
|
|
||||||
@objc(SavedInstance)
|
|
||||||
public final class SavedInstance: NSManagedObject {
|
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<SavedInstance> {
|
|
||||||
return NSFetchRequest<SavedInstance>(entityName: "SavedInstance")
|
|
||||||
}
|
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest(url: URL) -> NSFetchRequest<SavedInstance> {
|
|
||||||
let req = fetchRequest()
|
|
||||||
req.predicate = NSPredicate(format: "url = %@", url as NSURL)
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
@NSManaged public var url: URL
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SavedInstance {
|
|
||||||
convenience init(url: URL, context: NSManagedObjectContext) {
|
|
||||||
self.init(context: context)
|
|
||||||
self.url = url
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -36,6 +36,7 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
@NSManaged private var pinnedInternal: Bool
|
@NSManaged private var pinnedInternal: Bool
|
||||||
@NSManaged public var reblogged: Bool
|
@NSManaged public var reblogged: Bool
|
||||||
@NSManaged public var reblogsCount: Int
|
@NSManaged public var reblogsCount: Int
|
||||||
|
@NSManaged public var referenceCount: Int
|
||||||
@NSManaged public var sensitive: Bool
|
@NSManaged public var sensitive: Bool
|
||||||
@NSManaged public var spoilerText: String
|
@NSManaged public var spoilerText: String
|
||||||
@NSManaged public var uri: String // todo: are both uri and url necessary?
|
@NSManaged public var uri: String // todo: are both uri and url necessary?
|
||||||
|
@ -45,7 +46,6 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
@NSManaged public var account: AccountMO
|
@NSManaged public var account: AccountMO
|
||||||
@NSManaged public var reblog: StatusMO?
|
@NSManaged public var reblog: StatusMO?
|
||||||
@NSManaged public var localOnly: Bool
|
@NSManaged public var localOnly: Bool
|
||||||
@NSManaged public var lastFetchedAt: Date?
|
|
||||||
|
|
||||||
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
|
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
|
||||||
public var attachments: [Attachment]
|
public var attachments: [Attachment]
|
||||||
|
@ -77,21 +77,32 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func awakeFromFetch() {
|
func incrementReferenceCount() {
|
||||||
super.awakeFromFetch()
|
referenceCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
managedObjectContext?.perform {
|
func decrementReferenceCount() {
|
||||||
self.lastFetchedAt = Date()
|
referenceCount -= 1
|
||||||
|
if referenceCount <= 0 {
|
||||||
|
managedObjectContext!.delete(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override func prepareForDeletion() {
|
||||||
|
super.prepareForDeletion()
|
||||||
|
reblog?.decrementReferenceCount()
|
||||||
|
account.decrementReferenceCount()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusMO {
|
extension StatusMO {
|
||||||
convenience init(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
|
convenience init(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
|
||||||
self.init(context: context)
|
self.init(context: context)
|
||||||
self.updateFrom(apiStatus: status, container: container)
|
self.updateFrom(apiStatus: status, container: container)
|
||||||
self.lastFetchedAt = Date()
|
|
||||||
|
reblog?.incrementReferenceCount()
|
||||||
|
account.incrementReferenceCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) {
|
func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21E230" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||||
<attribute name="acct" attributeType="String"/>
|
<attribute name="acct" attributeType="String"/>
|
||||||
<attribute name="avatar" optional="YES" attributeType="URI"/>
|
<attribute name="avatar" optional="YES" attributeType="URI"/>
|
||||||
|
@ -12,10 +12,10 @@
|
||||||
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="header" optional="YES" attributeType="URI"/>
|
<attribute name="header" optional="YES" attributeType="URI"/>
|
||||||
<attribute name="id" attributeType="String"/>
|
<attribute name="id" attributeType="String"/>
|
||||||
<attribute name="lastFetchedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
|
||||||
<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"/>
|
||||||
|
@ -40,23 +40,6 @@
|
||||||
<attribute name="showingReblogs" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="showingReblogs" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="relationship" inverseEntity="Account"/>
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="relationship" inverseEntity="Account"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SavedHashtag" representedClassName="SavedHashtag" syncable="YES">
|
|
||||||
<attribute name="name" attributeType="String"/>
|
|
||||||
<attribute name="url" attributeType="URI"/>
|
|
||||||
<uniquenessConstraints>
|
|
||||||
<uniquenessConstraint>
|
|
||||||
<constraint value="name"/>
|
|
||||||
</uniquenessConstraint>
|
|
||||||
</uniquenessConstraints>
|
|
||||||
</entity>
|
|
||||||
<entity name="SavedInstance" representedClassName="SavedInstance" syncable="YES">
|
|
||||||
<attribute name="url" attributeType="URI"/>
|
|
||||||
<uniquenessConstraints>
|
|
||||||
<uniquenessConstraint>
|
|
||||||
<constraint value="url"/>
|
|
||||||
</uniquenessConstraint>
|
|
||||||
</uniquenessConstraints>
|
|
||||||
</entity>
|
|
||||||
<entity name="Status" representedClassName="StatusMO" syncable="YES">
|
<entity name="Status" representedClassName="StatusMO" syncable="YES">
|
||||||
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
||||||
<attribute name="attachmentsData" attributeType="Binary"/>
|
<attribute name="attachmentsData" attributeType="Binary"/>
|
||||||
|
@ -71,7 +54,6 @@
|
||||||
<attribute name="id" attributeType="String"/>
|
<attribute name="id" attributeType="String"/>
|
||||||
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
|
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="lastFetchedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
|
||||||
<attribute name="localOnly" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="localOnly" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<attribute name="mentionsData" attributeType="Binary"/>
|
<attribute name="mentionsData" attributeType="Binary"/>
|
||||||
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
@ -79,6 +61,7 @@
|
||||||
<attribute name="pollData" optional="YES" attributeType="Binary"/>
|
<attribute name="pollData" optional="YES" attributeType="Binary"/>
|
||||||
<attribute name="reblogged" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="reblogged" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="referenceCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<attribute name="spoilerText" attributeType="String"/>
|
<attribute name="spoilerText" attributeType="String"/>
|
||||||
<attribute name="uri" attributeType="String"/>
|
<attribute name="uri" attributeType="String"/>
|
||||||
|
@ -93,10 +76,8 @@
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="343"/>
|
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="329"/>
|
||||||
<element name="Relationship" positionX="63" positionY="135" width="128" height="194"/>
|
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
|
||||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="449"/>
|
<element name="Status" positionX="-63" positionY="-18" width="128" height="449"/>
|
||||||
<element name="SavedInstance" positionX="63" positionY="144" width="128" height="44"/>
|
|
||||||
<element name="SavedHashtag" positionX="72" positionY="153" width="128" height="59"/>
|
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -24,19 +24,6 @@ extension Status.Visibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitle: String {
|
|
||||||
switch self {
|
|
||||||
case .public:
|
|
||||||
return "Everyone"
|
|
||||||
case .unlisted:
|
|
||||||
return "Hidden from public timelines"
|
|
||||||
case .private:
|
|
||||||
return "Followers only"
|
|
||||||
case .direct:
|
|
||||||
return "Mentioned users only"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var imageName: String {
|
var imageName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .public:
|
case .public:
|
||||||
|
|
|
@ -58,8 +58,8 @@ public struct LazilyDecoding<Enclosing, Value: Codable> {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LazilyDecoding {
|
extension LazilyDecoding {
|
||||||
init<T: Codable>(arrayFrom keyPath: ReferenceWritableKeyPath<Enclosing, Data?>) where Value == [T] {
|
init(arrayFrom keyPath: ReferenceWritableKeyPath<Enclosing, Data?>) {
|
||||||
self.init(from: keyPath, fallback: [])
|
self.init(from: keyPath, fallback: [] as! Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import CrashReporter
|
import CrashReporter
|
||||||
import MessageUI
|
import MessageUI
|
||||||
import CoreData
|
|
||||||
|
|
||||||
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
|
@ -18,17 +17,12 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
private var launchActivity: NSUserActivity?
|
private var launchActivity: NSUserActivity?
|
||||||
|
|
||||||
var rootViewController: TuskerRootViewController? {
|
|
||||||
window?.rootViewController as? TuskerRootViewController
|
|
||||||
}
|
|
||||||
|
|
||||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||||
guard let windowScene = scene as? UIWindowScene else { return }
|
guard let windowScene = scene as? UIWindowScene else { return }
|
||||||
|
|
||||||
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
||||||
launchActivity = activity
|
launchActivity = activity
|
||||||
}
|
}
|
||||||
stateRestorationLogger.info("MainSceneDelegate.launchActivity = \(self.launchActivity?.activityType ?? "nil", privacy: .public)")
|
|
||||||
|
|
||||||
window = UIWindow(windowScene: windowScene)
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
|
||||||
|
@ -62,7 +56,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
if url.host == "x-callback-url" {
|
if url.host == "x-callback-url" {
|
||||||
_ = XCBManager.handle(url: url)
|
_ = XCBManager.handle(url: url)
|
||||||
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||||
let rootViewController = rootViewController {
|
let rootViewController = window!.rootViewController as? TuskerRootViewController {
|
||||||
components.scheme = "https"
|
components.scheme = "https"
|
||||||
let query = components.string!
|
let query = components.string!
|
||||||
rootViewController.performSearch(query: query)
|
rootViewController.performSearch(query: query)
|
||||||
|
@ -70,8 +64,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
||||||
stateRestorationLogger.info("MainSceneDelegate.scene(_:continue:) called with \(userActivity.activityType, privacy: .public)")
|
_ = userActivity.handleResume()
|
||||||
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||||
|
@ -123,22 +116,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
rootVC.sceneDidEnterBackground()
|
rootVC.sceneDidEnterBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let context = scene.session.mastodonController?.persistentContainer.viewContext {
|
try! scene.session.mastodonController?.persistentContainer.viewContext.save()
|
||||||
var minDate = Date()
|
|
||||||
minDate.addTimeInterval(-7 * 24 * 60 * 60)
|
|
||||||
|
|
||||||
let statusReq: NSFetchRequest<NSFetchRequestResult> = StatusMO.fetchRequest()
|
|
||||||
statusReq.predicate = NSPredicate(format: "(lastFetchedAt = nil) OR (lastFetchedAt < %@)", minDate as NSDate)
|
|
||||||
let deleteStatusReq = NSBatchDeleteRequest(fetchRequest: statusReq)
|
|
||||||
_ = try? context.execute(deleteStatusReq)
|
|
||||||
|
|
||||||
let accountReq: NSFetchRequest<NSFetchRequestResult> = AccountMO.fetchRequest()
|
|
||||||
accountReq.predicate = NSPredicate(format: "(lastFetchedAt = nil) OR (lastFetchedAt < %@)", minDate as NSDate)
|
|
||||||
let deleteAccountReq = NSBatchDeleteRequest(fetchRequest: accountReq)
|
|
||||||
_ = try? context.execute(deleteAccountReq)
|
|
||||||
|
|
||||||
try? context.save()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handlePendingCrashReport(_ report: PLCrashReport, session: UISceneSession) {
|
private func handlePendingCrashReport(_ report: PLCrashReport, session: UISceneSession) {
|
||||||
|
@ -171,11 +149,6 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
activateAccount(account, animated: false)
|
activateAccount(account, animated: false)
|
||||||
|
|
||||||
if let activity = launchActivity,
|
|
||||||
activity.activityType != UserActivityType.mainScene.rawValue {
|
|
||||||
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
window!.rootViewController = createOnboardingUI()
|
window!.rootViewController = createOnboardingUI()
|
||||||
}
|
}
|
||||||
|
@ -229,12 +202,6 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAddAccount() {
|
|
||||||
rootViewController?.presentPreferences {
|
|
||||||
NotificationCenter.default.post(name: .addAccount, object: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSceneDelegate: OnboardingViewControllerDelegate {
|
extension MainSceneDelegate: OnboardingViewControllerDelegate {
|
||||||
|
|
|
@ -88,13 +88,8 @@ enum CompositionAttachmentData {
|
||||||
options.isNetworkAccessAllowed = true
|
options.isNetworkAccessAllowed = true
|
||||||
options.version = .current
|
options.version = .current
|
||||||
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
|
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
|
||||||
if let exportSession = exportSession {
|
guard let exportSession = exportSession else { fatalError("failed to create export session") }
|
||||||
CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion)
|
CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion)
|
||||||
} else if let error = info?[PHImageErrorKey] as? Error {
|
|
||||||
completion(.failure(.videoExport(error)))
|
|
||||||
} else {
|
|
||||||
completion(.failure(.noVideoExportSession))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fatalError("assetType must be either image or video")
|
fatalError("assetType must be either image or video")
|
||||||
|
@ -102,8 +97,7 @@ enum CompositionAttachmentData {
|
||||||
case let .video(url):
|
case let .video(url):
|
||||||
let asset = AVURLAsset(url: url)
|
let asset = AVURLAsset(url: url)
|
||||||
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
||||||
completion(.failure(.noVideoExportSession))
|
fatalError("failed to create export session")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
CompositionAttachmentData.exportVideoData(session: session, completion: completion)
|
CompositionAttachmentData.exportVideoData(session: session, completion: completion)
|
||||||
|
|
||||||
|
@ -118,14 +112,14 @@ enum CompositionAttachmentData {
|
||||||
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
||||||
session.exportAsynchronously {
|
session.exportAsynchronously {
|
||||||
guard session.status == .completed else {
|
guard session.status == .completed else {
|
||||||
completion(.failure(.videoExport(session.error!)))
|
completion(.failure(.export(session.error!)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: session.outputURL!)
|
let data = try Data(contentsOf: session.outputURL!)
|
||||||
completion(.success((data, "video/mp4")))
|
completion(.success((data, "video/mp4")))
|
||||||
} catch {
|
} catch {
|
||||||
completion(.failure(.videoExport(error)))
|
completion(.failure(.export(error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,21 +128,9 @@ enum CompositionAttachmentData {
|
||||||
case image, video
|
case image, video
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Error: Swift.Error, LocalizedError {
|
enum Error: Swift.Error {
|
||||||
case missingData
|
case missingData
|
||||||
case videoExport(Swift.Error)
|
case export(Swift.Error)
|
||||||
case noVideoExportSession
|
|
||||||
|
|
||||||
var localizedDescription: String {
|
|
||||||
switch self {
|
|
||||||
case .missingData:
|
|
||||||
return "Missing Data"
|
|
||||||
case .videoExport(let error):
|
|
||||||
return "Exporting video: \(error)"
|
|
||||||
case .noVideoExportSession:
|
|
||||||
return "Couldn't create video export session"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -226,8 +226,6 @@ extension MastodonController {
|
||||||
draft.visibility = visibility
|
draft.visibility = visibility
|
||||||
draft.contentWarning = contentWarning
|
draft.contentWarning = contentWarning
|
||||||
draft.contentWarningEnabled = !contentWarning.isEmpty
|
draft.contentWarningEnabled = !contentWarning.isEmpty
|
||||||
|
|
||||||
DraftsManager.shared.add(draft)
|
|
||||||
return draft
|
return draft
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,68 +8,101 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import CoreData
|
|
||||||
|
|
||||||
class SavedDataManager: Codable {
|
class SavedDataManager: Codable {
|
||||||
|
private(set) static var shared: SavedDataManager = load()
|
||||||
|
|
||||||
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist")
|
private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist")
|
||||||
|
|
||||||
static func load() -> SavedDataManager? {
|
static func save() {
|
||||||
|
DispatchQueue.global(qos: .utility).async {
|
||||||
|
let encoder = PropertyListEncoder()
|
||||||
|
let data = try? encoder.encode(shared)
|
||||||
|
try? data?.write(to: archiveURL, options: .noFileProtection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func load() -> SavedDataManager {
|
||||||
let decoder = PropertyListDecoder()
|
let decoder = PropertyListDecoder()
|
||||||
if let data = try? Data(contentsOf: archiveURL),
|
if let data = try? Data(contentsOf: archiveURL),
|
||||||
let savedHashtagsManager = try? decoder.decode(Self.self, from: data) {
|
let savedHashtagsManager = try? decoder.decode(Self.self, from: data) {
|
||||||
return savedHashtagsManager
|
return savedHashtagsManager
|
||||||
}
|
}
|
||||||
return nil
|
return SavedDataManager()
|
||||||
}
|
|
||||||
|
|
||||||
static func destroy() throws {
|
|
||||||
try FileManager.default.removeItem(at: archiveURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
private(set) var savedHashtags: [String: [Hashtag]] = [:]
|
private var savedHashtags: [String: [Hashtag]] = [:] {
|
||||||
private(set) var savedInstances: [String: [URL]] = [:]
|
didSet {
|
||||||
|
SavedDataManager.save()
|
||||||
var accountIDs: Set<String> {
|
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
|
||||||
var s = Set<String>()
|
}
|
||||||
savedHashtags.keys.forEach { s.insert($0) }
|
|
||||||
savedInstances.keys.forEach { s.insert($0) }
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() {
|
private var savedInstances: [String: [URL]] = [:] {
|
||||||
let encoder = PropertyListEncoder()
|
didSet {
|
||||||
let data = try? encoder.encode(self)
|
SavedDataManager.save()
|
||||||
try? data?.write(to: SavedDataManager.archiveURL, options: .noFileProtection)
|
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToCoreData(accountID: String, context: NSManagedObjectContext) throws {
|
func sortedHashtags(for account: LocalData.UserAccountInfo) -> [Hashtag] {
|
||||||
var changed = false
|
if let hashtags = savedHashtags[account.id] {
|
||||||
|
return hashtags.sorted(by: { $0.name < $1.name })
|
||||||
if let hashtags = savedHashtags[accountID] {
|
} else {
|
||||||
let objects = hashtags.map {
|
return []
|
||||||
["url": $0.url, "name": $0.name]
|
|
||||||
}
|
|
||||||
let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects)
|
|
||||||
try context.execute(hashtagsReq)
|
|
||||||
savedHashtags.removeValue(forKey: accountID)
|
|
||||||
changed = true
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let instances = savedInstances[accountID] {
|
func isSaved(hashtag: Hashtag, for account: LocalData.UserAccountInfo) -> Bool {
|
||||||
let objects = instances.map {
|
return savedHashtags[account.id]?.contains(hashtag) ?? false
|
||||||
["url": $0]
|
}
|
||||||
}
|
|
||||||
let instancesReq = NSBatchInsertRequest(entity: SavedInstance.entity(), objects: objects)
|
func add(hashtag: Hashtag, for account: LocalData.UserAccountInfo) {
|
||||||
try context.execute(instancesReq)
|
if isSaved(hashtag: hashtag, for: account) {
|
||||||
savedInstances.removeValue(forKey: accountID)
|
return
|
||||||
changed = true
|
|
||||||
}
|
}
|
||||||
|
if var saved = savedHashtags[account.id] {
|
||||||
|
saved.append(hashtag)
|
||||||
|
savedHashtags[account.id] = saved
|
||||||
|
} else {
|
||||||
|
savedHashtags[account.id] = [hashtag]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if changed {
|
func remove(hashtag: Hashtag, for account: LocalData.UserAccountInfo) {
|
||||||
save()
|
guard isSaved(hashtag: hashtag, for: account) else { return }
|
||||||
|
if var saved = savedHashtags[account.id] {
|
||||||
|
saved.removeAll(where: { $0.name == hashtag.name })
|
||||||
|
savedHashtags[account.id] = saved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func savedInstances(for account: LocalData.UserAccountInfo) -> [URL] {
|
||||||
|
return savedInstances[account.id] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSaved(instance url: URL, for account: LocalData.UserAccountInfo) -> Bool {
|
||||||
|
return savedInstances[account.id]?.contains(url) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(instance url: URL, for account: LocalData.UserAccountInfo) {
|
||||||
|
if isSaved(instance: url, for: account) { return }
|
||||||
|
if var saved = savedInstances[account.id] {
|
||||||
|
saved.append(url)
|
||||||
|
savedInstances[account.id] = saved
|
||||||
|
} else {
|
||||||
|
savedInstances[account.id] = [url]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(instance url: URL, for account: LocalData.UserAccountInfo) {
|
||||||
|
guard isSaved(instance: url, for: account) else { return }
|
||||||
|
if var saved = savedInstances[account.id] {
|
||||||
|
saved.removeAll(where: { $0 == url })
|
||||||
|
savedInstances[account.id] = saved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,9 +67,3 @@ class AccountListTableViewController: EnhancedTableViewController {
|
||||||
extension AccountListTableViewController: TuskerNavigationDelegate {
|
extension AccountListTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountListTableViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AccountListTableViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
|
@ -134,7 +134,7 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
||||||
let request = Status.unbookmark(status.id)
|
let request = Status.unbookmark(status.id)
|
||||||
self.mastodonController.run(request) { (response) in
|
self.mastodonController.run(request) { (response) in
|
||||||
guard case let .success(newStatus, _) = response else { fatalError() }
|
guard case let .success(newStatus, _) = response else { fatalError() }
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus)
|
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
|
||||||
self.statuses.remove(at: indexPath.row)
|
self.statuses.remove(at: indexPath.row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,15 +153,6 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BookmarksTableViewController: TuskerNavigationDelegate {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BookmarksTableViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BookmarksTableViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BookmarksTableViewController: StatusTableViewCellDelegate {
|
extension BookmarksTableViewController: StatusTableViewCellDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
|
|
||||||
|
|
|
@ -376,8 +376,7 @@ struct ComposeAutocompleteHashtagsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateHashtags(searchResults: [Hashtag], trendingTags: [Hashtag], query: String) {
|
private func updateHashtags(searchResults: [Hashtag], trendingTags: [Hashtag], query: String) {
|
||||||
let savedTags = ((try? mastodonController.persistentContainer.viewContext.fetch(SavedHashtag.fetchRequest())) ?? [])
|
let savedTags = SavedDataManager.shared.sortedHashtags(for: mastodonController.accountInfo!)
|
||||||
.map { Hashtag(name: $0.name, url: $0.url) }
|
|
||||||
|
|
||||||
hashtags = (searchResults + savedTags + trendingTags)
|
hashtags = (searchResults + savedTags + trendingTags)
|
||||||
.map { (tag) -> (Hashtag, (matched: Bool, score: Int)) in
|
.map { (tag) -> (Hashtag, (matched: Bool, score: Int)) in
|
||||||
|
|
|
@ -52,9 +52,6 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
|
|
||||||
view.backgroundColor = backgroundColor
|
view.backgroundColor = backgroundColor
|
||||||
|
|
||||||
// otherwise when the text gets too wide it starts expanding the ComposeView
|
|
||||||
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
||||||
|
|
||||||
context.coordinator.textField = view
|
context.coordinator.textField = view
|
||||||
context.coordinator.uiState = uiState
|
context.coordinator.uiState = uiState
|
||||||
context.coordinator.text = $text
|
context.coordinator.text = $text
|
||||||
|
|
|
@ -250,7 +250,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
||||||
let state = visibility == newVisibility ? UIMenuElement.State.on : .off
|
let state = visibility == newVisibility ? UIMenuElement.State.on : .off
|
||||||
return UIAction(title: visibility.displayName, subtitle: visibility.subtitle, image: UIImage(systemName: visibility.unfilledImageName), state: state) { [unowned self] (_) in
|
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { [unowned self] (_) in
|
||||||
self.draft.visibility = visibility
|
self.draft.visibility = visibility
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,9 +270,9 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
item.image = UIImage(systemName: "link")
|
item.image = UIImage(systemName: "link")
|
||||||
item.accessibilityLabel = "Federated"
|
item.accessibilityLabel = "Federated"
|
||||||
}
|
}
|
||||||
let instanceSubtitle = "Only \(mastodonController.accountInfo!.instanceURL.host!)"
|
|
||||||
item.menu = UIMenu(children: [
|
item.menu = UIMenu(children: [
|
||||||
UIAction(title: "Local-only", subtitle: instanceSubtitle, image: UIImage(named: "link.broken"), state: localOnly ? .on : .off) { [unowned self] (_) in
|
// todo: iOS 15, action subtitles
|
||||||
|
UIAction(title: "Local-only", image: UIImage(named: "link.broken"), state: localOnly ? .on : .off) { [unowned self] (_) in
|
||||||
self.draft.localOnly = true
|
self.draft.localOnly = true
|
||||||
},
|
},
|
||||||
UIAction(title: "Federated", image: UIImage(systemName: "link"), state: localOnly ? .off : .on) { [unowned self] (_) in
|
UIAction(title: "Federated", image: UIImage(systemName: "link"), state: localOnly ? .off : .on) { [unowned self] (_) in
|
||||||
|
@ -474,13 +474,3 @@ extension ComposeHostingController: ComposeDrawingViewControllerDelegate {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension UIAction {
|
|
||||||
convenience init(title: String, subtitle: String?, image: UIImage?, state: UIAction.State, handler: @escaping UIActionHandler) {
|
|
||||||
if #available(iOS 15.0, *) {
|
|
||||||
self.init(title: title, subtitle: subtitle, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
|
||||||
} else {
|
|
||||||
self.init(title: title, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,49 +10,16 @@ import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
@propertyWrapper struct OptionalStateObject<T: ObservableObject>: DynamicProperty {
|
|
||||||
private class Republisher: ObservableObject {
|
|
||||||
var cancellable: AnyCancellable?
|
|
||||||
var wrapped: T? {
|
|
||||||
didSet {
|
|
||||||
cancellable?.cancel()
|
|
||||||
cancellable = wrapped?.objectWillChange
|
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.sink { [unowned self] _ in
|
|
||||||
self.objectWillChange.send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@StateObject private var republisher = Republisher()
|
|
||||||
@State private var object: T?
|
|
||||||
var wrappedValue: T? {
|
|
||||||
get {
|
|
||||||
object
|
|
||||||
}
|
|
||||||
nonmutating set {
|
|
||||||
object = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func update() {
|
|
||||||
republisher.wrapped = wrappedValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ComposeView: View {
|
struct ComposeView: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@EnvironmentObject var mastodonController: MastodonController
|
@EnvironmentObject var mastodonController: MastodonController
|
||||||
@EnvironmentObject var uiState: ComposeUIState
|
@EnvironmentObject var uiState: ComposeUIState
|
||||||
|
|
||||||
@OptionalStateObject private var poster: PostService?
|
@State private var isPosting = false
|
||||||
|
@State private var postProgress: Double = 0
|
||||||
|
@State private var postTotalProgress: Double = 0
|
||||||
@State private var isShowingPostErrorAlert = false
|
@State private var isShowingPostErrorAlert = false
|
||||||
@State private var postError: PostService.Error?
|
@State private var postError: PostError?
|
||||||
|
|
||||||
private var isPosting: Bool {
|
|
||||||
poster != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private let stackPadding: CGFloat = 8
|
private let stackPadding: CGFloat = 8
|
||||||
|
|
||||||
|
@ -91,9 +58,9 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let poster = poster {
|
if postProgress > 0 {
|
||||||
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
||||||
WrappedProgressView(value: poster.currentStep, total: poster.totalSteps)
|
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
autocompleteSuggestions
|
autocompleteSuggestions
|
||||||
|
@ -156,7 +123,6 @@ struct ComposeView: View {
|
||||||
// the list rows provide their own padding, so we cancel out the extra spacing from the VStack
|
// the list rows provide their own padding, so we cancel out the extra spacing from the VStack
|
||||||
.padding([.top, .bottom], -8)
|
.padding([.top, .bottom], -8)
|
||||||
}
|
}
|
||||||
.disabled(isPosting)
|
|
||||||
.padding(stackPadding)
|
.padding(stackPadding)
|
||||||
.padding(.bottom, uiState.autocompleteState != nil ? 46 : nil)
|
.padding(.bottom, uiState.autocompleteState != nil ? 46 : nil)
|
||||||
}
|
}
|
||||||
|
@ -181,11 +147,7 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var postButton: some View {
|
private var postButton: some View {
|
||||||
Button {
|
Button(action: self.postStatus) {
|
||||||
Task {
|
|
||||||
await self.postStatus()
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("Post")
|
Text("Post")
|
||||||
}
|
}
|
||||||
.disabled(!postButtonEnabled)
|
.disabled(!postButtonEnabled)
|
||||||
|
@ -222,31 +184,177 @@ struct ComposeView: View {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func postStatus() async {
|
private func postStatus() {
|
||||||
guard !isPosting,
|
guard draft.hasContent else { return }
|
||||||
draft.hasContent else {
|
|
||||||
return
|
isPosting = true
|
||||||
|
|
||||||
|
// save before posting, so if a crash occurs during network request, the status won't be lost
|
||||||
|
DraftsManager.save()
|
||||||
|
|
||||||
|
let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : nil
|
||||||
|
let sensitive = contentWarning != nil
|
||||||
|
|
||||||
|
// 2 steps (request data, then upload) for each attachment
|
||||||
|
postTotalProgress = Double(2 + (draft.attachments.count * 2))
|
||||||
|
postProgress = 1
|
||||||
|
|
||||||
|
uploadAttachments { (result) in
|
||||||
|
switch result {
|
||||||
|
case let .failure(error):
|
||||||
|
self.isShowingPostErrorAlert = true
|
||||||
|
self.postError = error
|
||||||
|
self.postProgress = 0
|
||||||
|
self.postTotalProgress = 0
|
||||||
|
self.isPosting = false
|
||||||
|
|
||||||
|
case let .success(uploadedAttachments):
|
||||||
|
let request = Client.createStatus(text: draft.textForPosting(on: mastodonController.instanceFeatures),
|
||||||
|
contentType: Preferences.shared.statusContentType,
|
||||||
|
inReplyTo: draft.inReplyToID,
|
||||||
|
media: uploadedAttachments,
|
||||||
|
sensitive: sensitive,
|
||||||
|
spoilerText: contentWarning,
|
||||||
|
visibility: draft.visibility,
|
||||||
|
language: nil,
|
||||||
|
pollOptions: draft.poll?.options.map(\.text),
|
||||||
|
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
||||||
|
pollMultiple: draft.poll?.multiple,
|
||||||
|
localOnly: mastodonController.instanceFeatures.instanceType == .hometown ? draft.localOnly : nil)
|
||||||
|
self.mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
self.isShowingPostErrorAlert = true
|
||||||
|
self.postError = error
|
||||||
|
|
||||||
|
case .success(_, _):
|
||||||
|
self.postProgress += 1
|
||||||
|
|
||||||
|
DraftsManager.shared.remove(self.draft)
|
||||||
|
|
||||||
|
// wait .25 seconds so the user can see the progress bar has completed
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
||||||
|
self.uiState.delegate?.dismissCompose(mode: .post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
|
||||||
|
let group = DispatchGroup()
|
||||||
|
|
||||||
|
var attachmentDataResults = [Result<(Data, String), CompositionAttachmentData.Error>?]()
|
||||||
|
|
||||||
|
for (index, compAttachment) in draft.attachments.enumerated() {
|
||||||
|
group.enter()
|
||||||
|
|
||||||
|
attachmentDataResults.append(nil)
|
||||||
|
|
||||||
|
compAttachment.data.getData { (result) in
|
||||||
|
postProgress += 1
|
||||||
|
|
||||||
|
attachmentDataResults[index] = result
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let poster = PostService(mastodonController: mastodonController, draft: draft)
|
group.notify(queue: .global(qos: .userInitiated)) {
|
||||||
self.poster = poster
|
|
||||||
|
|
||||||
do {
|
var anyFailed = false
|
||||||
try await poster.post()
|
var uploadedAttachments = [Result<Attachment, Error>?]()
|
||||||
|
|
||||||
// wait .25 seconds so the user can see the progress bar has completed
|
// Mastodon does not respect the order of the `media_ids` parameter in the create post request,
|
||||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
// it determines attachment order by which was uploaded first. Since the upload attachment request
|
||||||
|
// does not include any timestamp data, and requests may arrive at the server out-of-order,
|
||||||
|
// attachments need to be uploaded serially in order to ensure the order of attachments in the
|
||||||
|
// posted status reflects order the user set.
|
||||||
|
// Pleroma does respect the order of the `media_ids` parameter.
|
||||||
|
|
||||||
uiState.delegate?.dismissCompose(mode: .post)
|
let datas: [(Data, String)]
|
||||||
|
do {
|
||||||
|
datas = try attachmentDataResults.map { try $0!.get() }
|
||||||
|
} catch {
|
||||||
|
completion(.failure(AttachmentUploadError(errors: [error])))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, (data, mimeType)) in datas.enumerated() {
|
||||||
|
group.enter()
|
||||||
|
|
||||||
|
let compAttachment = draft.attachments[index]
|
||||||
|
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
||||||
|
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
||||||
|
self.mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
uploadedAttachments.append(.failure(error))
|
||||||
|
anyFailed = true
|
||||||
|
|
||||||
|
case let .success(attachment, _):
|
||||||
|
self.postProgress += 1
|
||||||
|
uploadedAttachments.append(.success(attachment))
|
||||||
|
}
|
||||||
|
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
|
||||||
|
group.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if anyFailed {
|
||||||
|
let errors = uploadedAttachments.map { (result) -> Error? in
|
||||||
|
if case let .failure(error) = result {
|
||||||
|
return error
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completion(.failure(AttachmentUploadError(errors: errors)))
|
||||||
|
} else {
|
||||||
|
let uploadedAttachments = uploadedAttachments.map {
|
||||||
|
try! $0!.get()
|
||||||
|
}
|
||||||
|
completion(.success(uploadedAttachments))
|
||||||
|
}
|
||||||
|
|
||||||
} catch let error as PostService.Error {
|
|
||||||
self.isShowingPostErrorAlert = true
|
|
||||||
self.postError = error
|
|
||||||
} catch {
|
|
||||||
fatalError("Unreachable")
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.poster = nil
|
fileprivate protocol PostError: LocalizedError {}
|
||||||
|
|
||||||
|
extension PostError {
|
||||||
|
var localizedDescription: String {
|
||||||
|
if let self = self as? Client.Error {
|
||||||
|
return self.localizedDescription
|
||||||
|
} else if let self = self as? AttachmentUploadError {
|
||||||
|
return self.localizedDescription
|
||||||
|
} else {
|
||||||
|
return "Unknown Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Client.Error: PostError {}
|
||||||
|
|
||||||
|
fileprivate struct AttachmentUploadError: PostError {
|
||||||
|
let errors: [Error?]
|
||||||
|
|
||||||
|
var localizedDescription: String {
|
||||||
|
return errors.enumerated().compactMap { (index, error) -> String? in
|
||||||
|
guard let error = error else { return nil }
|
||||||
|
let description: String
|
||||||
|
// need to downcast to use more specific localizedDescription impl from Pachyderm
|
||||||
|
if let error = error as? Client.Error {
|
||||||
|
description = error.localizedDescription
|
||||||
|
} else {
|
||||||
|
description = error.localizedDescription
|
||||||
|
}
|
||||||
|
return "Attachment \(index + 1): \(description)"
|
||||||
|
}.joined(separator: ",\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
let mainStatusID: String
|
let mainStatusID: String
|
||||||
let mainStatusState: StatusState
|
let mainStatusState: StatusState
|
||||||
var statusIDToScrollToOnLoad: String
|
|
||||||
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
var showStatusesAutomatically = false
|
var showStatusesAutomatically = false
|
||||||
|
@ -42,7 +41,6 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) {
|
init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) {
|
||||||
self.mainStatusID = mainStatusID
|
self.mainStatusID = mainStatusID
|
||||||
self.mainStatusState = state
|
self.mainStatusState = state
|
||||||
self.statusIDToScrollToOnLoad = mainStatusID
|
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(style: .plain)
|
super.init(style: .plain)
|
||||||
|
@ -54,6 +52,14 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
guard let persistentContainer = mastodonController?.persistentContainer else { return }
|
||||||
|
let snapshot = dataSource.snapshot()
|
||||||
|
for case let .status(id: id, state: _) in snapshot.itemIdentifiers {
|
||||||
|
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -117,59 +123,54 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
|
|
||||||
case let .expandThread(childThreads: childThreads, inline: inline):
|
case let .expandThread(childThreads: childThreads):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "expandThreadCell", for: indexPath) as! ExpandThreadTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: "expandThreadCell", for: indexPath) as! ExpandThreadTableViewCell
|
||||||
cell.updateUI(childThreads: childThreads, inline: inline)
|
cell.updateUI(childThreads: childThreads)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if #available(iOS 15.0, *) {
|
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
|
||||||
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||||
visibilityBarButtonItem.isSelected = showStatusesAutomatically
|
|
||||||
} else {
|
|
||||||
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
|
|
||||||
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
|
||||||
}
|
|
||||||
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
||||||
// disable transparent background when scroll to top because it looks weird when items earlier in the thread load in
|
|
||||||
// (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar)
|
|
||||||
let appearance = UINavigationBarAppearance()
|
|
||||||
appearance.configureWithDefaultBackground()
|
|
||||||
navigationItem.scrollEdgeAppearance = appearance
|
|
||||||
|
|
||||||
Task {
|
loadMainStatus()
|
||||||
await loadMainStatus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
private func loadMainStatus() {
|
||||||
private func loadMainStatus() async {
|
|
||||||
guard loadingState == .unloaded else { return }
|
guard loadingState == .unloaded else { return }
|
||||||
|
|
||||||
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
|
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
|
||||||
await mainStatusLoaded(mainStatus)
|
self.mainStatusLoaded(mainStatus)
|
||||||
} else {
|
} else {
|
||||||
loadingState = .loadingMain
|
loadingState = .loadingMain
|
||||||
let req = Client.getStatus(id: mainStatusID)
|
let request = Client.getStatus(id: mainStatusID)
|
||||||
do {
|
mastodonController.run(request) { (response) in
|
||||||
let (status, _) = try await mastodonController.run(req)
|
switch response {
|
||||||
let statusMO = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
case let .success(status, _):
|
||||||
await mainStatusLoaded(statusMO)
|
let viewContext = self.mastodonController.persistentContainer.viewContext
|
||||||
} catch {
|
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false, context: viewContext) { (statusMO) in
|
||||||
let error = error as! Client.Error
|
self.mainStatusLoaded(statusMO)
|
||||||
loadingState = .unloaded
|
}
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] toast in
|
|
||||||
toast.dismissToast(animated: true)
|
case let .failure(error):
|
||||||
await self?.loadMainStatus()
|
DispatchQueue.main.async {
|
||||||
|
self.loadingState = .unloaded
|
||||||
|
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
self?.loadMainStatus()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
showToast(configuration: config, animated: true)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mainStatusLoaded(_ mainStatus: StatusMO) async {
|
private func mainStatusLoaded(_ mainStatus: StatusMO) {
|
||||||
|
mainStatus.incrementReferenceCount()
|
||||||
|
|
||||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
@ -179,11 +180,10 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
loadingState = .loadedMain
|
loadingState = .loadedMain
|
||||||
|
|
||||||
await loadContext(for: mainStatus)
|
loadContext(for: mainStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
private func loadContext(for mainStatus: StatusMO) {
|
||||||
private func loadContext(for mainStatus: StatusMO) async {
|
|
||||||
guard loadingState == .loadedMain else { return }
|
guard loadingState == .loadedMain else { return }
|
||||||
|
|
||||||
loadingState = .loadingContext
|
loadingState = .loadingContext
|
||||||
|
@ -193,24 +193,30 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
// todo: it would be nice to cache these contexts
|
// todo: it would be nice to cache these contexts
|
||||||
let request = Status.getContext(mainStatusID)
|
let request = Status.getContext(mainStatusID)
|
||||||
do {
|
mastodonController.run(request) { response in
|
||||||
let (context, _) = try await mastodonController.run(request)
|
switch response {
|
||||||
let parentIDs = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
case let .success(context, _):
|
||||||
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
let parentIDs = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
||||||
|
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
||||||
|
|
||||||
// todo: should this really be blindly adding all the descendants?
|
// todo: should this really be blindly adding all the descendants?
|
||||||
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
|
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants) {
|
||||||
self.contextLoaded(mainStatus: mainStatus, context: context, parentIDs: parentIDs)
|
DispatchQueue.main.async {
|
||||||
|
self.contextLoaded(mainStatus: mainStatus, context: context, parentIDs: parentIDs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
case let .failure(error):
|
||||||
let error = error as! Client.Error
|
DispatchQueue.main.async {
|
||||||
self.loadingState = .loadedMain
|
self.loadingState = .loadedMain
|
||||||
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Content", in: self) { [weak self] (toast) in
|
let config = ToastConfiguration(from: error, with: "Error Loading Content", in: self) { [weak self] (toast) in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
await self?.loadContext(for: mainStatus)
|
self?.loadContext(for: mainStatus)
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,19 +240,10 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
let item: Item
|
// ensure that the main status is on-screen after newly loaded statuses are added
|
||||||
let position: UITableView.ScrollPosition
|
|
||||||
if self.statusIDToScrollToOnLoad == self.mainStatusID {
|
|
||||||
item = mainStatusItem
|
|
||||||
position = .middle
|
|
||||||
} else {
|
|
||||||
item = Item.status(id: self.statusIDToScrollToOnLoad, state: .unknown)
|
|
||||||
position = .top
|
|
||||||
}
|
|
||||||
// ensure that the status is on-screen after newly loaded statuses are added
|
|
||||||
// todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
|
// todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
|
||||||
if let indexPath = self.dataSource.indexPath(for: item) {
|
if let indexPath = self.dataSource.indexPath(for: mainStatusItem) {
|
||||||
self.tableView.scrollToRow(at: indexPath, at: position, animated: false)
|
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,10 +324,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
let sameAuthorStatuses = currentNode.children.filter({ $0.status.account.id == node.status.account.id })
|
let sameAuthorStatuses = currentNode.children.filter({ $0.status.account.id == node.status.account.id })
|
||||||
if sameAuthorStatuses.count == 1 {
|
if sameAuthorStatuses.count == 1 {
|
||||||
next = sameAuthorStatuses[0]
|
next = sameAuthorStatuses[0]
|
||||||
let nonSameAuthorChildren = currentNode.children.filter { $0.status.id != sameAuthorStatuses[0].status.id }
|
|
||||||
snapshot.appendItems([.expandThread(childThreads: nonSameAuthorChildren, inline: true)])
|
|
||||||
} else {
|
} else {
|
||||||
snapshot.appendItems([.expandThread(childThreads: currentNode.children, inline: false)])
|
snapshot.appendItems([.expandThread(childThreads: currentNode.children)])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -355,11 +350,9 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
// MARK: - Table view delegate
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
if case let .expandThread(childThreads: childThreads, inline: _) = dataSource.itemIdentifier(for: indexPath),
|
if case .expandThread = dataSource.itemIdentifier(for: indexPath),
|
||||||
case let .status(id: id, state: state) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
|
case let .status(id: id, state: state) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
|
||||||
let conv = ConversationTableViewController(for: id, state: state, mastodonController: mastodonController)
|
self.selected(status: id, state: state)
|
||||||
conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
|
|
||||||
show(conv)
|
|
||||||
} else {
|
} else {
|
||||||
super.tableView(tableView, didSelectRowAt: indexPath)
|
super.tableView(tableView, didSelectRowAt: indexPath)
|
||||||
}
|
}
|
||||||
|
@ -416,14 +409,14 @@ extension ConversationTableViewController {
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case status(id: String, state: StatusState)
|
case status(id: String, state: StatusState)
|
||||||
case expandThread(childThreads: [ConversationNode], inline: Bool)
|
case expandThread(childThreads: [ConversationNode])
|
||||||
|
|
||||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
||||||
return a == b
|
return a == b
|
||||||
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
|
case let (.expandThread(childThreads: a), .expandThread(childThreads: b)):
|
||||||
return zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
|
return zip(a, b).allSatisfy { $0.status.id == $1.status.id }
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -434,10 +427,9 @@ extension ConversationTableViewController {
|
||||||
case let .status(id: id, state: _):
|
case let .status(id: id, state: _):
|
||||||
hasher.combine("status")
|
hasher.combine("status")
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
case let .expandThread(childThreads: children, inline: inline):
|
case let .expandThread(childThreads: children):
|
||||||
hasher.combine("expandThread")
|
hasher.combine("expandThread")
|
||||||
hasher.combine(children.map(\.status.id))
|
hasher.combine(children.map(\.status.id))
|
||||||
hasher.combine(inline)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -462,9 +454,6 @@ extension ConversationTableViewController: TuskerNavigationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConversationTableViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ConversationTableViewController: StatusTableViewCellDelegate {
|
extension ConversationTableViewController: StatusTableViewCellDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
|
|
|
@ -10,49 +10,34 @@ import UIKit
|
||||||
|
|
||||||
class ExpandThreadTableViewCell: UITableViewCell {
|
class ExpandThreadTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
@IBOutlet weak var stackViewLeadingConstraint: NSLayoutConstraint!
|
|
||||||
@IBOutlet weak var avatarContainerView: UIView!
|
@IBOutlet weak var avatarContainerView: UIView!
|
||||||
@IBOutlet weak var avatarContainerWidthConstraint: NSLayoutConstraint!
|
@IBOutlet weak var avatarContainerWidthConstraint: NSLayoutConstraint!
|
||||||
@IBOutlet weak var replyCountLabel: UILabel!
|
@IBOutlet weak var replyCountLabel: UILabel!
|
||||||
private var threadLinkView: UIView!
|
var avatarImageViews: [UIImageView] = []
|
||||||
private var threadLinkViewFullHeightConstraint: NSLayoutConstraint!
|
|
||||||
private var threadLinkViewShortHeightConstraint: NSLayoutConstraint!
|
|
||||||
private var avatarImageViews: [UIImageView] = []
|
|
||||||
|
|
||||||
private var avatarRequests: [ImageCache.Request] = []
|
private var avatarRequests: [ImageCache.Request] = []
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
threadLinkView = UIView()
|
let prevThreadLinkView = UIView()
|
||||||
threadLinkView.translatesAutoresizingMaskIntoConstraints = false
|
prevThreadLinkView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
threadLinkView.backgroundColor = tintColor.withAlphaComponent(0.5)
|
prevThreadLinkView.backgroundColor = tintColor.withAlphaComponent(0.5)
|
||||||
threadLinkView.layer.cornerRadius = 2.5
|
prevThreadLinkView.layer.cornerRadius = 2.5
|
||||||
addSubview(threadLinkView)
|
prevThreadLinkView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
threadLinkViewFullHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
contentView.addSubview(prevThreadLinkView)
|
||||||
threadLinkViewShortHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2)
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
threadLinkView.widthAnchor.constraint(equalToConstant: 5),
|
prevThreadLinkView.widthAnchor.constraint(equalToConstant: 5),
|
||||||
threadLinkView.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 25 + 16 /* system spacing */),
|
prevThreadLinkView.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 16 + 25),
|
||||||
threadLinkView.topAnchor.constraint(equalTo: topAnchor),
|
prevThreadLinkView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
threadLinkViewFullHeightConstraint,
|
prevThreadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2),
|
||||||
])
|
])
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(childThreads: [ConversationNode], inline: Bool) {
|
func updateUI(childThreads: [ConversationNode]) {
|
||||||
stackViewLeadingConstraint.constant = inline ? 50 + 4 : 0
|
let format = NSLocalizedString("expand threads count", comment: "expand conversation threads button label")
|
||||||
threadLinkView.layer.maskedCorners = inline ? [] : [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
||||||
threadLinkViewFullHeightConstraint.isActive = inline
|
|
||||||
threadLinkViewShortHeightConstraint.isActive = !inline
|
|
||||||
|
|
||||||
let format: String
|
|
||||||
if inline {
|
|
||||||
format = NSLocalizedString("expand threads inline count", comment: "expnad converstaion threads inline button label")
|
|
||||||
} else {
|
|
||||||
format = NSLocalizedString("expand threads count", comment: "expand conversation threads button label")
|
|
||||||
}
|
|
||||||
replyCountLabel.text = String.localizedStringWithFormat(format, childThreads.count)
|
replyCountLabel.text = String.localizedStringWithFormat(format, childThreads.count)
|
||||||
|
|
||||||
let accounts = childThreads.map(\.status.account).uniques().prefix(3)
|
let accounts = childThreads.map(\.status.account).uniques().prefix(3)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
@ -62,7 +62,6 @@
|
||||||
<outlet property="avatarContainerView" destination="eFB-F1-d3A" id="xGo-40-nn7"/>
|
<outlet property="avatarContainerView" destination="eFB-F1-d3A" id="xGo-40-nn7"/>
|
||||||
<outlet property="avatarContainerWidthConstraint" destination="tiI-Rj-gjh" id="34n-ev-EKi"/>
|
<outlet property="avatarContainerWidthConstraint" destination="tiI-Rj-gjh" id="34n-ev-EKi"/>
|
||||||
<outlet property="replyCountLabel" destination="Dcm-ll-GeE" id="E4m-xk-DiQ"/>
|
<outlet property="replyCountLabel" destination="Dcm-ll-GeE" id="E4m-xk-DiQ"/>
|
||||||
<outlet property="stackViewLeadingConstraint" destination="iD5-Av-ORS" id="Try-cG-8uA"/>
|
|
||||||
</connections>
|
</connections>
|
||||||
<point key="canvasLocation" x="132" y="132"/>
|
<point key="canvasLocation" x="132" y="132"/>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
|
|
|
@ -123,7 +123,6 @@ extension DraftsTableViewController: UITableViewDragDelegate {
|
||||||
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
let draft = self.draft(for: indexPath)
|
let draft = self.draft(for: indexPath)
|
||||||
let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: account.id)
|
let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: account.id)
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
let provider = NSItemProvider(object: activity)
|
let provider = NSItemProvider(object: activity)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,9 +86,7 @@ class AddSavedHashtagViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectHashtag(_ hashtag: Hashtag) {
|
private func selectHashtag(_ hashtag: Hashtag) {
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!)
|
||||||
_ = SavedHashtag(hashtag: hashtag, context: context)
|
|
||||||
try! context.save()
|
|
||||||
presentingViewController!.dismiss(animated: true)
|
presentingViewController!.dismiss(animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import CoreData
|
|
||||||
|
|
||||||
class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
|
|
||||||
|
@ -135,6 +134,8 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyInitialSnapshot() {
|
private func applyInitialSnapshot() {
|
||||||
|
let account = mastodonController.accountInfo!
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
|
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
|
||||||
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
|
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
|
||||||
|
@ -143,15 +144,9 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
addDiscoverSection(to: &snapshot)
|
addDiscoverSection(to: &snapshot)
|
||||||
}
|
}
|
||||||
snapshot.appendItems([.addList], toSection: .lists)
|
snapshot.appendItems([.addList], toSection: .lists)
|
||||||
let hashtags = fetchSavedHashtags().map {
|
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) }, toSection: .savedHashtags)
|
||||||
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
|
|
||||||
}
|
|
||||||
snapshot.appendItems(hashtags, toSection: .savedHashtags)
|
|
||||||
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
|
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
|
||||||
let instances = fetchSavedInstances().map {
|
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) }, toSection: .savedInstances)
|
||||||
Item.savedInstance($0.url)
|
|
||||||
}
|
|
||||||
snapshot.appendItems(instances, toSection: .savedInstances)
|
|
||||||
snapshot.appendItems([.findInstance], toSection: .savedInstances)
|
snapshot.appendItems([.findInstance], toSection: .savedInstances)
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
@ -195,46 +190,20 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func fetchSavedHashtags() -> [SavedHashtag] {
|
|
||||||
let req = SavedHashtag.fetchRequest()
|
|
||||||
req.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true, selector: #selector(NSString.localizedCompare(_:)))]
|
|
||||||
do {
|
|
||||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func fetchSavedInstances() -> [SavedInstance] {
|
|
||||||
let req = SavedInstance.fetchRequest()
|
|
||||||
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
|
|
||||||
do {
|
|
||||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func savedHashtagsChanged() {
|
@objc private func savedHashtagsChanged() {
|
||||||
|
let account = mastodonController.accountInfo!
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
|
||||||
let hashtags = fetchSavedHashtags().map {
|
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) }, toSection: .savedHashtags)
|
||||||
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
|
|
||||||
}
|
|
||||||
snapshot.appendItems(hashtags, toSection: .savedHashtags)
|
|
||||||
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
|
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
|
||||||
dataSource.apply(snapshot)
|
dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func savedInstancesChanged() {
|
@objc private func savedInstancesChanged() {
|
||||||
|
let account = mastodonController.accountInfo!
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
|
||||||
let instances = fetchSavedInstances().map {
|
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) }, toSection: .savedInstances)
|
||||||
Item.savedInstance($0.url)
|
|
||||||
}
|
|
||||||
snapshot.appendItems(instances, toSection: .savedInstances)
|
|
||||||
snapshot.appendItems([.findInstance], toSection: .savedInstances)
|
snapshot.appendItems([.findInstance], toSection: .savedInstances)
|
||||||
dataSource.apply(snapshot)
|
dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
@ -280,40 +249,30 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeSavedHashtag(_ hashtag: Hashtag) {
|
func removeSavedHashtag(_ hashtag: Hashtag) {
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let account = mastodonController.accountInfo!
|
||||||
if let hashtag = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
SavedDataManager.shared.remove(hashtag: hashtag, for: account)
|
||||||
context.delete(hashtag)
|
|
||||||
try! context.save()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeSavedInstance(_ instanceURL: URL) {
|
func removeSavedInstance(_ instanceURL: URL) {
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let account = mastodonController.accountInfo!
|
||||||
if let instance = try? context.fetch(SavedInstance.fetchRequest(url: instanceURL)).first {
|
SavedDataManager.shared.remove(instance: instanceURL, for: account)
|
||||||
context.delete(instance)
|
|
||||||
try! context.save()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func trailingSwipeActionsForCell(at indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
private func trailingSwipeActionsForCell(at indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
let title: String
|
|
||||||
let handler: UIContextualAction.Handler
|
let handler: UIContextualAction.Handler
|
||||||
switch dataSource.itemIdentifier(for: indexPath) {
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
title = NSLocalizedString("Delete", comment: "delete swipe action title")
|
|
||||||
handler = { (_, _, completion) in
|
handler = { (_, _, completion) in
|
||||||
self.deleteList(list, completion: completion)
|
self.deleteList(list, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
title = NSLocalizedString("Unsave", comment: "unsave swipe action title")
|
|
||||||
handler = { (_, _, completion) in
|
handler = { (_, _, completion) in
|
||||||
self.removeSavedHashtag(hashtag)
|
self.removeSavedHashtag(hashtag)
|
||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .savedInstance(url):
|
case let .savedInstance(url):
|
||||||
title = NSLocalizedString("Unsave", comment: "unsave swipe action title")
|
|
||||||
handler = { (_, _, completion) in
|
handler = { (_, _, completion) in
|
||||||
self.removeSavedInstance(url)
|
self.removeSavedInstance(url)
|
||||||
completion(true)
|
completion(true)
|
||||||
|
@ -324,7 +283,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
return UISwipeActionsConfiguration(actions: [
|
return UISwipeActionsConfiguration(actions: [
|
||||||
UIContextualAction(style: .destructive, title: title, handler: handler)
|
UIContextualAction(style: .destructive, title: NSLocalizedString("Delete", comment: "delete swipe action title"), handler: handler)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -574,17 +533,13 @@ extension ExploreViewController: UICollectionViewDragDelegate {
|
||||||
let provider: NSItemProvider
|
let provider: NSItemProvider
|
||||||
switch item {
|
switch item {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
let activity = UserActivityManager.bookmarksActivity()
|
provider = NSItemProvider(object: UserActivityManager.bookmarksActivity())
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider = NSItemProvider(object: activity)
|
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
guard let activity = UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: accountID) else { return [] }
|
guard let activity = UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: accountID) else { return [] }
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider = NSItemProvider(object: activity)
|
provider = NSItemProvider(object: activity)
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
provider = NSItemProvider(object: hashtag.url as NSURL)
|
provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
}
|
}
|
||||||
case let .savedInstance(url):
|
case let .savedInstance(url):
|
||||||
|
|
|
@ -134,10 +134,8 @@ extension ProfileDirectoryViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileDirectoryViewController: ToastableViewController {
|
extension ProfileDirectoryViewController: MenuPreviewProvider {
|
||||||
}
|
var navigationDelegate: TuskerNavigationDelegate? { self }
|
||||||
|
|
||||||
extension ProfileDirectoryViewController: MenuActionProvider {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileDirectoryViewController: UICollectionViewDelegate {
|
extension ProfileDirectoryViewController: UICollectionViewDelegate {
|
||||||
|
@ -183,7 +181,6 @@ extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: account.url as NSURL)
|
let provider = NSItemProvider(object: account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,6 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: hashtag.url as NSURL)
|
let provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
}
|
}
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
|
@ -111,8 +110,6 @@ extension TrendingHashtagsViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: ToastableViewController {
|
extension TrendingHashtagsViewController: MenuPreviewProvider {
|
||||||
}
|
var navigationDelegate: TuskerNavigationDelegate? { self }
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: MenuActionProvider {
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,8 +104,6 @@ extension TrendingLinksViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingLinksViewController: ToastableViewController {
|
extension TrendingLinksViewController: MenuPreviewProvider {
|
||||||
}
|
var navigationDelegate: TuskerNavigationDelegate? { self }
|
||||||
|
|
||||||
extension TrendingLinksViewController: MenuActionProvider {
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,12 +85,6 @@ extension TrendingStatusesViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingStatusesViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TrendingStatusesViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TrendingStatusesViewController: StatusTableViewCellDelegate {
|
extension TrendingStatusesViewController: StatusTableViewCellDelegate {
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
tableView.beginUpdates()
|
tableView.beginUpdates()
|
||||||
|
|
|
@ -9,8 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
|
|
||||||
/// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.
|
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,13 +20,11 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
@IBOutlet weak var blurContentView: UIView!
|
@IBOutlet weak var blurContentView: UIView!
|
||||||
@IBOutlet weak var accountsStack: UIStackView!
|
@IBOutlet weak var accountsStack: UIStackView!
|
||||||
|
|
||||||
private(set) var accountViews: [FastSwitchingAccountView] = []
|
private var accountViews: [FastSwitchingAccountView] = []
|
||||||
private var lastSelectedAccountViewIndex: Int?
|
private var lastSelectedAccountViewIndex: Int?
|
||||||
private var selectionChangedFeedbackGenerator: UIImpactFeedbackGenerator?
|
private var selectionChangedFeedbackGenerator: UIImpactFeedbackGenerator?
|
||||||
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
var itemOrientation: ItemOrientation = .iconsTrailing
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
||||||
}
|
}
|
||||||
|
@ -55,15 +51,6 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
func show() {
|
func show() {
|
||||||
createAccountViews()
|
createAccountViews()
|
||||||
// add after creating account views so that the presenter can align based on them
|
|
||||||
delegate?.fastAccountSwitcherAddToViewHierarchy(self)
|
|
||||||
|
|
||||||
switch itemOrientation {
|
|
||||||
case .iconsLeading:
|
|
||||||
accountsStack.alignment = .leading
|
|
||||||
case .iconsTrailing:
|
|
||||||
accountsStack.alignment = .trailing
|
|
||||||
}
|
|
||||||
|
|
||||||
view.isHidden = false
|
view.isHidden = false
|
||||||
|
|
||||||
|
@ -100,35 +87,24 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func hide(completion: (() -> Void)? = nil) {
|
func hide(completion: (() -> Void)? = nil) {
|
||||||
guard view.superview != nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lastSelectedAccountViewIndex = nil
|
lastSelectedAccountViewIndex = nil
|
||||||
selectionChangedFeedbackGenerator = nil
|
selectionChangedFeedbackGenerator = nil
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut) {
|
UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut) {
|
||||||
self.view.alpha = 0
|
self.view.alpha = 0
|
||||||
} completion: { (_) in
|
} completion: { (_) in
|
||||||
// todo: probably remove these two lines
|
|
||||||
self.view.alpha = 1
|
self.view.alpha = 1
|
||||||
self.view.isHidden = true
|
self.view.isHidden = true
|
||||||
completion?()
|
completion?()
|
||||||
self.view.removeFromSuperview()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createAccountViews() {
|
private func createAccountViews() {
|
||||||
accountsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
accountsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
|
accountViews = []
|
||||||
let addAccountPlaceholder = FastSwitchingAccountView(orientation: itemOrientation)
|
|
||||||
accountsStack.addArrangedSubview(addAccountPlaceholder)
|
|
||||||
|
|
||||||
accountViews = [
|
|
||||||
addAccountPlaceholder
|
|
||||||
]
|
|
||||||
|
|
||||||
for account in LocalData.shared.accounts {
|
for account in LocalData.shared.accounts {
|
||||||
let accountView = FastSwitchingAccountView(account: account, orientation: itemOrientation)
|
let accountView = FastSwitchingAccountView(account: account)
|
||||||
accountView.isCurrent = account.id == LocalData.shared.mostRecentAccountID
|
accountView.isCurrent = account.id == LocalData.shared.mostRecentAccountID
|
||||||
accountsStack.addArrangedSubview(accountView)
|
accountsStack.addArrangedSubview(accountView)
|
||||||
accountViews.append(accountView)
|
accountViews.append(accountView)
|
||||||
|
@ -146,30 +122,19 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) {
|
private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) {
|
||||||
if newIndex == 0 { // add account placeholder
|
let account = LocalData.shared.accounts[newIndex]
|
||||||
|
|
||||||
|
if account.id != LocalData.shared.mostRecentAccountID {
|
||||||
if hapticFeedback {
|
if hapticFeedback {
|
||||||
selectionChangedFeedbackGenerator?.impactOccurred()
|
selectionChangedFeedbackGenerator?.impactOccurred()
|
||||||
}
|
}
|
||||||
selectionChangedFeedbackGenerator = nil
|
selectionChangedFeedbackGenerator = nil
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
(self.view.window!.windowScene!.delegate as! MainSceneDelegate).showAddAccount()
|
(self.view.window!.windowScene!.delegate as! MainSceneDelegate).activateAccount(account, animated: true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let account = LocalData.shared.accounts[newIndex - 1]
|
hide()
|
||||||
|
|
||||||
if account.id != LocalData.shared.mostRecentAccountID {
|
|
||||||
if hapticFeedback {
|
|
||||||
selectionChangedFeedbackGenerator?.impactOccurred()
|
|
||||||
}
|
|
||||||
selectionChangedFeedbackGenerator = nil
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
(self.view.window!.windowScene!.delegate as! MainSceneDelegate).activateAccount(account, animated: true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,9 +155,10 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
handleGestureMoved(to: location)
|
handleGestureMoved(to: location)
|
||||||
|
|
||||||
case .ended:
|
case .ended:
|
||||||
|
let location = recognizer.location(in: view)
|
||||||
if let index = lastSelectedAccountViewIndex {
|
if let index = lastSelectedAccountViewIndex {
|
||||||
switchAccount(newIndex: index)
|
switchAccount(newIndex: index)
|
||||||
} else if !(delegate?.fastAccountSwitcher(self, triggerZoneContains: recognizer.location(in: recognizer.view)) ?? false) {
|
} else if !(delegate?.fastAccountSwitcher(self, triggerZoneContains: location) ?? false) {
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,16 +240,9 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FastAccountSwitcherViewController {
|
|
||||||
enum ItemOrientation {
|
|
||||||
case iconsLeading
|
|
||||||
case iconsTrailing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension FastAccountSwitcherViewController: UIGestureRecognizerDelegate {
|
extension FastAccountSwitcherViewController: UIGestureRecognizerDelegate {
|
||||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
let point = gestureRecognizer.location(in: gestureRecognizer.view)
|
let point = gestureRecognizer.location(in: view)
|
||||||
return delegate?.fastAccountSwitcher(self, triggerZoneContains: point) ?? false
|
return delegate?.fastAccountSwitcher(self, triggerZoneContains: point) ?? false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -12,6 +12,7 @@
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="accountsStack" destination="lYU-Bb-3Wi" id="Dxs-ta-ORu"/>
|
<outlet property="accountsStack" destination="lYU-Bb-3Wi" id="Dxs-ta-ORu"/>
|
||||||
<outlet property="blurContentView" destination="1Gd-Da-Vab" id="JqT-uq-1o2"/>
|
<outlet property="blurContentView" destination="1Gd-Da-Vab" id="JqT-uq-1o2"/>
|
||||||
|
<outlet property="dimmingView" destination="Lul-oI-bZ7" id="JhP-ZX-8fb"/>
|
||||||
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
||||||
</connections>
|
</connections>
|
||||||
</placeholder>
|
</placeholder>
|
||||||
|
@ -20,6 +21,10 @@
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
|
<view alpha="0.25" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Lul-oI-bZ7">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</view>
|
||||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5fd-Ni-Owc">
|
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5fd-Ni-Owc">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="1Gd-Da-Vab">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="1Gd-Da-Vab">
|
||||||
|
@ -33,7 +38,7 @@
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="top" secondItem="1Gd-Da-Vab" secondAttribute="topMargin" placeholder="YES" id="KQs-d5-U3f"/>
|
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="top" secondItem="1Gd-Da-Vab" secondAttribute="topMargin" placeholder="YES" id="KQs-d5-U3f"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="lYU-Bb-3Wi" secondAttribute="trailingMargin" constant="8" id="UZh-xR-XVt"/>
|
<constraint firstAttribute="trailing" secondItem="lYU-Bb-3Wi" secondAttribute="trailingMargin" constant="8" id="UZh-xR-XVt"/>
|
||||||
<constraint firstAttribute="bottomMargin" secondItem="lYU-Bb-3Wi" secondAttribute="bottom" placeholder="YES" id="j6f-r5-NNI"/>
|
<constraint firstAttribute="bottomMargin" secondItem="lYU-Bb-3Wi" secondAttribute="bottom" id="j6f-r5-NNI"/>
|
||||||
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="leading" secondItem="1Gd-Da-Vab" secondAttribute="leading" constant="8" id="sae-ga-MGE"/>
|
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="leading" secondItem="1Gd-Da-Vab" secondAttribute="leading" constant="8" id="sae-ga-MGE"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
|
@ -43,11 +48,20 @@
|
||||||
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
||||||
<gestureRecognizers/>
|
<gestureRecognizers/>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="Lul-oI-bZ7" secondAttribute="trailing" id="9Fp-IG-O9W"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="5fd-Ni-Owc" secondAttribute="trailing" id="c27-P9-lLK"/>
|
<constraint firstAttribute="trailing" secondItem="5fd-Ni-Owc" secondAttribute="trailing" id="c27-P9-lLK"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="Lul-oI-bZ7" secondAttribute="bottom" id="o6y-tG-MwH"/>
|
||||||
<constraint firstItem="5fd-Ni-Owc" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="phf-PC-bdH"/>
|
<constraint firstItem="5fd-Ni-Owc" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="phf-PC-bdH"/>
|
||||||
<constraint firstItem="5fd-Ni-Owc" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="rz7-cQ-PIC"/>
|
<constraint firstItem="5fd-Ni-Owc" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="rz7-cQ-PIC"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="5fd-Ni-Owc" secondAttribute="bottom" id="sHl-iD-kGi"/>
|
<constraint firstAttribute="bottom" secondItem="5fd-Ni-Owc" secondAttribute="bottom" id="sHl-iD-kGi"/>
|
||||||
|
<constraint firstItem="Lul-oI-bZ7" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="tfE-Xr-YBo"/>
|
||||||
|
<constraint firstItem="Lul-oI-bZ7" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="ua7-DO-kdp"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
|
<variation key="default">
|
||||||
|
<mask key="subviews">
|
||||||
|
<exclude reference="Lul-oI-bZ7"/>
|
||||||
|
</mask>
|
||||||
|
</variation>
|
||||||
<point key="canvasLocation" x="140.57971014492756" y="144.64285714285714"/>
|
<point key="canvasLocation" x="140.57971014492756" y="144.64285714285714"/>
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</objects>
|
||||||
|
|
|
@ -10,6 +10,8 @@ import UIKit
|
||||||
|
|
||||||
class FastSwitchingAccountView: UIView {
|
class FastSwitchingAccountView: UIView {
|
||||||
|
|
||||||
|
let account: LocalData.UserAccountInfo
|
||||||
|
|
||||||
private static let selectedColor = UIColor { (traits) in
|
private static let selectedColor = UIColor { (traits) in
|
||||||
if traits.userInterfaceStyle == .dark {
|
if traits.userInterfaceStyle == .dark {
|
||||||
return UIColor(hue: 211 / 360, saturation: 85 / 100, brightness: 100 / 100, alpha: 1)
|
return UIColor(hue: 211 / 360, saturation: 85 / 100, brightness: 100 / 100, alpha: 1)
|
||||||
|
@ -35,38 +37,23 @@ class FastSwitchingAccountView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let orientation: FastAccountSwitcherViewController.ItemOrientation
|
|
||||||
|
|
||||||
private let usernameLabel = UILabel()
|
private let usernameLabel = UILabel()
|
||||||
private let instanceLabel = UILabel()
|
private let instanceLabel = UILabel()
|
||||||
private let avatarImageView = UIImageView()
|
|
||||||
|
|
||||||
private var avatarRequest: ImageCache.Request?
|
private var avatarRequest: ImageCache.Request?
|
||||||
|
|
||||||
init(account: LocalData.UserAccountInfo, orientation: FastAccountSwitcherViewController.ItemOrientation) {
|
init(account: LocalData.UserAccountInfo) {
|
||||||
self.orientation = orientation
|
self.account = account
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
commonInit()
|
|
||||||
setupAccount(account: account)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(orientation: FastAccountSwitcherViewController.ItemOrientation) {
|
|
||||||
self.orientation = orientation
|
|
||||||
super.init(frame: .zero)
|
|
||||||
commonInit()
|
|
||||||
setupPlaceholder()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func commonInit() {
|
|
||||||
usernameLabel.textColor = .white
|
usernameLabel.textColor = .white
|
||||||
usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0)
|
usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0)
|
||||||
|
usernameLabel.text = account.username
|
||||||
|
|
||||||
instanceLabel.textColor = .white
|
instanceLabel.textColor = .white
|
||||||
instanceLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .subheadline), size: 0)
|
instanceLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .subheadline), size: 0)
|
||||||
|
instanceLabel.text = account.instanceURL.host!
|
||||||
|
|
||||||
let stackView = UIStackView(arrangedSubviews: [
|
let stackView = UIStackView(arrangedSubviews: [
|
||||||
usernameLabel,
|
usernameLabel,
|
||||||
|
@ -74,66 +61,45 @@ class FastSwitchingAccountView: UIView {
|
||||||
])
|
])
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
stackView.axis = .vertical
|
stackView.axis = .vertical
|
||||||
|
stackView.alignment = .trailing
|
||||||
addSubview(stackView)
|
addSubview(stackView)
|
||||||
|
|
||||||
|
let avatarImageView = UIImageView()
|
||||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
avatarImageView.layer.masksToBounds = true
|
avatarImageView.layer.masksToBounds = true
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 40
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 40
|
||||||
avatarImageView.image = UIImage(systemName: Preferences.shared.avatarStyle == .circle ? "person.crop.circle" : "person.crop.square")
|
avatarImageView.image = UIImage(systemName: Preferences.shared.avatarStyle == .circle ? "person.crop.circle" : "person.crop.square")
|
||||||
avatarImageView.contentMode = .scaleAspectFill
|
avatarImageView.contentMode = .scaleAspectFit
|
||||||
addSubview(avatarImageView)
|
addSubview(avatarImageView)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||||
avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
||||||
|
avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
avatarImageView.widthAnchor.constraint(equalToConstant: 40),
|
avatarImageView.widthAnchor.constraint(equalToConstant: 40),
|
||||||
avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor),
|
avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor),
|
||||||
|
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: avatarImageView.leadingAnchor, constant: -8),
|
||||||
stackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
stackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
switch orientation {
|
|
||||||
case .iconsLeading:
|
|
||||||
stackView.alignment = .leading
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
avatarImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8),
|
|
||||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
case .iconsTrailing:
|
|
||||||
stackView.alignment = .trailing
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
|
||||||
stackView.trailingAnchor.constraint(equalTo: avatarImageView.leadingAnchor, constant: -8),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLabelColors()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupAccount(account: LocalData.UserAccountInfo) {
|
|
||||||
usernameLabel.text = account.username
|
|
||||||
instanceLabel.text = account.instanceURL.host!
|
|
||||||
let controller = MastodonController.getForAccount(account)
|
let controller = MastodonController.getForAccount(account)
|
||||||
controller.getOwnAccount { [weak self] (result) in
|
controller.getOwnAccount { [weak self] (result) in
|
||||||
guard let self = self,
|
guard let self = self,
|
||||||
case let .success(account) = result,
|
case let .success(account) = result,
|
||||||
let avatar = account.avatar else { return }
|
let avatar = account.avatar else { return }
|
||||||
self.avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in
|
self.avatarRequest = ImageCache.avatars.get(avatar) { [weak avatarImageView] (_, image) in
|
||||||
guard let self = self, let image = image else { return }
|
guard let avatarImageView = avatarImageView, let image = image else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = image
|
avatarImageView.image = image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupPlaceholder() {
|
required init?(coder: NSCoder) {
|
||||||
usernameLabel.text = "Add Account"
|
fatalError("init(coder:) has not been implemented")
|
||||||
instanceLabel.isHidden = true
|
|
||||||
avatarImageView.image = UIImage(systemName: "plus")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLabelColors() {
|
private func updateLabelColors() {
|
||||||
|
@ -147,7 +113,6 @@ class FastSwitchingAccountView: UIView {
|
||||||
}
|
}
|
||||||
usernameLabel.textColor = color
|
usernameLabel.textColor = color
|
||||||
instanceLabel.textColor = color
|
instanceLabel.textColor = color
|
||||||
avatarImageView.tintColor = color
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,9 +176,3 @@ extension EditListAccountsViewController: SearchResultsViewControllerDelegate {
|
||||||
extension EditListAccountsViewController: TuskerNavigationDelegate {
|
extension EditListAccountsViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EditListAccountsViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension EditListAccountsViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
|
@ -86,14 +86,8 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func performSearch(query: String) {
|
func performSearch(query: String) {
|
||||||
loadViewIfNeeded()
|
|
||||||
root.performSearch(query: query)
|
root.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentPreferences(completion: (() -> Void)?) {
|
|
||||||
loadViewIfNeeded()
|
|
||||||
root.presentPreferences(completion: completion)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountSwitchingContainerViewController: BackgroundableViewController {
|
extension AccountSwitchingContainerViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
//
|
|
||||||
// MainSidebarMyProfileCollectionViewCell.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/30/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
|
|
||||||
|
|
||||||
private var verticalImageInset: CGFloat {
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .mac {
|
|
||||||
return (28 - avatarImageSize) / 2
|
|
||||||
} else {
|
|
||||||
return (44 - avatarImageSize) / 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var avatarImageSize: CGFloat {
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .mac {
|
|
||||||
return 20
|
|
||||||
} else {
|
|
||||||
return 28
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUI(item: MainSidebarViewController.Item, account: LocalData.UserAccountInfo) async {
|
|
||||||
var config = defaultContentConfiguration()
|
|
||||||
config.text = item.title
|
|
||||||
config.image = UIImage(systemName: item.imageName!)
|
|
||||||
self.contentConfiguration = config
|
|
||||||
if UIDevice.current.userInterfaceIdiom != .mac {
|
|
||||||
let indicator = FastAccountSwitcherIndicatorView()
|
|
||||||
// need to explicitly set the frame to get it vertically centered
|
|
||||||
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
|
|
||||||
accessories = [
|
|
||||||
.customView(configuration: .init(customView: indicator, placement: .trailing()))
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
let mastodonController = MastodonController.getForAccount(account)
|
|
||||||
guard let account = try? await mastodonController.getOwnAccount(),
|
|
||||||
let avatar = account.avatar else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = ImageCache.avatars.get(avatar, loadOriginal: false) { [weak self] _, image in
|
|
||||||
guard let self = self,
|
|
||||||
let image = image else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard var config = self.contentConfiguration as? UIListContentConfiguration else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
config.image = image
|
|
||||||
config.directionalLayoutMargins.top = self.verticalImageInset
|
|
||||||
config.directionalLayoutMargins.bottom = self.verticalImageInset
|
|
||||||
config.imageProperties.maximumSize = CGSize(width: self.avatarImageSize, height: self.avatarImageSize)
|
|
||||||
config.imageProperties.reservedLayoutSize = CGSize(width: UIListContentConfiguration.ImageProperties.standardDimension, height: 0)
|
|
||||||
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * self.avatarImageSize
|
|
||||||
self.contentConfiguration = config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
|
||||||
guard var config = self.contentConfiguration as? UIListContentConfiguration else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize
|
|
||||||
self.contentConfiguration = contentConfiguration
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -19,11 +19,6 @@ class MainSidebarViewController: UIViewController {
|
||||||
private weak var mastodonController: MastodonController!
|
private weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
weak var sidebarDelegate: MainSidebarViewControllerDelegate?
|
weak var sidebarDelegate: MainSidebarViewControllerDelegate?
|
||||||
var onViewDidLoad: (() -> Void)? = nil {
|
|
||||||
willSet {
|
|
||||||
precondition(onViewDidLoad == nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
private var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
@ -100,8 +95,6 @@ class MainSidebarViewController: UIViewController {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
onViewDidLoad?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(item: Item, animated: Bool) {
|
func select(item: Item, animated: Bool) {
|
||||||
|
@ -122,12 +115,6 @@ class MainSidebarViewController: UIViewController {
|
||||||
cell.contentConfiguration = config
|
cell.contentConfiguration = config
|
||||||
}
|
}
|
||||||
|
|
||||||
let myProfileCell = UICollectionView.CellRegistration<MainSidebarMyProfileCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
|
|
||||||
Task {
|
|
||||||
await cell.updateUI(item: item, account: self.mastodonController.accountInfo!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let outlineHeaderCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
let outlineHeaderCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
||||||
var config = cell.defaultContentConfiguration()
|
var config = cell.defaultContentConfiguration()
|
||||||
config.attributedText = NSAttributedString(string: item.title, attributes: [
|
config.attributedText = NSAttributedString(string: item.title, attributes: [
|
||||||
|
@ -138,9 +125,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
return UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
|
return UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
|
||||||
if case .tab(.myProfile) = item {
|
if item.hasChildren {
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: myProfileCell, for: indexPath, item: item)
|
|
||||||
} else if item.hasChildren {
|
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: outlineHeaderCell, for: indexPath, item: item)
|
return collectionView.dequeueConfiguredReusableCell(using: outlineHeaderCell, for: indexPath, item: item)
|
||||||
} else {
|
} else {
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: item)
|
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: item)
|
||||||
|
@ -225,38 +210,14 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func fetchSavedHashtags() -> [SavedHashtag] {
|
|
||||||
let req = SavedHashtag.fetchRequest()
|
|
||||||
req.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true, selector: #selector(NSString.localizedCompare(_:)))]
|
|
||||||
do {
|
|
||||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func fetchSavedInstances() -> [SavedInstance] {
|
|
||||||
let req = SavedInstance.fetchRequest()
|
|
||||||
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
|
|
||||||
do {
|
|
||||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func reloadSavedHashtags() {
|
@objc private func reloadSavedHashtags() {
|
||||||
let selected = collectionView.indexPathsForSelectedItems?.first
|
let selected = collectionView.indexPathsForSelectedItems?.first
|
||||||
|
|
||||||
var hashtagsSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
var hashtagsSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||||
hashtagsSnapshot.append([.savedHashtagsHeader])
|
hashtagsSnapshot.append([.savedHashtagsHeader])
|
||||||
hashtagsSnapshot.expand([.savedHashtagsHeader])
|
hashtagsSnapshot.expand([.savedHashtagsHeader])
|
||||||
let hashtags = fetchSavedHashtags().map {
|
let sortedHashtags = SavedDataManager.shared.sortedHashtags(for: mastodonController.accountInfo!)
|
||||||
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
|
hashtagsSnapshot.append(sortedHashtags.map { .savedHashtag($0) }, to: .savedHashtagsHeader)
|
||||||
}
|
|
||||||
hashtagsSnapshot.append(hashtags, to: .savedHashtagsHeader)
|
|
||||||
hashtagsSnapshot.append([.addSavedHashtag], to: .savedHashtagsHeader)
|
hashtagsSnapshot.append([.addSavedHashtag], to: .savedHashtagsHeader)
|
||||||
self.dataSource.apply(hashtagsSnapshot, to: .savedHashtags, animatingDifferences: false) {
|
self.dataSource.apply(hashtagsSnapshot, to: .savedHashtags, animatingDifferences: false) {
|
||||||
if let selected = selected {
|
if let selected = selected {
|
||||||
|
@ -271,10 +232,8 @@ class MainSidebarViewController: UIViewController {
|
||||||
var instancesSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
var instancesSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||||
instancesSnapshot.append([.savedInstancesHeader])
|
instancesSnapshot.append([.savedInstancesHeader])
|
||||||
instancesSnapshot.expand([.savedInstancesHeader])
|
instancesSnapshot.expand([.savedInstancesHeader])
|
||||||
let instances = fetchSavedInstances().map {
|
let sortedInstances = SavedDataManager.shared.savedInstances(for: mastodonController.accountInfo!)
|
||||||
Item.savedInstance($0.url)
|
instancesSnapshot.append(sortedInstances.map { .savedInstance($0) }, to: .savedInstancesHeader)
|
||||||
}
|
|
||||||
instancesSnapshot.append(instances, to: .savedInstancesHeader)
|
|
||||||
instancesSnapshot.append([.addSavedInstance], to: .savedInstancesHeader)
|
instancesSnapshot.append([.addSavedInstance], to: .savedInstancesHeader)
|
||||||
self.dataSource.apply(instancesSnapshot, to: .savedInstances, animatingDifferences: false) {
|
self.dataSource.apply(instancesSnapshot, to: .savedInstances, animatingDifferences: false) {
|
||||||
if let selected = selected {
|
if let selected = selected {
|
||||||
|
@ -363,14 +322,6 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func myProfileCell() -> UICollectionViewCell? {
|
|
||||||
guard let indexPath = dataSource.indexPath(for: .tab(.myProfile)),
|
|
||||||
let item = collectionView.cellForItem(at: indexPath) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSidebarViewController {
|
extension MainSidebarViewController {
|
||||||
|
@ -555,12 +506,6 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
let activity = userActivityForItem(item) else {
|
let activity = userActivityForItem(item) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if case .tab(.myProfile) = item,
|
|
||||||
// only disable context menu on long-press, to allow fast account switching
|
|
||||||
collectionView.contextMenuInteraction?.menuAppearance == .rich {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
|
||||||
return UIMenu(children: [
|
return UIMenu(children: [
|
||||||
UIWindowScene.ActivationAction({ action in
|
UIWindowScene.ActivationAction({ action in
|
||||||
|
@ -577,11 +522,7 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
|
||||||
let activity = userActivityForItem(item) else {
|
let activity = userActivityForItem(item) else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
if case .tab(.myProfile) = item {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
let provider = NSItemProvider(object: activity)
|
let provider = NSItemProvider(object: activity)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
@ -590,7 +531,6 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
|
||||||
extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
|
extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
|
||||||
func didSaveInstance(url: URL) {
|
func didSaveInstance(url: URL) {
|
||||||
dismiss(animated: true) {
|
dismiss(animated: true) {
|
||||||
self.select(item: .savedInstance(url), animated: true)
|
|
||||||
self.sidebarDelegate?.sidebar(self, didSelectItem: .savedInstance(url))
|
self.sidebarDelegate?.sidebar(self, didSelectItem: .savedInstance(url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var sidebar: MainSidebarViewController!
|
private var sidebar: MainSidebarViewController!
|
||||||
private var fastAccountSwitcher: FastAccountSwitcherViewController?
|
|
||||||
|
|
||||||
// Keep track of navigation stacks per-item so that we can only ever use a single navigation controller
|
// Keep track of navigation stacks per-item so that we can only ever use a single navigation controller
|
||||||
private var navigationStacks: [MainSidebarViewController.Item: [UIViewController]] = [:]
|
private var navigationStacks: [MainSidebarViewController.Item: [UIViewController]] = [:]
|
||||||
|
@ -24,6 +23,14 @@ class MainSplitViewController: UISplitViewController {
|
||||||
viewController(for: .secondary) as? UINavigationController
|
viewController(for: .secondary) as? UINavigationController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
return .portrait
|
||||||
|
} else {
|
||||||
|
return .all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -53,23 +60,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
select(item: .tab(.timelines))
|
select(item: .tab(.timelines))
|
||||||
}
|
}
|
||||||
|
|
||||||
if UIDevice.current.userInterfaceIdiom != .mac {
|
|
||||||
let switcher = FastAccountSwitcherViewController()
|
|
||||||
fastAccountSwitcher = switcher
|
|
||||||
switcher.itemOrientation = .iconsLeading
|
|
||||||
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
switcher.delegate = self
|
|
||||||
// accessing .view unconditionally loads the view, which we don't want to happen
|
|
||||||
// because the sidebar view not being loaded is how we know not to transfer nav state
|
|
||||||
// in splitViewControllerDidCollapse on devices where the sidebar is never shown
|
|
||||||
sidebar.onViewDidLoad = { [unowned self] in
|
|
||||||
self.sidebar.view.addGestureRecognizer(switcher.createSwitcherGesture())
|
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
|
|
||||||
tapRecognizer.cancelsTouchesInView = false
|
|
||||||
self.sidebar.view.addGestureRecognizer(tapRecognizer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
||||||
setViewController(tabBarViewController, for: .compact)
|
setViewController(tabBarViewController, for: .compact)
|
||||||
|
|
||||||
|
@ -111,10 +101,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
select(item: item)
|
select(item: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sidebarTapped() {
|
|
||||||
fastAccountSwitcher?.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: UISplitViewControllerDelegate {
|
extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
|
@ -455,10 +441,6 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
searchViewController.searchController.searchBar.text = query
|
searchViewController.searchController.searchBar.text = query
|
||||||
searchViewController.resultsController.performSearch(query: query)
|
searchViewController.resultsController.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentPreferences(completion: (() -> Void)?) {
|
|
||||||
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: BackgroundableViewController {
|
extension MainSplitViewController: BackgroundableViewController {
|
||||||
|
@ -473,27 +455,3 @@ extension MainSplitViewController: BackgroundableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
|
||||||
view.addSubview(fastAccountSwitcher.view)
|
|
||||||
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
|
||||||
let myProfileCell = sidebar.myProfileCell()!
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
currentAccount.centerYAnchor.constraint(equalTo: myProfileCell.centerYAnchor),
|
|
||||||
|
|
||||||
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: sidebar.view.trailingAnchor),
|
|
||||||
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
|
||||||
guard !isCollapsed,
|
|
||||||
let cell = sidebar.myProfileCell() else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let cellRect = cell.convert(cell.bounds, to: sidebar.view)
|
|
||||||
return cellRect.contains(point)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -43,8 +43,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
stateRestorationLogger.info("MainTabBarViewController: viewDidLoad, selectedIndex=\(self.selectedIndex, privacy: .public)")
|
|
||||||
|
|
||||||
self.delegate = self
|
self.delegate = self
|
||||||
|
|
||||||
composePlaceholder = UIViewController()
|
composePlaceholder = UIViewController()
|
||||||
|
@ -62,6 +60,13 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
fastAccountSwitcher = FastAccountSwitcherViewController()
|
fastAccountSwitcher = FastAccountSwitcherViewController()
|
||||||
fastAccountSwitcher.delegate = self
|
fastAccountSwitcher.delegate = self
|
||||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
|
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
|
||||||
|
@ -72,6 +77,10 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
|
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
|
||||||
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
|
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(fastSwitcherIndicator)
|
view.addSubview(fastSwitcherIndicator)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
fastSwitcherIndicator.widthAnchor.constraint(equalToConstant: 10),
|
||||||
|
fastSwitcherIndicator.heightAnchor.constraint(equalToConstant: 12),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
tabBar.isSpringLoaded = true
|
tabBar.isSpringLoaded = true
|
||||||
|
@ -79,20 +88,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
stateRestorationLogger.info("MainTabBarViewController: viewWillAppear, selectedIndex=\(self.selectedIndex, privacy: .public)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
stateRestorationLogger.info("MainTabBarViewController: viewDidAppear, selectedIndex=\(self.selectedIndex, privacy: .public)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
|
||||||
super.viewDidLayoutSubviews()
|
|
||||||
|
|
||||||
// i hate that we have to do this so often :S
|
|
||||||
// but doing it only in viewWillAppear makes it not appear initially
|
|
||||||
// doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed
|
|
||||||
repositionFastSwitcherIndicator()
|
repositionFastSwitcherIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,23 +201,11 @@ extension MainTabBarViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
|
||||||
view.addSubview(fastAccountSwitcher.view)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
|
|
||||||
|
|
||||||
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
||||||
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view)
|
||||||
return myProfileButton.bounds.contains(locationInButton)
|
return myProfileButton.bounds.contains(locationInButton)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,7 +236,6 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
self.selectedIndex = tab.rawValue
|
self.selectedIndex = tab.rawValue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
|
|
||||||
selectedIndex = tab.rawValue
|
selectedIndex = tab.rawValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,10 +262,6 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
exploreController.searchController.searchBar.text = query
|
exploreController.searchController.searchBar.text = query
|
||||||
exploreController.resultsController.performSearch(query: query)
|
exploreController.resultsController.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentPreferences(completion: (() -> Void)?) {
|
|
||||||
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainTabBarViewController: BackgroundableViewController {
|
extension MainTabBarViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -13,5 +13,4 @@ protocol TuskerRootViewController: UIViewController {
|
||||||
func select(tab: MainTabBarViewController.Tab)
|
func select(tab: MainTabBarViewController.Tab)
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
||||||
func performSearch(query: String)
|
func performSearch(query: String)
|
||||||
func presentPreferences(completion: (() -> Void)?)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,14 +140,11 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
self.older = older
|
self.older = older
|
||||||
}
|
}
|
||||||
|
|
||||||
let olderGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||||
let existingGroups = currentSnapshot().itemIdentifiers
|
var snapshot = currentSnapshot()
|
||||||
let merged = NotificationGroup.mergeGroups(first: existingGroups, second: olderGroups, only: self.groupTypes)
|
snapshot.appendItems(groups, toSection: .notifications)
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, NotificationGroup>()
|
|
||||||
snapshot.appendSections([.notifications])
|
|
||||||
snapshot.appendItems(merged, toSection: .notifications)
|
|
||||||
completion(.success(snapshot))
|
completion(.success(snapshot))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,14 +173,15 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
self.newer = newer
|
self.newer = newer
|
||||||
}
|
}
|
||||||
|
|
||||||
let newerGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||||
let existingGroups = currentSnapshot().itemIdentifiers
|
var snapshot = currentSnapshot()
|
||||||
let merged = NotificationGroup.mergeGroups(first: newerGroups, second: existingGroups, only: self.groupTypes)
|
if let first = snapshot.itemIdentifiers(inSection: .notifications).first {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, NotificationGroup>()
|
snapshot.insertItems(groups, beforeItem: first)
|
||||||
snapshot.appendSections([.notifications])
|
} else {
|
||||||
snapshot.appendItems(merged, toSection: .notifications)
|
snapshot.appendItems(groups, toSection: .notifications)
|
||||||
|
}
|
||||||
completion(.success(snapshot))
|
completion(.success(snapshot))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,9 +248,6 @@ extension NotificationsTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsTableViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
cellHeightChanged()
|
cellHeightChanged()
|
||||||
|
|
|
@ -57,6 +57,12 @@ class ProfileViewController: UIPageViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if let accountID = accountID {
|
||||||
|
mastodonController.persistentContainer.account(for: accountID)?.decrementReferenceCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -114,21 +120,10 @@ class ProfileViewController: UIPageViewController {
|
||||||
let req = Client.getAccount(id: accountID)
|
let req = Client.getAccount(id: accountID)
|
||||||
mastodonController.run(req) { [weak self] (response) in
|
mastodonController.run(req) { [weak self] (response) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch response {
|
guard case let .success(account, _) = response else { fatalError() }
|
||||||
case .success(let account, _):
|
self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (account) in
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(account: account) { (account) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.updateAccountUI()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let error):
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let config = ToastConfiguration(from: error, with: "Loading", in: self) { [unowned self] (toast) in
|
self.updateAccountUI()
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
self.loadAccount()
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,6 +271,3 @@ extension ProfileViewController: TabbedPageViewController {
|
||||||
selectPage(at: currentIndex - 1, animated: true)
|
selectPage(at: currentIndex - 1, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
|
@ -172,9 +172,24 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showSearchResults(_ results: SearchResults) {
|
private func showSearchResults(_ results: SearchResults) {
|
||||||
|
let oldSnapshot = self.dataSource.snapshot()
|
||||||
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
|
||||||
|
if oldSnapshot.indexOfSection(.accounts) != nil {
|
||||||
|
oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in
|
||||||
|
guard case let .account(id) = item else { return }
|
||||||
|
self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldSnapshot.indexOfSection(.statuses) != nil {
|
||||||
|
oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in
|
||||||
|
guard case let .status(id, _) = item else { return }
|
||||||
|
self.mastodonController.persistentContainer.status(for: id, in: context)?.decrementReferenceCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let resultTypes = self.resultTypes
|
let resultTypes = self.resultTypes
|
||||||
if !results.accounts.isEmpty && (resultTypes == nil || resultTypes!.contains(.accounts)) {
|
if !results.accounts.isEmpty && (resultTypes == nil || resultTypes!.contains(.accounts)) {
|
||||||
snapshot.appendSections([.accounts])
|
snapshot.appendSections([.accounts])
|
||||||
|
@ -288,15 +303,6 @@ extension SearchResultsViewController: UISearchBarDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultsViewController: TuskerNavigationDelegate {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchResultsViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchResultsViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchResultsViewController: StatusTableViewCellDelegate {
|
extension SearchResultsViewController: StatusTableViewCellDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
|
|
|
@ -60,6 +60,16 @@ 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 = 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()
|
||||||
|
|
||||||
|
@ -73,7 +83,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)")
|
||||||
|
@ -144,15 +158,6 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusActionAccountListTableViewController: TuskerNavigationDelegate {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension StatusActionAccountListTableViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension StatusActionAccountListTableViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate {
|
extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
|
|
|
@ -15,17 +15,13 @@ class HashtagTimelineViewController: TimelineTableViewController {
|
||||||
|
|
||||||
var toggleSaveButton: UIBarButtonItem!
|
var toggleSaveButton: UIBarButtonItem!
|
||||||
var toggleSaveButtonTitle: String {
|
var toggleSaveButtonTitle: String {
|
||||||
if isHashtagSaved {
|
if SavedDataManager.shared.isSaved(hashtag: hashtag, for: mastodonController.accountInfo!) {
|
||||||
return NSLocalizedString("Unsave", comment: "unsave hashtag button")
|
return NSLocalizedString("Unsave", comment: "unsave hashtag button")
|
||||||
} else {
|
} else {
|
||||||
return NSLocalizedString("Save", comment: "save hashtag button")
|
return NSLocalizedString("Save", comment: "save hashtag button")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isHashtagSaved: Bool {
|
|
||||||
mastodonController.persistentContainer.viewContext.objectExists(for: SavedHashtag.fetchRequest(name: hashtag.name))
|
|
||||||
}
|
|
||||||
|
|
||||||
init(for hashtag: Hashtag, mastodonController: MastodonController) {
|
init(for hashtag: Hashtag, mastodonController: MastodonController) {
|
||||||
self.hashtag = hashtag
|
self.hashtag = hashtag
|
||||||
|
|
||||||
|
@ -52,13 +48,11 @@ class HashtagTimelineViewController: TimelineTableViewController {
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc func toggleSaveButtonPressed() {
|
@objc func toggleSaveButtonPressed() {
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
if SavedDataManager.shared.isSaved(hashtag: hashtag, for: mastodonController.accountInfo!) {
|
||||||
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
SavedDataManager.shared.remove(hashtag: hashtag, for: mastodonController.accountInfo!)
|
||||||
context.delete(existing)
|
|
||||||
} else {
|
} else {
|
||||||
_ = SavedHashtag(hashtag: hashtag, context: context)
|
SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!)
|
||||||
}
|
}
|
||||||
try! context.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,9 @@ class InstanceTimelineViewController: TimelineTableViewController {
|
||||||
let instanceURL: URL
|
let instanceURL: URL
|
||||||
let instanceMastodonController: MastodonController
|
let instanceMastodonController: MastodonController
|
||||||
|
|
||||||
private var toggleSaveButton: UIBarButtonItem!
|
var toggleSaveButton: UIBarButtonItem!
|
||||||
|
var toggleSaveButtonTitle: String {
|
||||||
private var isInstanceSaved: Bool {
|
if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) {
|
||||||
parentMastodonController!.persistentContainer.viewContext.objectExists(for: SavedInstance.fetchRequest(url: instanceURL))
|
|
||||||
}
|
|
||||||
private var toggleSaveButtonTitle: String {
|
|
||||||
if isInstanceSaved {
|
|
||||||
return NSLocalizedString("Unsave", comment: "unsave instance button")
|
return NSLocalizedString("Unsave", comment: "unsave instance button")
|
||||||
} else {
|
} else {
|
||||||
return NSLocalizedString("Save", comment: "save instance button")
|
return NSLocalizedString("Save", comment: "save instance button")
|
||||||
|
@ -85,16 +81,13 @@ class InstanceTimelineViewController: TimelineTableViewController {
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
@objc func toggleSaveButtonPressed() {
|
@objc func toggleSaveButtonPressed() {
|
||||||
let context = parentMastodonController!.persistentContainer.viewContext
|
if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) {
|
||||||
let existing = try? context.fetch(SavedInstance.fetchRequest(url: instanceURL)).first
|
SavedDataManager.shared.remove(instance: instanceURL, for: parentMastodonController!.accountInfo!)
|
||||||
if let existing = existing {
|
|
||||||
context.delete(existing)
|
|
||||||
delegate?.didUnsaveInstance(url: instanceURL)
|
delegate?.didUnsaveInstance(url: instanceURL)
|
||||||
} else {
|
} else {
|
||||||
_ = SavedInstance(url: instanceURL, context: context)
|
SavedDataManager.shared.add(instance: instanceURL, for: parentMastodonController!.accountInfo!)
|
||||||
delegate?.didSaveInstance(url: instanceURL)
|
delegate?.didSaveInstance(url: instanceURL)
|
||||||
}
|
}
|
||||||
try? context.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,18 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
guard let persistentContainer = mastodonController?.persistentContainer,
|
||||||
|
let dataSource = dataSource else { return }
|
||||||
|
// decrement reference counts of any statuses we still have
|
||||||
|
// if the app is currently being quit, this will not affect the persisted data because
|
||||||
|
// the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:)
|
||||||
|
// todo: remove the whole reference count system
|
||||||
|
for case let .status(id: id, state: _) in dataSource.snapshot().itemIdentifiers {
|
||||||
|
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -233,6 +245,12 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func willRemoveItems(_ items: [Item]) {
|
||||||
|
for case let .status(id: id, state: _) in items {
|
||||||
|
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
super.scrollViewWillBeginDragging(scrollView)
|
super.scrollViewWillBeginDragging(scrollView)
|
||||||
|
|
||||||
|
@ -294,9 +312,6 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineTableViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
let ids: [String] = indexPaths.compactMap {
|
let ids: [String] = indexPaths.compactMap {
|
||||||
|
|
|
@ -10,33 +10,29 @@ import UIKit
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
protocol MenuActionProvider: AnyObject {
|
|
||||||
var navigationDelegate: TuskerNavigationDelegate? { get }
|
|
||||||
var toastableViewController: ToastableViewController? { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol MenuPreviewProvider: AnyObject {
|
protocol MenuPreviewProvider: AnyObject {
|
||||||
|
|
||||||
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
|
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
|
||||||
|
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { get }
|
||||||
|
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders?
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol CustomPreviewPresenting {
|
protocol CustomPreviewPresenting {
|
||||||
func presentFromPreview(presenter: UIViewController)
|
func presentFromPreview(presenter: UIViewController)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MenuActionProvider where Self: TuskerNavigationDelegate {
|
extension MenuPreviewProvider {
|
||||||
var navigationDelegate: TuskerNavigationDelegate? { self }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MenuActionProvider where Self: ToastableViewController {
|
|
||||||
var toastableViewController: ToastableViewController? { self }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MenuActionProvider {
|
|
||||||
|
|
||||||
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
|
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
|
||||||
|
|
||||||
|
// Default no-op implementation
|
||||||
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIMenuElement] {
|
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIMenuElement] {
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
|
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
|
||||||
|
@ -59,7 +55,7 @@ extension MenuActionProvider {
|
||||||
draft.visibility = .direct
|
draft.visibility = .direct
|
||||||
self.navigationDelegate?.compose(editing: draft)
|
self.navigationDelegate?.compose(editing: draft)
|
||||||
}),
|
}),
|
||||||
UIDeferredMenuElement.uncachedIfPossible({ (elementHandler) in
|
UIDeferredMenuElement({ (elementHandler) in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
if let action = await self.followAction(for: accountID, mastodonController: mastodonController) {
|
if let action = await self.followAction(for: accountID, mastodonController: mastodonController) {
|
||||||
elementHandler([action])
|
elementHandler([action])
|
||||||
|
@ -88,7 +84,7 @@ extension MenuActionProvider {
|
||||||
|
|
||||||
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: url),
|
openInSafariAction(url: url),
|
||||||
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
||||||
|
@ -97,24 +93,18 @@ extension MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIMenuElement] {
|
func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIMenuElement] {
|
||||||
let actionsSection: [UIMenuElement]
|
let account = mastodonController!.accountInfo!
|
||||||
if let mastodonController = mastodonController,
|
let saved = SavedDataManager.shared.isSaved(hashtag: hashtag, for: account)
|
||||||
mastodonController.loggedIn {
|
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let actionsSection = [
|
||||||
let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first
|
createAction(identifier: "save", title: saved ? "Unsave Hashtag" : "Save Hashtag", systemImageName: "number", handler: { (_) in
|
||||||
actionsSection = [
|
if saved {
|
||||||
createAction(identifier: "save", title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", systemImageName: "number", handler: { (_) in
|
SavedDataManager.shared.remove(hashtag: hashtag, for: account)
|
||||||
if let existing = existing {
|
} else {
|
||||||
context.delete(existing)
|
SavedDataManager.shared.add(hashtag: hashtag, for: account)
|
||||||
} else {
|
}
|
||||||
_ = SavedHashtag(hashtag: hashtag, context: context)
|
})
|
||||||
}
|
]
|
||||||
try! context.save()
|
|
||||||
})
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
actionsSection = []
|
|
||||||
}
|
|
||||||
|
|
||||||
let shareSection = actionsForURL(hashtag.url, sourceView: sourceView)
|
let shareSection = actionsForURL(hashtag.url, sourceView: sourceView)
|
||||||
|
|
||||||
|
@ -145,17 +135,8 @@ extension MenuActionProvider {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
|
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
|
||||||
self.mastodonController?.run(request) { (response) in
|
self.mastodonController?.run(request) { (response) in
|
||||||
switch response {
|
if case let .success(status, _) = response {
|
||||||
case .success(let status, _):
|
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status)
|
|
||||||
|
|
||||||
case .failure(let error):
|
|
||||||
if let toastable = self.toastableViewController {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error \(bookmarked ? "Unb" : "B")ookmarking", in: toastable, retryAction: nil)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -176,16 +157,8 @@ extension MenuActionProvider {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
|
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
|
||||||
self.mastodonController?.run(request) { (response) in
|
self.mastodonController?.run(request) { (response) in
|
||||||
switch response {
|
if case let .success(status, _) = response {
|
||||||
case .success(let status, _):
|
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status)
|
|
||||||
case .failure(let error):
|
|
||||||
if let toastable = self.toastableViewController {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error \(muted ? "Unm" : "M")uting", in: toastable, retryAction: nil)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
@ -200,16 +173,8 @@ extension MenuActionProvider {
|
||||||
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
||||||
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch response {
|
if case let .success(status, _) = response {
|
||||||
case .success(let status, _):
|
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status)
|
|
||||||
case .failure(let error):
|
|
||||||
if let toastable = self.toastableViewController {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error \(pinned ? "Unp" :"P")inning", in: toastable, retryAction: nil)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
@ -221,20 +186,10 @@ extension MenuActionProvider {
|
||||||
guard let mastodonController = self?.mastodonController else { return }
|
guard let mastodonController = self?.mastodonController else { return }
|
||||||
let request = Client.getStatus(id: status.id)
|
let request = Client.getStatus(id: status.id)
|
||||||
mastodonController.run(request, completion: { (response) in
|
mastodonController.run(request, completion: { (response) in
|
||||||
switch response {
|
if case let .success(status, _) = response {
|
||||||
case .success(let status, _):
|
|
||||||
// todo: this shouldn't really use the viewContext, but for some reason saving the
|
// todo: this shouldn't really use the viewContext, but for some reason saving the
|
||||||
// backgroundContext with the new version of the status isn't updating the viewContext
|
// backgroundContext with the new version of the status isn't updating the viewContext
|
||||||
DispatchQueue.main.async {
|
mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false, context: mastodonController.persistentContainer.viewContext)
|
||||||
mastodonController.persistentContainer.addOrUpdate(status: status, context: mastodonController.persistentContainer.viewContext)
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
if let toastable = self?.toastableViewController {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Refreshing Poll", in: toastable, retryAction: nil)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}), at: 0)
|
}), at: 0)
|
||||||
|
@ -302,7 +257,7 @@ extension MenuActionProvider {
|
||||||
actions.append(UIWindowScene.ActivationAction { (_) in
|
actions.append(UIWindowScene.ActivationAction { (_) in
|
||||||
return .init(userActivity: activity(), options: options, preview: nil)
|
return .init(userActivity: activity(), options: options, preview: nil)
|
||||||
})
|
})
|
||||||
} else if UIApplication.shared.supportsMultipleScenes {
|
} else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
|
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
|
||||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil)
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil)
|
||||||
}))
|
}))
|
||||||
|
@ -324,13 +279,8 @@ extension MenuActionProvider {
|
||||||
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
||||||
mastodonController.run(request) { response in
|
mastodonController.run(request) { response in
|
||||||
switch response {
|
switch response {
|
||||||
case .failure(let error):
|
case .failure(_):
|
||||||
if let toastable = self.toastableViewController {
|
fatalError()
|
||||||
let config = ToastConfiguration(from: error, with: "Error \(following ? "Unf" : "F")ollowing", in: toastable, retryAction: nil)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .success(let relationship, _):
|
case .success(let relationship, _):
|
||||||
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||||
}
|
}
|
||||||
|
@ -357,13 +307,3 @@ extension SFSafariViewController: CustomPreviewPresenting {
|
||||||
presenter.present(self, animated: true)
|
presenter.present(self, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension UIDeferredMenuElement {
|
|
||||||
static func uncachedIfPossible(_ elementProvider: @escaping (@escaping ([UIMenuElement]) -> Void) -> Void) -> UIDeferredMenuElement {
|
|
||||||
if #available(iOS 15.0, *) {
|
|
||||||
return UIDeferredMenuElement.uncached(elementProvider)
|
|
||||||
} else {
|
|
||||||
return UIDeferredMenuElement(elementProvider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
//
|
|
||||||
// PostService.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 4/27/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class PostService: ObservableObject {
|
|
||||||
private let mastodonController: MastodonController
|
|
||||||
private let draft: Draft
|
|
||||||
let totalSteps: Int
|
|
||||||
|
|
||||||
@Published var currentStep = 1
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController, draft: Draft) {
|
|
||||||
self.mastodonController = mastodonController
|
|
||||||
self.draft = draft
|
|
||||||
// 2 steps (request data, then upload) for each attachment
|
|
||||||
self.totalSteps = 2 + (draft.attachments.count * 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func post() async throws {
|
|
||||||
guard draft.hasContent else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// save before posting, so if a crash occurs during network request, the status won't be lost
|
|
||||||
DraftsManager.save()
|
|
||||||
|
|
||||||
let uploadedAttachments = try await uploadAttachments()
|
|
||||||
|
|
||||||
let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : nil
|
|
||||||
let sensitive = contentWarning != nil
|
|
||||||
|
|
||||||
let request = Client.createStatus(
|
|
||||||
text: draft.textForPosting(on: mastodonController.instanceFeatures),
|
|
||||||
contentType: Preferences.shared.statusContentType,
|
|
||||||
inReplyTo: draft.inReplyToID,
|
|
||||||
media: uploadedAttachments,
|
|
||||||
sensitive: sensitive,
|
|
||||||
spoilerText: contentWarning,
|
|
||||||
visibility: draft.visibility,
|
|
||||||
language: nil,
|
|
||||||
pollOptions: draft.poll?.options.map(\.text),
|
|
||||||
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
|
||||||
pollMultiple: draft.poll?.multiple,
|
|
||||||
localOnly: mastodonController.instanceFeatures.localOnlyPosts ? draft.localOnly : nil
|
|
||||||
)
|
|
||||||
do {
|
|
||||||
let (_, _) = try await mastodonController.run(request)
|
|
||||||
currentStep += 1
|
|
||||||
|
|
||||||
DraftsManager.shared.remove(self.draft)
|
|
||||||
} catch let error as Client.Error {
|
|
||||||
throw Error.posting(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func uploadAttachments() async throws -> [Attachment] {
|
|
||||||
var attachments: [Attachment] = []
|
|
||||||
attachments.reserveCapacity(draft.attachments.count)
|
|
||||||
for (index, attachment) in draft.attachments.enumerated() {
|
|
||||||
let data: Data
|
|
||||||
let mimeType: String
|
|
||||||
do {
|
|
||||||
(data, mimeType) = try await getData(for: attachment)
|
|
||||||
currentStep += 1
|
|
||||||
} catch let error as CompositionAttachmentData.Error {
|
|
||||||
throw Error.attachmentData(index: index, cause: error)
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
let uploaded = try await uploadAttachment(data: data, mimeType: mimeType, description: attachment.description)
|
|
||||||
attachments.append(uploaded)
|
|
||||||
currentStep += 1
|
|
||||||
} catch let error as Client.Error {
|
|
||||||
throw Error.attachmentUpload(index: index, cause: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return attachments
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getData(for attachment: CompositionAttachment) async throws -> (Data, String) {
|
|
||||||
return try await withCheckedThrowingContinuation { continuation in
|
|
||||||
attachment.data.getData { result in
|
|
||||||
switch result {
|
|
||||||
case let .success(res):
|
|
||||||
continuation.resume(returning: res)
|
|
||||||
case let .failure(error):
|
|
||||||
continuation.resume(throwing: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func uploadAttachment(data: Data, mimeType: String, description: String?) async throws -> Attachment {
|
|
||||||
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
|
||||||
let req = Client.upload(attachment: formAttachment, description: description)
|
|
||||||
return try await mastodonController.run(req).0
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Error: Swift.Error, LocalizedError {
|
|
||||||
case attachmentData(index: Int, cause: CompositionAttachmentData.Error)
|
|
||||||
case attachmentUpload(index: Int, cause: Client.Error)
|
|
||||||
case posting(Client.Error)
|
|
||||||
|
|
||||||
var localizedDescription: String {
|
|
||||||
switch self {
|
|
||||||
case let .attachmentData(index: index, cause: cause):
|
|
||||||
return "Attachment \(index + 1): \(cause.localizedDescription)"
|
|
||||||
case let .attachmentUpload(index: index, cause: cause):
|
|
||||||
return "Attachment \(index + 1): \(cause.localizedDescription)"
|
|
||||||
case let .posting(error):
|
|
||||||
return error.localizedDescription
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,25 +10,13 @@ import Foundation
|
||||||
|
|
||||||
extension NSUserActivity {
|
extension NSUserActivity {
|
||||||
|
|
||||||
var displaysAuxiliaryScene: Bool {
|
|
||||||
get {
|
|
||||||
(userInfo?["displaysAuxiliaryScene"] as? Bool) ?? false
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
if userInfo == nil {
|
|
||||||
userInfo = [:]
|
|
||||||
}
|
|
||||||
userInfo!["displaysAuxiliaryScene"] = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(type: UserActivityType) {
|
convenience init(type: UserActivityType) {
|
||||||
self.init(activityType: type.rawValue)
|
self.init(activityType: type.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleResume(manager: UserActivityManager) -> Bool {
|
func handleResume() -> Bool {
|
||||||
guard let type = UserActivityType(rawValue: activityType) else { return false }
|
guard let type = UserActivityType(rawValue: activityType) else { return false }
|
||||||
type.handle(manager)(self)
|
type.handle(self)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,32 +9,25 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Intents
|
import Intents
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import OSLog
|
|
||||||
|
|
||||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UserActivityManager")
|
|
||||||
|
|
||||||
class UserActivityManager {
|
class UserActivityManager {
|
||||||
|
|
||||||
private let scene: UIWindowScene
|
|
||||||
|
|
||||||
init(scene: UIWindowScene) {
|
|
||||||
self.scene = scene
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Utils
|
// MARK: - Utils
|
||||||
private static let encoder = PropertyListEncoder()
|
private static let encoder = PropertyListEncoder()
|
||||||
private static let decoder = PropertyListDecoder()
|
private static let decoder = PropertyListDecoder()
|
||||||
|
|
||||||
private var mastodonController: MastodonController {
|
private static var mastodonController: MastodonController {
|
||||||
scene.session.mastodonController!
|
let scene = UIApplication.shared.activeOrBackgroundScene!
|
||||||
|
return scene.session.mastodonController!
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getMainViewController() -> TuskerRootViewController {
|
private static func getMainViewController() -> TuskerRootViewController {
|
||||||
let window = scene.windows.first { $0.isKeyWindow } ?? scene.windows.first!
|
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
|
||||||
|
let window = scene.windows.first { $0.isKeyWindow }!
|
||||||
return window.rootViewController as! TuskerRootViewController
|
return window.rootViewController as! TuskerRootViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
private func present(_ vc: UIViewController, animated: Bool = true) {
|
private static func present(_ vc: UIViewController, animated: Bool = true) {
|
||||||
getMainViewController().present(vc, animated: animated)
|
getMainViewController().present(vc, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +66,7 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNewPost(activity: NSUserActivity) {
|
static func handleNewPost(activity: NSUserActivity) {
|
||||||
// TODO: check not currently showing compose screen
|
// TODO: check not currently showing compose screen
|
||||||
let mentioning = activity.userInfo?["mentioning"] as? String
|
let mentioning = activity.userInfo?["mentioning"] as? String
|
||||||
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
||||||
|
@ -118,14 +111,14 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCheckNotifications(activity: NSUserActivity) {
|
static func handleCheckNotifications(activity: NSUserActivity) {
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .notifications)
|
mainViewController.select(tab: .notifications)
|
||||||
if let navigationController = mainViewController.getTabController(tab: .notifications) as? UINavigationController,
|
if let navigationController = mainViewController.getTabController(tab: .notifications) as? UINavigationController,
|
||||||
let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController {
|
let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController {
|
||||||
navigationController.popToRootViewController(animated: false)
|
navigationController.popToRootViewController(animated: false)
|
||||||
notificationsPageController.loadViewIfNeeded()
|
notificationsPageController.loadViewIfNeeded()
|
||||||
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
|
notificationsPageController.selectMode(getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,11 +168,11 @@ class UserActivityManager {
|
||||||
let data = activity.userInfo?["timelineData"] as? Data else {
|
let data = activity.userInfo?["timelineData"] as? Data else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return try? UserActivityManager.decoder.decode(Timeline.self, from: data)
|
return try? decoder.decode(Timeline.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleShowTimeline(activity: NSUserActivity) {
|
static func handleShowTimeline(activity: NSUserActivity) {
|
||||||
guard let timeline = Self.getTimeline(from: activity) else { return }
|
guard let timeline = getTimeline(from: activity) else { return }
|
||||||
|
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .timelines)
|
mainViewController.select(tab: .timelines)
|
||||||
|
@ -235,7 +228,7 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSearch(activity: NSUserActivity) {
|
static func handleSearch(activity: NSUserActivity) {
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .explore)
|
mainViewController.select(tab: .explore)
|
||||||
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController,
|
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController,
|
||||||
|
@ -254,7 +247,7 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBookmarks(activity: NSUserActivity) {
|
static func handleBookmarks(activity: NSUserActivity) {
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .explore)
|
mainViewController.select(tab: .explore)
|
||||||
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController {
|
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController {
|
||||||
|
@ -272,7 +265,7 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMyProfile(activity: NSUserActivity) {
|
static func handleMyProfile(activity: NSUserActivity) {
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .myProfile)
|
mainViewController.select(tab: .myProfile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ enum UserActivityType: String {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UserActivityType {
|
extension UserActivityType {
|
||||||
var handle: (UserActivityManager) -> (NSUserActivity) -> Void {
|
var handle: (NSUserActivity) -> Void {
|
||||||
switch self {
|
switch self {
|
||||||
case .mainScene:
|
case .mainScene:
|
||||||
fatalError("cannot handle main scene activity")
|
fatalError("cannot handle main scene activity")
|
||||||
|
|
|
@ -11,7 +11,7 @@ import SwiftSoup
|
||||||
|
|
||||||
class AccountTableViewCell: UITableViewCell {
|
class AccountTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)?
|
weak var delegate: TuskerNavigationDelegate?
|
||||||
var mastodonController: MastodonController! { delegate?.apiController }
|
var mastodonController: MastodonController! { delegate?.apiController }
|
||||||
|
|
||||||
@IBOutlet weak var avatarImageView: UIImageView!
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
|
@ -98,11 +98,13 @@ extension AccountTableViewCell: SelectableTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountTableViewCell: MenuPreviewProvider {
|
extension AccountTableViewCell: MenuPreviewProvider {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||||
|
|
||||||
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 }
|
||||||
return (
|
return (
|
||||||
content: { ProfileViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
content: { ProfileViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
||||||
actions: { self.delegate?.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) ?? [] }
|
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,7 +117,6 @@ extension AccountTableViewCell: DraggableTableViewCell {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: account.url as NSURL)
|
let provider = NSItemProvider(object: account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,11 +78,6 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
||||||
mutAttrString.collapseWhitespace()
|
mutAttrString.collapseWhitespace()
|
||||||
|
|
||||||
let style = NSMutableParagraphStyle()
|
|
||||||
// 2 points is enough that it doesn't make things look weirdly spaced out, but leaves a slight gap when there are multiple lines of emojis
|
|
||||||
style.lineSpacing = 2
|
|
||||||
mutAttrString.addAttribute(.paragraphStyle, value: style, range: mutAttrString.fullRange)
|
|
||||||
|
|
||||||
self.attributedText = mutAttrString
|
self.attributedText = mutAttrString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,9 +99,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
|
|
||||||
switch node.tagName() {
|
switch node.tagName() {
|
||||||
case "br":
|
case "br":
|
||||||
// need to specify defaultFont here b/c otherwise it uses the default 12pt Helvetica which
|
attributed.append(NSAttributedString(string: "\n"))
|
||||||
// screws up its determination of the line height making multiple lines of emojis squash together
|
|
||||||
attributed.append(NSAttributedString(string: "\n", attributes: [.font: defaultFont]))
|
|
||||||
case "a":
|
case "a":
|
||||||
if let link = try? node.attr("href"),
|
if let link = try? node.attr("href"),
|
||||||
let webURL = WebURL(link),
|
let webURL = WebURL(link),
|
||||||
|
@ -114,7 +107,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
||||||
}
|
}
|
||||||
case "p":
|
case "p":
|
||||||
attributed.append(NSAttributedString(string: "\n\n", attributes: [.font: defaultFont]))
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
case "em", "i":
|
case "em", "i":
|
||||||
let currentFont: UIFont
|
let currentFont: UIFont
|
||||||
if attributed.length == 0 {
|
if attributed.length == 0 {
|
||||||
|
@ -136,11 +129,11 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
case "code":
|
case "code":
|
||||||
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
||||||
case "pre":
|
case "pre":
|
||||||
attributed.append(NSAttributedString(string: "\n\n"))
|
|
||||||
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
||||||
case "ol", "ul":
|
|
||||||
attributed.append(NSAttributedString(string: "\n\n"))
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
|
case "ol", "ul":
|
||||||
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
case "li":
|
case "li":
|
||||||
let parentEl = node.parent()!
|
let parentEl = node.parent()!
|
||||||
let parentTag = parentEl.tagName()
|
let parentTag = parentEl.tagName()
|
||||||
|
@ -150,12 +143,12 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
// we use the monospace digit font so that the periods of all the list items line up
|
// we use the monospace digit font so that the periods of all the list items line up
|
||||||
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: defaultFont.pointSize, weight: .regular)])
|
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: defaultFont.pointSize, weight: .regular)])
|
||||||
} else if parentTag == "ul" {
|
} else if parentTag == "ul" {
|
||||||
bullet = NSAttributedString(string: "\u{2022}\t", attributes: [.font: defaultFont])
|
bullet = NSAttributedString(string: "\u{2022}\t")
|
||||||
} else {
|
} else {
|
||||||
bullet = NSAttributedString()
|
bullet = NSAttributedString()
|
||||||
}
|
}
|
||||||
attributed.insert(bullet, at: 0)
|
attributed.insert(bullet, at: 0)
|
||||||
attributed.append(NSAttributedString(string: "\n", attributes: [.font: defaultFont]))
|
attributed.append(NSAttributedString(string: "\n"))
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -250,10 +243,9 @@ extension ContentTextView: UITextViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ContentTextView: MenuActionProvider {
|
extension ContentTextView: MenuPreviewProvider {
|
||||||
var toastableViewController: ToastableViewController? {
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
// todo: pass this down through the text view
|
fatalError("unimplemented")
|
||||||
nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,6 @@ import UIKit
|
||||||
|
|
||||||
class FastAccountSwitcherIndicatorView: UIView {
|
class FastAccountSwitcherIndicatorView: UIView {
|
||||||
|
|
||||||
override var intrinsicContentSize: CGSize {
|
|
||||||
CGSize(width: 10, height: 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import SwiftSoup
|
||||||
|
|
||||||
class ActionNotificationGroupTableViewCell: UITableViewCell {
|
class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)?
|
weak var delegate: TuskerNavigationDelegate?
|
||||||
var mastodonController: MastodonController! { delegate?.apiController }
|
var mastodonController: MastodonController! { delegate?.apiController }
|
||||||
|
|
||||||
@IBOutlet weak var actionImageView: UIImageView!
|
@IBOutlet weak var actionImageView: UIImageView!
|
||||||
|
@ -244,6 +244,8 @@ extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
|
extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||||
|
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
return (content: {
|
return (content: {
|
||||||
let notifications = self.group.notifications
|
let notifications = self.group.notifications
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
class FollowNotificationGroupTableViewCell: UITableViewCell {
|
class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)?
|
weak var delegate: TuskerNavigationDelegate?
|
||||||
var mastodonController: MastodonController! { delegate?.apiController }
|
var mastodonController: MastodonController! { delegate?.apiController }
|
||||||
|
|
||||||
@IBOutlet weak var avatarStackView: UIStackView!
|
@IBOutlet weak var avatarStackView: UIStackView!
|
||||||
|
@ -196,6 +196,8 @@ extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
|
extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||||
|
|
||||||
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.notifications.map { $0.account.id }
|
let accountIDs = self.group.notifications.map { $0.account.id }
|
||||||
|
@ -207,7 +209,7 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
|
||||||
}
|
}
|
||||||
}, actions: {
|
}, actions: {
|
||||||
if accountIDs.count == 1 {
|
if accountIDs.count == 1 {
|
||||||
return self.delegate?.actionsForProfile(accountID: accountIDs.first!, sourceView: self) ?? []
|
return self.actionsForProfile(accountID: accountIDs.first!, sourceView: self)
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -223,7 +225,6 @@ extension FollowNotificationGroupTableViewCell: DraggableTableViewCell {
|
||||||
let notification = group.notifications[0]
|
let notification = group.notifications[0]
|
||||||
let provider = NSItemProvider(object: notification.account.url as NSURL)
|
let provider = NSItemProvider(object: notification.account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: notification.account.id, accountID: mastodonController.accountInfo!.id)
|
let activity = UserActivityManager.showProfileActivity(id: notification.account.id, accountID: mastodonController.accountInfo!.id)
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
class FollowRequestNotificationTableViewCell: UITableViewCell {
|
class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)?
|
weak var delegate: TuskerNavigationDelegate?
|
||||||
var mastodonController: MastodonController! { delegate?.apiController }
|
var mastodonController: MastodonController! { delegate?.apiController }
|
||||||
|
|
||||||
@IBOutlet weak var stackView: UIStackView!
|
@IBOutlet weak var stackView: UIStackView!
|
||||||
|
@ -134,24 +134,13 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
acceptButton.isEnabled = false
|
acceptButton.isEnabled = false
|
||||||
rejectButton.isEnabled = false
|
rejectButton.isEnabled = false
|
||||||
|
|
||||||
Task {
|
let request = Account.rejectFollowRequest(account)
|
||||||
let request = Account.rejectFollowRequest(account)
|
mastodonController.run(request) { (response) in
|
||||||
do {
|
guard case .success(_, _) = response else { fatalError() }
|
||||||
_ = try await mastodonController.run(request)
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||||
self.actionButtonsStackView.isHidden = true
|
self.actionButtonsStackView.isHidden = true
|
||||||
self.addLabel(NSLocalizedString("Rejected", comment: "rejected follow request label"))
|
self.addLabel(NSLocalizedString("Rejected", comment: "rejected follow request label"))
|
||||||
} catch let error as Client.Error {
|
|
||||||
acceptButton.isEnabled = true
|
|
||||||
rejectButton.isEnabled = true
|
|
||||||
if let toastable = delegate?.toastableViewController {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Rejecting Follow", in: toastable) { [weak self] toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
self?.rejectButtonPressed()
|
|
||||||
}
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,25 +149,13 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
acceptButton.isEnabled = false
|
acceptButton.isEnabled = false
|
||||||
rejectButton.isEnabled = false
|
rejectButton.isEnabled = false
|
||||||
|
|
||||||
Task {
|
let request = Account.authorizeFollowRequest(account)
|
||||||
let request = Account.authorizeFollowRequest(account)
|
mastodonController.run(request) { (response) in
|
||||||
do {
|
guard case .success(_, _) = response else { fatalError() }
|
||||||
_ = try await mastodonController.run(request)
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||||
self.actionButtonsStackView.isHidden = true
|
self.actionButtonsStackView.isHidden = true
|
||||||
self.addLabel(NSLocalizedString("Accepted", comment: "accepted follow request label"))
|
self.addLabel(NSLocalizedString("Accepted", comment: "accepted follow request label"))
|
||||||
} catch let error as Client.Error {
|
|
||||||
acceptButton.isEnabled = true
|
|
||||||
rejectButton.isEnabled = true
|
|
||||||
|
|
||||||
if let toastable = delegate?.toastableViewController {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Accepting Follow", in: toastable) { [weak self] toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
self?.acceptButtonPressed()
|
|
||||||
}
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,6 +169,8 @@ extension FollowRequestNotificationTableViewCell: SelectableTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FollowRequestNotificationTableViewCell: MenuPreviewProvider {
|
extension FollowRequestNotificationTableViewCell: MenuPreviewProvider {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||||
|
|
||||||
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 }
|
||||||
return (content: {
|
return (content: {
|
||||||
|
@ -206,7 +185,6 @@ extension FollowRequestNotificationTableViewCell: DraggableTableViewCell {
|
||||||
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
||||||
let provider = NSItemProvider(object: account.url as NSURL)
|
let provider = NSItemProvider(object: account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: mastodonController.accountInfo!.id)
|
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: mastodonController.accountInfo!.id)
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import SwiftSoup
|
||||||
|
|
||||||
class PollFinishedTableViewCell: UITableViewCell {
|
class PollFinishedTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)?
|
weak var delegate: TuskerNavigationDelegate?
|
||||||
var mastodonController: MastodonController? { delegate?.apiController }
|
var mastodonController: MastodonController? { delegate?.apiController }
|
||||||
|
|
||||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
|
@ -91,6 +91,8 @@ extension PollFinishedTableViewCell: SelectableTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PollFinishedTableViewCell: MenuPreviewProvider {
|
extension PollFinishedTableViewCell: MenuPreviewProvider {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { delegate }
|
||||||
|
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
guard let delegate = delegate,
|
guard let delegate = delegate,
|
||||||
let statusID = notification?.status?.id,
|
let statusID = notification?.status?.id,
|
||||||
|
@ -100,7 +102,7 @@ extension PollFinishedTableViewCell: MenuPreviewProvider {
|
||||||
return (content: {
|
return (content: {
|
||||||
delegate.conversation(mainStatusID: statusID, state: .unknown)
|
delegate.conversation(mainStatusID: statusID, state: .unknown)
|
||||||
}, actions: {
|
}, actions: {
|
||||||
delegate.actionsForStatus(status, sourceView: self)
|
self.actionsForStatus(status, sourceView: self)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
//
|
|
||||||
// PollVoteButton.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/1/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
/// Wraps a UILabel and UIButton to allow setting disabled titles on Catalyst, where `setTitle(_:for:)` only works for the normal state.
|
|
||||||
class PollVoteButton: UIView {
|
|
||||||
|
|
||||||
var disabledTitle: String = "" {
|
|
||||||
didSet {
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var isEnabled = true {
|
|
||||||
didSet {
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var button = UIButton(type: .system)
|
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
private var label = UILabel()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
button.setTitleColor(.secondaryLabel, for: .disabled)
|
|
||||||
embedSubview(button)
|
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
label.textColor = .secondaryLabel
|
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
embedSubview(label)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func addTarget(_ target: Any, action: Selector) {
|
|
||||||
button.addTarget(target, action: action, for: .touchUpInside)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setFont(_ font: UIFont) {
|
|
||||||
button.titleLabel!.font = font
|
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
label.font = font
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private func update() {
|
|
||||||
button.isEnabled = isEnabled
|
|
||||||
if isEnabled {
|
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
label.isHidden = true
|
|
||||||
button.isHidden = false
|
|
||||||
#endif
|
|
||||||
button.setTitle("Vote", for: .normal)
|
|
||||||
} else {
|
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
label.text = disabledTitle
|
|
||||||
label.isHidden = false
|
|
||||||
button.isHidden = true
|
|
||||||
#else
|
|
||||||
button.setTitle(disabledTitle, for: .disabled)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -21,13 +21,12 @@ class StatusPollView: UIView {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
weak var toastableViewController: ToastableViewController?
|
|
||||||
|
|
||||||
private var statusID: String!
|
private var statusID: String!
|
||||||
private(set) var poll: Poll?
|
private(set) var poll: Poll?
|
||||||
|
|
||||||
private var optionsView: PollOptionsView!
|
private var optionsView: PollOptionsView!
|
||||||
private var voteButton: PollVoteButton!
|
private var voteButton: UIButton!
|
||||||
private var infoLabel: UILabel!
|
private var infoLabel: UILabel!
|
||||||
|
|
||||||
private var canVote = true
|
private var canVote = true
|
||||||
|
@ -55,16 +54,12 @@ class StatusPollView: UIView {
|
||||||
infoLabel.adjustsFontSizeToFitWidth = true
|
infoLabel.adjustsFontSizeToFitWidth = true
|
||||||
addSubview(infoLabel)
|
addSubview(infoLabel)
|
||||||
|
|
||||||
// voteButton = UIButton(type: .system)
|
voteButton = UIButton(type: .system)
|
||||||
// voteButton.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
// voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside)
|
|
||||||
// voteButton.setTitle("Vote", for: .normal)
|
|
||||||
// voteButton.setTitleColor(.secondaryLabel, for: .disabled)
|
|
||||||
// voteButton.titleLabel!.font = infoLabel.font
|
|
||||||
voteButton = PollVoteButton()
|
|
||||||
voteButton.translatesAutoresizingMaskIntoConstraints = false
|
voteButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
voteButton.addTarget(self, action: #selector(votePressed))
|
voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside)
|
||||||
voteButton.setFont(infoLabel.font)
|
voteButton.setTitle("Vote", for: .normal)
|
||||||
|
voteButton.setTitleColor(.secondaryLabel, for: .disabled)
|
||||||
|
voteButton.titleLabel!.font = infoLabel.font
|
||||||
addSubview(voteButton)
|
addSubview(voteButton)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -122,17 +117,17 @@ class StatusPollView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
if expired {
|
if expired {
|
||||||
voteButton.disabledTitle = "Expired"
|
voteButton.setTitle("Expired", for: .disabled)
|
||||||
} else if poll.voted ?? false {
|
} else if poll.voted ?? false {
|
||||||
if status.account.id == mastodonController.account.id {
|
if status.account.id == mastodonController.account.id {
|
||||||
voteButton.isHidden = true
|
voteButton.setTitle("", for: .disabled)
|
||||||
} else {
|
} else {
|
||||||
voteButton.disabledTitle = "Voted"
|
voteButton.setTitle("Voted", for: .disabled)
|
||||||
}
|
}
|
||||||
} else if poll.multiple {
|
} else if poll.multiple {
|
||||||
voteButton.disabledTitle = "Select multiple"
|
voteButton.setTitle("Select multiple", for: .disabled)
|
||||||
} else {
|
} else {
|
||||||
voteButton.disabledTitle = "Select one"
|
voteButton.setTitle("Select one", for: .disabled)
|
||||||
}
|
}
|
||||||
voteButton.isEnabled = false
|
voteButton.isEnabled = false
|
||||||
}
|
}
|
||||||
|
@ -144,7 +139,7 @@ class StatusPollView: UIView {
|
||||||
@objc private func votePressed() {
|
@objc private func votePressed() {
|
||||||
optionsView.isEnabled = false
|
optionsView.isEnabled = false
|
||||||
voteButton.isEnabled = false
|
voteButton.isEnabled = false
|
||||||
voteButton.disabledTitle = "Voted"
|
voteButton.setTitle("Voted", for: .disabled)
|
||||||
|
|
||||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
|
|
||||||
|
@ -152,14 +147,7 @@ class StatusPollView: UIView {
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
DispatchQueue.main.async {
|
fatalError("error voting in poll: \(error)")
|
||||||
self.updateUI(status: self.mastodonController.persistentContainer.status(for: self.statusID)!, poll: self.poll)
|
|
||||||
|
|
||||||
if let toastable = self.toastableViewController {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Voting", in: toastable, retryAction: nil)
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case let .success(poll, _):
|
case let .success(poll, _):
|
||||||
let container = self.mastodonController.persistentContainer
|
let container = self.mastodonController.persistentContainer
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate, MenuActionProvider {
|
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate {
|
||||||
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int)
|
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
updateImages(account: account)
|
updateImages(account: account)
|
||||||
|
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, sourceView: moreButton) ?? [])
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForProfile(accountID: accountID, sourceView: moreButton))
|
||||||
|
|
||||||
noteTextView.navigationDelegate = delegate
|
noteTextView.navigationDelegate = delegate
|
||||||
noteTextView.setTextFromHtml(account.note)
|
noteTextView.setTextFromHtml(account.note)
|
||||||
|
@ -182,12 +182,12 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
@objc private func updateUIForPreferences() {
|
@objc private func updateUIForPreferences() {
|
||||||
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
|
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController else {
|
||||||
// nil if prefs changed before own account is loaded
|
|
||||||
let accountID = accountID,
|
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID) else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||||
|
fatalError("Missing cached account \(accountID!)")
|
||||||
|
}
|
||||||
|
|
||||||
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||||
|
@ -272,3 +272,7 @@ extension ProfileHeaderView: UIPointerInteractionDelegate {
|
||||||
return UIPointerStyle(effect: .lift(preview), shape: .none)
|
return UIPointerStyle(effect: .lift(preview), shape: .none)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ProfileHeaderView: MenuPreviewProvider {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { delegate }
|
||||||
|
}
|
||||||
|
|
|
@ -11,11 +11,11 @@ import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
import AVKit
|
import AVKit
|
||||||
|
|
||||||
protocol StatusTableViewCellDelegate: TuskerNavigationDelegate, MenuActionProvider {
|
protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
class BaseStatusTableViewCell: UITableViewCell {
|
class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
|
|
||||||
weak var delegate: StatusTableViewCellDelegate? {
|
weak var delegate: StatusTableViewCellDelegate? {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -153,8 +153,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
cardView.card = status.card
|
cardView.card = status.card
|
||||||
cardView.isHidden = status.card == nil
|
cardView.isHidden = status.card == nil
|
||||||
cardView.navigationDelegate = delegate
|
cardView.navigationDelegate = navigationDelegate
|
||||||
cardView.actionProvider = delegate
|
|
||||||
|
|
||||||
attachmentsView.updateUI(status: status)
|
attachmentsView.updateUI(status: status)
|
||||||
|
|
||||||
|
@ -207,11 +206,10 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||||
// do not include reply action here, because the cell already contains a button for it
|
// do not include reply action here, because the cell already contains a button for it
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, sourceView: moreButton, includeReply: false) ?? [])
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton, includeReply: false))
|
||||||
|
|
||||||
pollView.isHidden = status.poll == nil
|
pollView.isHidden = status.poll == nil
|
||||||
pollView.mastodonController = mastodonController
|
pollView.mastodonController = mastodonController
|
||||||
pollView.toastableViewController = delegate?.toastableViewController
|
|
||||||
pollView.updateUI(status: status, poll: status.poll)
|
pollView.updateUI(status: status, poll: status.poll)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,6 +326,13 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
showStatusAutomatically = false
|
showStatusAutomatically = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - MenuPreviewProvider
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||||
|
|
||||||
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@IBAction func collapseButtonPressed() {
|
@IBAction func collapseButtonPressed() {
|
||||||
|
@ -388,7 +393,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if case let .success(newStatus, _) = response {
|
if case let .success(newStatus, _) = response {
|
||||||
self.favorited = newStatus.favourited ?? false
|
self.favorited = newStatus.favourited ?? false
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus)
|
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
|
||||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||||
} else {
|
} else {
|
||||||
self.favorited = oldValue
|
self.favorited = oldValue
|
||||||
|
@ -430,7 +435,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if case let .success(newStatus, _) = response {
|
if case let .success(newStatus, _) = response {
|
||||||
self.reblogged = newStatus.reblogged ?? false
|
self.reblogged = newStatus.reblogged ?? false
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus)
|
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
|
||||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||||
} else {
|
} else {
|
||||||
self.reblogged = oldValue
|
self.reblogged = oldValue
|
||||||
|
@ -531,7 +536,6 @@ extension BaseStatusTableViewCell: UIDragInteractionDelegate {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: account.url as NSURL)
|
let provider = NSItemProvider(object: account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
|
let activity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ extension ConversationMainStatusTableViewCell: UIContextMenuInteractionDelegate
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
|
ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
|
||||||
} actionProvider: { (_) in
|
} actionProvider: { (_) in
|
||||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.delegate?.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) ?? [])
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import WebURLFoundationExtras
|
||||||
class StatusCardView: UIView {
|
class StatusCardView: UIView {
|
||||||
|
|
||||||
weak var navigationDelegate: TuskerNavigationDelegate?
|
weak var navigationDelegate: TuskerNavigationDelegate?
|
||||||
weak var actionProvider: MenuActionProvider?
|
|
||||||
|
|
||||||
var card: Card? {
|
var card: Card? {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -213,6 +212,9 @@ class StatusCardView: UIView {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusCardView: MenuPreviewProvider {
|
||||||
|
}
|
||||||
|
|
||||||
extension StatusCardView: UIContextMenuInteractionDelegate {
|
extension StatusCardView: UIContextMenuInteractionDelegate {
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
guard let card = card else { return nil }
|
guard let card = card else { return nil }
|
||||||
|
@ -220,7 +222,7 @@ extension StatusCardView: UIContextMenuInteractionDelegate {
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
return SFSafariViewController(url: URL(card.url)!)
|
return SFSafariViewController(url: URL(card.url)!)
|
||||||
} actionProvider: { (_) in
|
} actionProvider: { (_) in
|
||||||
let actions = self.actionProvider?.actionsForURL(URL(card.url)!, sourceView: self) ?? []
|
let actions = self.actionsForURL(URL(card.url)!, sourceView: self)
|
||||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,6 +214,17 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
reply()
|
reply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? {
|
||||||
|
guard let mastodonController = mastodonController,
|
||||||
|
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) },
|
||||||
|
actions: { self.actionsForStatus(status, sourceView: self) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Accessibility
|
// MARK: - Accessibility
|
||||||
|
|
||||||
override var accessibilityLabel: String? {
|
override var accessibilityLabel: String? {
|
||||||
|
@ -282,7 +293,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
completion(true)
|
completion(true)
|
||||||
mastodonController.persistentContainer.addOrUpdate(status: status)
|
mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -309,7 +320,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
completion(true)
|
completion(true)
|
||||||
mastodonController.persistentContainer.addOrUpdate(status: status)
|
mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -356,7 +367,6 @@ extension TimelineStatusTableViewCell: DraggableTableViewCell {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: status.url! as NSURL)
|
let provider = NSItemProvider(object: status.url! as NSURL)
|
||||||
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID)
|
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID)
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
@ -367,13 +377,13 @@ extension TimelineStatusTableViewCell: UIContextMenuInteractionDelegate {
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
|
ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
|
||||||
} actionProvider: { (_) in
|
} actionProvider: { (_) in
|
||||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.delegate?.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) ?? [])
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||||
if let viewController = animator.previewViewController,
|
if let viewController = animator.previewViewController,
|
||||||
let delegate = delegate {
|
let delegate = navigationDelegate {
|
||||||
animator.preferredCommitStyle = .pop
|
animator.preferredCommitStyle = .pop
|
||||||
animator.addCompletion {
|
animator.addCompletion {
|
||||||
if let customPresenting = viewController as? CustomPreviewPresenting {
|
if let customPresenting = viewController as? CustomPreviewPresenting {
|
||||||
|
@ -386,16 +396,3 @@ extension TimelineStatusTableViewCell: UIContextMenuInteractionDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineStatusTableViewCell: MenuPreviewProvider {
|
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
|
||||||
guard let mastodonController = mastodonController,
|
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) },
|
|
||||||
actions: { self.delegate?.actionsForStatus(status, sourceView: self) ?? [] }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -35,14 +35,12 @@ struct ToastConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ToastConfiguration {
|
extension ToastConfiguration {
|
||||||
init(from error: Client.Error, with title: String, in viewController: UIViewController, retryAction: ((ToastView) -> Void)?) {
|
init(from error: Client.Error, with title: String, in viewController: UIViewController, retryAction: @escaping (ToastView) -> Void) {
|
||||||
self.init(title: title)
|
self.init(title: title)
|
||||||
self.subtitle = error.localizedDescription
|
self.subtitle = error.localizedDescription
|
||||||
self.systemImageName = error.systemImageName
|
self.systemImageName = error.systemImageName
|
||||||
if let retryAction = retryAction {
|
self.actionTitle = "Retry"
|
||||||
self.actionTitle = "Retry"
|
self.action = retryAction
|
||||||
self.action = retryAction
|
|
||||||
}
|
|
||||||
self.longPressAction = { [unowned viewController] toast in
|
self.longPressAction = { [unowned viewController] toast in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
let text = """
|
let text = """
|
||||||
|
@ -57,14 +55,6 @@ extension ToastConfiguration {
|
||||||
viewController.present(reporter, animated: true)
|
viewController.present(reporter, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from error: Client.Error, with title: String, in viewController: UIViewController, retryAction: @escaping @MainActor (ToastView) async -> Void) {
|
|
||||||
self.init(from: error, with: title, in: viewController) { toast in
|
|
||||||
Task {
|
|
||||||
await retryAction(toast)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension Client.Error {
|
fileprivate extension Client.Error {
|
||||||
|
|
|
@ -12,7 +12,6 @@ class ToastView: UIView {
|
||||||
|
|
||||||
let configuration: ToastConfiguration
|
let configuration: ToastConfiguration
|
||||||
|
|
||||||
private var panRecognizer: UIPanGestureRecognizer!
|
|
||||||
private var shrinkAnimator: UIViewPropertyAnimator?
|
private var shrinkAnimator: UIViewPropertyAnimator?
|
||||||
private var recognizedGesture = false
|
private var recognizedGesture = false
|
||||||
private var handledLongPress = false
|
private var handledLongPress = false
|
||||||
|
@ -102,10 +101,9 @@ class ToastView: UIView {
|
||||||
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
||||||
])
|
])
|
||||||
|
|
||||||
panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
|
||||||
addGestureRecognizer(panRecognizer)
|
addGestureRecognizer(pan)
|
||||||
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressRecognized))
|
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressRecognized))
|
||||||
longPress.delegate = self
|
|
||||||
addGestureRecognizer(longPress)
|
addGestureRecognizer(longPress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,11 +266,3 @@ class ToastView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ToastView: UIGestureRecognizerDelegate {
|
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
||||||
// if another recognizer can recognize simulatenously (e.g., table view cell drag initiation) it should require the toast one to fail
|
|
||||||
// otherwise long-pressing on a toast results in the drag beginning instead
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@MainActor
|
|
||||||
protocol ToastableViewController: UIViewController {
|
protocol ToastableViewController: UIViewController {
|
||||||
|
|
||||||
var toastParentView: UIView { get }
|
var toastParentView: UIView { get }
|
||||||
|
|
|
@ -11,8 +11,8 @@ import SwiftUI
|
||||||
struct WrappedProgressView: UIViewRepresentable {
|
struct WrappedProgressView: UIViewRepresentable {
|
||||||
typealias UIViewType = UIProgressView
|
typealias UIViewType = UIProgressView
|
||||||
|
|
||||||
let value: Int
|
let value: Double
|
||||||
let total: Int
|
let total: Double
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIProgressView {
|
func makeUIView(context: Context) -> UIProgressView {
|
||||||
return UIProgressView(progressViewStyle: .bar)
|
return UIProgressView(progressViewStyle: .bar)
|
||||||
|
@ -20,9 +20,7 @@ struct WrappedProgressView: UIViewRepresentable {
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIProgressView, context: Context) {
|
func updateUIView(_ uiView: UIProgressView, context: Context) {
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
let progress = Float(value) / Float(total)
|
uiView.setProgress(Float(value / total), animated: true)
|
||||||
print(progress)
|
|
||||||
uiView.setProgress(progress, animated: true)
|
|
||||||
} else {
|
} else {
|
||||||
uiView.setProgress(0, animated: true)
|
uiView.setProgress(0, animated: true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,22 +61,6 @@
|
||||||
<string>%u replies</string>
|
<string>%u replies</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>expand threads inline count</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
|
||||||
<string>%#@replies@</string>
|
|
||||||
<key>replies</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSStringFormatSpecTypeKey</key>
|
|
||||||
<string>NSStringPluralRuleType</string>
|
|
||||||
<key>NSStringFormatValueTypeKey</key>
|
|
||||||
<string>u</string>
|
|
||||||
<key>one</key>
|
|
||||||
<string>1 more reply</string>
|
|
||||||
<key>other</key>
|
|
||||||
<string>%u more replies</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>favorites count</key>
|
<key>favorites count</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
|
Loading…
Reference in New Issue