Compare commits
6 Commits
811aac35d7
...
f1a39c2faa
Author | SHA1 | Date |
---|---|---|
Shadowfacts | f1a39c2faa | |
Shadowfacts | ab8e498cee | |
Shadowfacts | c6da754875 | |
Shadowfacts | 97d5b955a0 | |
Shadowfacts | 80f9800fd6 | |
Shadowfacts | 0485400c1f |
|
@ -261,6 +261,10 @@ public class Client {
|
||||||
return Request<Account>(method: .post, path: "/api/v1/follows", body: ParametersBody(["uri" => acct]))
|
return Request<Account>(method: .post, path: "/api/v1/follows", body: ParametersBody(["uri" => acct]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func getFollowedHashtags() -> Request<[Hashtag]> {
|
||||||
|
return Request(method: .get, path: "/api/v1/followed_tags")
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Lists
|
// MARK: - Lists
|
||||||
public static func getLists() -> Request<[List]> {
|
public static func getLists() -> Request<[List]> {
|
||||||
return Request<[List]>(method: .get, path: "/api/v1/lists")
|
return Request<[List]>(method: .get, path: "/api/v1/lists")
|
||||||
|
|
|
@ -15,11 +15,14 @@ public class Hashtag: Codable {
|
||||||
public let url: WebURL
|
public let url: WebURL
|
||||||
/// Only present when returned from the trending hashtags endpoint
|
/// Only present when returned from the trending hashtags endpoint
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
|
/// Only present on Mastodon >= 4 and when logged in
|
||||||
|
public let following: Bool?
|
||||||
|
|
||||||
public init(name: String, url: URL) {
|
public init(name: String, url: URL) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = WebURL(url)!
|
self.url = WebURL(url)!
|
||||||
self.history = nil
|
self.history = nil
|
||||||
|
self.following = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
public required init(from decoder: Decoder) throws {
|
||||||
|
@ -28,6 +31,7 @@ public class Hashtag: Codable {
|
||||||
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
self.url = try container.decode(WebURL.self, forKey: .url)
|
||||||
self.history = try container.decodeIfPresent([History].self, forKey: .history)
|
self.history = try container.decodeIfPresent([History].self, forKey: .history)
|
||||||
|
self.following = try container.decodeIfPresent(Bool.self, forKey: .following)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
@ -35,12 +39,22 @@ public class Hashtag: Codable {
|
||||||
try container.encode(name, forKey: .name)
|
try container.encode(name, forKey: .name)
|
||||||
try container.encode(url, forKey: .url)
|
try container.encode(url, forKey: .url)
|
||||||
try container.encodeIfPresent(history, forKey: .history)
|
try container.encodeIfPresent(history, forKey: .history)
|
||||||
|
try container.encodeIfPresent(following, forKey: .following)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func follow(name: String) -> Request<Hashtag> {
|
||||||
|
return Request(method: .post, path: "/api/v1/tags/\(name)/follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func unfollow(name: String) -> Request<Hashtag> {
|
||||||
|
return Request(method: .post, path: "/api/v1/tags/\(name)/unfollow")
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case name
|
case name
|
||||||
case url
|
case url
|
||||||
case history
|
case history
|
||||||
|
case following
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,8 @@
|
||||||
D61F758E2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */; };
|
D61F758E2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */; };
|
||||||
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758F29353B4300C0B37F /* FileManager+Size.swift */; };
|
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758F29353B4300C0B37F /* FileManager+Size.swift */; };
|
||||||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
||||||
|
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
|
||||||
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */; };
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||||
|
@ -292,7 +294,7 @@
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
||||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
|
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
|
||||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||||
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
|
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* Weak.swift */; };
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
|
||||||
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 */; };
|
||||||
|
@ -416,6 +418,8 @@
|
||||||
D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusUpdatedNotificationTableViewCell.xib; sourceTree = "<group>"; };
|
D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusUpdatedNotificationTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D61F758F29353B4300C0B37F /* FileManager+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Size.swift"; sourceTree = "<group>"; };
|
D61F758F29353B4300C0B37F /* FileManager+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Size.swift"; sourceTree = "<group>"; };
|
||||||
D61F759129365C6C00C0B37F /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = "<group>"; };
|
D61F759129365C6C00C0B37F /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = "<group>"; };
|
||||||
|
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleFollowHashtagService.swift; sourceTree = "<group>"; };
|
||||||
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -666,7 +670,7 @@
|
||||||
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
||||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
|
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
|
D6DFC69F242C4CCC00ACC392 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
||||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
|
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
|
||||||
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInTusker.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInTusker.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D6E343AA265AAD6B00C4AA01 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
|
D6E343AA265AAD6B00C4AA01 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
|
||||||
|
@ -881,6 +885,7 @@
|
||||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
|
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
|
||||||
D6B9366E2828452F00237D0E /* SavedHashtag.swift */,
|
D6B9366E2828452F00237D0E /* SavedHashtag.swift */,
|
||||||
D6B9366C2828444F00237D0E /* SavedInstance.swift */,
|
D6B9366C2828444F00237D0E /* SavedInstance.swift */,
|
||||||
|
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */,
|
||||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
||||||
);
|
);
|
||||||
|
@ -1418,7 +1423,7 @@
|
||||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||||
D6895DE828D962C2006341DA /* TimelineLikeController.swift */,
|
D6895DE828D962C2006341DA /* TimelineLikeController.swift */,
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
||||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
D6DFC69F242C4CCC00ACC392 /* Weak.swift */,
|
||||||
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */,
|
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */,
|
||||||
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
|
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
|
||||||
D6AEBB3F2321640F00E5038B /* Activities */,
|
D6AEBB3F2321640F00E5038B /* Activities */,
|
||||||
|
@ -1526,6 +1531,7 @@
|
||||||
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
|
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
|
||||||
D6F6A551291F098700F496A8 /* RenameListService.swift */,
|
D6F6A551291F098700F496A8 /* RenameListService.swift */,
|
||||||
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */,
|
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */,
|
||||||
|
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */,
|
||||||
);
|
);
|
||||||
path = API;
|
path = API;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1824,6 +1830,7 @@
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||||
|
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */,
|
||||||
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */,
|
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */,
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
||||||
|
@ -1994,6 +2001,7 @@
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||||
|
@ -2006,7 +2014,7 @@
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
||||||
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
|
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
|
||||||
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */,
|
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
|
||||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
|
||||||
D61ABEF828EFC3F900B29151 /* ProfileStatusesViewController.swift in Sources */,
|
D61ABEF828EFC3F900B29151 /* ProfileStatusesViewController.swift in Sources */,
|
||||||
D6ADB6EA28E91C30009924AB /* TimelineStatusCollectionViewCell.swift in Sources */,
|
D6ADB6EA28E91C30009924AB /* TimelineStatusCollectionViewCell.swift in Sources */,
|
||||||
|
|
|
@ -84,6 +84,14 @@ struct InstanceFeatures {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var canFollowHashtags: Bool {
|
||||||
|
if case .mastodon(_, .some(let version)) = instanceType {
|
||||||
|
return version >= Version(4, 0, 0)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
|
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
|
||||||
let ver = instance.version.lowercased()
|
let ver = instance.version.lowercased()
|
||||||
if ver.contains("glitch") {
|
if ver.contains("glitch") {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import Combine
|
||||||
|
|
||||||
class MastodonController: ObservableObject {
|
class MastodonController: ObservableObject {
|
||||||
|
|
||||||
|
@ -47,7 +48,10 @@ class MastodonController: ObservableObject {
|
||||||
@Published private(set) var nodeInfo: NodeInfo!
|
@Published private(set) var nodeInfo: NodeInfo!
|
||||||
@Published private(set) var instanceFeatures = InstanceFeatures()
|
@Published private(set) var instanceFeatures = InstanceFeatures()
|
||||||
@Published private(set) var lists: [List] = []
|
@Published private(set) var lists: [List] = []
|
||||||
private(set) var customEmojis: [Emoji]?
|
@Published private(set) var customEmojis: [Emoji]?
|
||||||
|
@Published private(set) var followedHashtags: [FollowedHashtag] = []
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
|
private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
|
||||||
private var ownInstanceRequest: URLSessionTask?
|
private var ownInstanceRequest: URLSessionTask?
|
||||||
|
@ -61,6 +65,29 @@ class MastodonController: ObservableObject {
|
||||||
self.accountInfo = nil
|
self.accountInfo = nil
|
||||||
self.client = Client(baseURL: instanceURL, session: .appDefault)
|
self.client = Client(baseURL: instanceURL, session: .appDefault)
|
||||||
self.transient = transient
|
self.transient = transient
|
||||||
|
|
||||||
|
$instance
|
||||||
|
.combineLatest($nodeInfo)
|
||||||
|
.compactMap { (instance, nodeInfo) in
|
||||||
|
if let instance {
|
||||||
|
return (instance, nodeInfo)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sink { [unowned self] (instance, nodeInfo) in
|
||||||
|
self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
$instanceFeatures
|
||||||
|
.filter { [unowned self] in $0.canFollowHashtags && self.followedHashtags.isEmpty }
|
||||||
|
.sink { [unowned self] _ in
|
||||||
|
Task {
|
||||||
|
await self.loadFollowedHashtags()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
|
@ -230,7 +257,6 @@ class MastodonController: ObservableObject {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.ownInstanceRequest = nil
|
self.ownInstanceRequest = nil
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo)
|
|
||||||
|
|
||||||
for completion in self.pendingOwnInstanceRequestCallbacks {
|
for completion in self.pendingOwnInstanceRequestCallbacks {
|
||||||
completion(.success(instance))
|
completion(.success(instance))
|
||||||
|
@ -248,9 +274,6 @@ class MastodonController: ObservableObject {
|
||||||
case let .success(nodeInfo, _):
|
case let .success(nodeInfo, _):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.nodeInfo = nodeInfo
|
self.nodeInfo = nodeInfo
|
||||||
if let instance = self.instance {
|
|
||||||
self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -308,6 +331,25 @@ class MastodonController: ObservableObject {
|
||||||
self.lists = new
|
self.lists = new
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func loadFollowedHashtags() async {
|
||||||
|
updateFollowedHashtags()
|
||||||
|
|
||||||
|
let req = Client.getFollowedHashtags()
|
||||||
|
if let (hashtags, _) = try? await run(req) {
|
||||||
|
self.persistentContainer.updateFollowedHashtags(hashtags) {
|
||||||
|
if case .success(let hashtags) = $0 {
|
||||||
|
self.followedHashtags = hashtags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func updateFollowedHashtags() {
|
||||||
|
followedHashtags = (try? persistentContainer.viewContext.fetch(FollowedHashtag.fetchRequest())) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct ListComparator: SortComparator {
|
private struct ListComparator: SortComparator {
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
//
|
||||||
|
// ToggleFollowHashtagService.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/29/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class ToggleFollowHashtagService {
|
||||||
|
|
||||||
|
private let hashtag: Hashtag
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
private let presenter: any TuskerNavigationDelegate
|
||||||
|
|
||||||
|
init(hashtag: Hashtag, presenter: any TuskerNavigationDelegate) {
|
||||||
|
self.hashtag = hashtag
|
||||||
|
self.mastodonController = presenter.apiController
|
||||||
|
self.presenter = presenter
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleFollow() async {
|
||||||
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
|
var config: ToastConfiguration
|
||||||
|
if let existing = mastodonController.followedHashtags.first(where: { $0.name == hashtag.name }) {
|
||||||
|
do {
|
||||||
|
let req = Hashtag.unfollow(name: hashtag.name)
|
||||||
|
_ = try await mastodonController.run(req)
|
||||||
|
|
||||||
|
context.delete(existing)
|
||||||
|
mastodonController.updateFollowedHashtags()
|
||||||
|
|
||||||
|
config = ToastConfiguration(title: "Unfollowed Hashtag")
|
||||||
|
config.systemImageName = "checkmark"
|
||||||
|
config.dismissAutomaticallyAfter = 2
|
||||||
|
} catch {
|
||||||
|
config = ToastConfiguration(from: error, with: "Error Unfollowing Hashtag", in: presenter) { [unowned self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self.toggleFollow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
let req = Hashtag.follow(name: hashtag.name)
|
||||||
|
let (hashtag, _) = try await mastodonController.run(req)
|
||||||
|
|
||||||
|
_ = FollowedHashtag(hashtag: hashtag, context: context)
|
||||||
|
mastodonController.updateFollowedHashtags()
|
||||||
|
|
||||||
|
config = ToastConfiguration(title: "Followed Hashtag")
|
||||||
|
config.systemImageName = "checkmark"
|
||||||
|
config.dismissAutomaticallyAfter = 2
|
||||||
|
} catch {
|
||||||
|
config = ToastConfiguration(from: error, with: "Error Following Hashtag", in: presenter) { [unowned self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self.toggleFollow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
presenter.showToast(configuration: config, animated: true)
|
||||||
|
mastodonController.persistentContainer.save(context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
//
|
||||||
|
// FollowedHashtag.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/29/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import Pachyderm
|
||||||
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
|
@objc(FollowedHashtag)
|
||||||
|
public final class FollowedHashtag: NSManagedObject {
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<FollowedHashtag> {
|
||||||
|
return NSFetchRequest<FollowedHashtag>(entityName: "FollowedHashtag")
|
||||||
|
}
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest(name: String) -> NSFetchRequest<FollowedHashtag> {
|
||||||
|
let req = NSFetchRequest<FollowedHashtag>(entityName: "FollowedHashtag")
|
||||||
|
req.predicate = NSPredicate(format: "name = %@", name)
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged public var name: String
|
||||||
|
@NSManaged public var url: URL
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FollowedHashtag {
|
||||||
|
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
||||||
|
self.init(context: context)
|
||||||
|
self.name = hashtag.name
|
||||||
|
self.url = URL(hashtag.url)!
|
||||||
|
}
|
||||||
|
}
|
|
@ -267,6 +267,38 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateFollowedHashtags(_ hashtags: [Hashtag], completion: @escaping (Result<[FollowedHashtag], Error>) -> Void) {
|
||||||
|
viewContext.perform {
|
||||||
|
do {
|
||||||
|
var all = try self.viewContext.fetch(FollowedHashtag.fetchRequest())
|
||||||
|
|
||||||
|
let toDelete = all.filter { existing in !hashtags.contains(where: { $0.name == existing.name }) }.map(\.objectID)
|
||||||
|
if !toDelete.isEmpty {
|
||||||
|
try self.viewContext.execute(NSBatchDeleteRequest(objectIDs: toDelete))
|
||||||
|
}
|
||||||
|
|
||||||
|
for hashtag in hashtags where !all.contains(where: { $0.name == hashtag.name}) {
|
||||||
|
let mo = FollowedHashtag(hashtag: hashtag, context: self.viewContext)
|
||||||
|
all.append(mo)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save(context: self.viewContext)
|
||||||
|
completion(.success(all))
|
||||||
|
} catch {
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasFollowedHashtag(_ hashtag: Hashtag) -> Bool {
|
||||||
|
do {
|
||||||
|
let req = FollowedHashtag.fetchRequest(name: name)
|
||||||
|
return try viewContext.count(for: req) > 0
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
||||||
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
||||||
if changes.hashtags {
|
if changes.hashtags {
|
||||||
|
|
|
@ -28,6 +28,10 @@
|
||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="FollowedHashtag" representedClassName="FollowedHashtag" syncable="YES">
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="url" attributeType="URI"/>
|
||||||
|
</entity>
|
||||||
<entity name="Relationship" representedClassName="RelationshipMO" syncable="YES">
|
<entity name="Relationship" representedClassName="RelationshipMO" syncable="YES">
|
||||||
<attribute name="accountID" optional="YES" attributeType="String"/>
|
<attribute name="accountID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="blocking" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="blocking" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
|
|
@ -125,7 +125,7 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo
|
||||||
|
|
||||||
private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
|
private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
|
||||||
let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in
|
let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in
|
||||||
container.navigationDelegate.showMoreOptions(forStatus: status.id, sourceView: container)
|
container.navigationDelegate.showMoreOptions(forStatus: status.id, source: .view(container))
|
||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
// bold to more closesly match other action symbols
|
// bold to more closesly match other action symbols
|
||||||
|
|
|
@ -88,7 +88,7 @@ extension AccountListViewController: UICollectionViewDelegate {
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
|
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
|
||||||
} actionProvider: { _ in
|
} actionProvider: { _ in
|
||||||
UIMenu(children: self.actionsForProfile(accountID: id, sourceView: cell))
|
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,7 +174,7 @@ extension ProfileDirectoryViewController: UICollectionViewDelegate {
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
return ProfileViewController(accountID: account.id, mastodonController: self.mastodonController)
|
return ProfileViewController(accountID: account.id, mastodonController: self.mastodonController)
|
||||||
} actionProvider: { (_) in
|
} actionProvider: { (_) in
|
||||||
let actions = self.actionsForProfile(accountID: account.id, sourceView: self.collectionView.cellForItem(at: indexPath))
|
let actions = self.actionsForProfile(accountID: account.id, source: .view(self.collectionView.cellForItem(at: indexPath)))
|
||||||
return UIMenu(children: actions)
|
return UIMenu(children: actions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ extension TrendingHashtagsViewController: UICollectionViewDelegate {
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
||||||
} actionProvider: { (_) in
|
} actionProvider: { (_) in
|
||||||
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.collectionView.cellForItem(at: indexPath)))
|
UIMenu(children: self.actionsForHashtag(hashtag, source: .view(self.collectionView.cellForItem(at: indexPath))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,7 +270,7 @@ extension SearchViewController: UICollectionViewDelegate {
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
||||||
} actionProvider: { (_) in
|
} actionProvider: { (_) in
|
||||||
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.collectionView.cellForItem(at: indexPath)))
|
UIMenu(children: self.actionsForHashtag(hashtag, source: .view(self.collectionView.cellForItem(at: indexPath))))
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .link(card):
|
case let .link(card):
|
||||||
|
|
|
@ -206,7 +206,7 @@ extension StatusActionAccountListViewController: UICollectionViewDelegate {
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
|
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
|
||||||
} actionProvider: { _ in
|
} actionProvider: { _ in
|
||||||
UIMenu(children: self.actionsForProfile(accountID: id, sourceView: cell))
|
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,18 +14,15 @@ class HashtagTimelineViewController: TimelineViewController {
|
||||||
let hashtag: Hashtag
|
let hashtag: Hashtag
|
||||||
|
|
||||||
var toggleSaveButton: UIBarButtonItem!
|
var toggleSaveButton: UIBarButtonItem!
|
||||||
var toggleSaveButtonTitle: String {
|
|
||||||
if isHashtagSaved {
|
|
||||||
return NSLocalizedString("Unsave", comment: "unsave hashtag button")
|
|
||||||
} else {
|
|
||||||
return NSLocalizedString("Save", comment: "save hashtag button")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isHashtagSaved: Bool {
|
private var isHashtagSaved: Bool {
|
||||||
mastodonController.persistentContainer.viewContext.objectExists(for: SavedHashtag.fetchRequest(name: hashtag.name))
|
mastodonController.persistentContainer.viewContext.objectExists(for: SavedHashtag.fetchRequest(name: hashtag.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isHashtagFollowed: Bool {
|
||||||
|
mastodonController.followedHashtags.contains(where: { $0.name == hashtag.name })
|
||||||
|
}
|
||||||
|
|
||||||
init(for hashtag: Hashtag, mastodonController: MastodonController) {
|
init(for hashtag: Hashtag, mastodonController: MastodonController) {
|
||||||
self.hashtag = hashtag
|
self.hashtag = hashtag
|
||||||
|
|
||||||
|
@ -39,19 +36,16 @@ class HashtagTimelineViewController: TimelineViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
toggleSaveButton = UIBarButtonItem(title: toggleSaveButtonTitle, style: .plain, target: self, action: #selector(toggleSaveButtonPressed))
|
let menu = UIMenu(children: [
|
||||||
navigationItem.rightBarButtonItem = toggleSaveButton
|
// uncached so that the saved/followed updates every time
|
||||||
|
UIDeferredMenuElement.uncached({ [unowned self] elementHandler in
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
|
elementHandler(actionsForHashtag(hashtag, source: .barButtonItem(self.navigationItem.rightBarButtonItem!)))
|
||||||
|
})
|
||||||
|
])
|
||||||
|
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), menu: menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func savedHashtagsChanged() {
|
private func toggleSave() {
|
||||||
toggleSaveButton.title = toggleSaveButtonTitle
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Interaction
|
|
||||||
|
|
||||||
@objc func toggleSaveButtonPressed() {
|
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
|
@ -61,4 +55,10 @@ class HashtagTimelineViewController: TimelineViewController {
|
||||||
mastodonController.persistentContainer.save(context: context)
|
mastodonController.persistentContainer.save(context: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func toggleFollow() {
|
||||||
|
Task {
|
||||||
|
await ToggleFollowHashtagService(hashtag: hashtag, presenter: self).toggleFollow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,6 +101,11 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
// separate method because InstanceTimelineViewController needs to be able to customize it
|
// separate method because InstanceTimelineViewController needs to be able to customize it
|
||||||
func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: StatusState) {
|
func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: StatusState) {
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
|
if case .home = timeline {
|
||||||
|
cell.showFollowedHashtags = true
|
||||||
|
} else {
|
||||||
|
cell.showFollowedHashtags = false
|
||||||
|
}
|
||||||
cell.updateUI(statusID: id, state: state)
|
cell.updateUI(statusID: id, state: state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,21 +348,19 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
|
|
||||||
private func insertPresentItemsIfNecessary(_ presentItems: [String]) {
|
private func insertPresentItemsIfNecessary(_ presentItems: [String]) {
|
||||||
var snapshot = dataSource.snapshot()
|
let snapshot = dataSource.snapshot()
|
||||||
guard snapshot.indexOfSection(.statuses) != nil else {
|
guard snapshot.indexOfSection(.statuses) != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let currentItems = snapshot.itemIdentifiers(inSection: .statuses)
|
let currentItems = snapshot.itemIdentifiers(inSection: .statuses)
|
||||||
if case .status(id: let firstID, state: _) = currentItems.first,
|
if case .status(id: let firstID, state: _) = currentItems.first,
|
||||||
// if there's no overlap between presentItems and the existing items in the data source, insert the present items and prompt the user
|
// if there's no overlap between presentItems and the existing items in the data source, prompt the user
|
||||||
!presentItems.contains(firstID) {
|
!presentItems.contains(firstID) {
|
||||||
|
|
||||||
// remove any existing gap, if there is one
|
// create a new snapshot to reset the timeline to the "present" state
|
||||||
if let index = currentItems.lastIndex(of: .gap) {
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.deleteItems(Array(currentItems[index...]))
|
snapshot.appendSections([.statuses])
|
||||||
}
|
snapshot.appendItems(presentItems.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
|
||||||
snapshot.insertItems([.gap], beforeItem: currentItems.first!)
|
|
||||||
snapshot.insertItems(presentItems.map { .status(id: $0, state: .unknown) }, beforeItem: .gap)
|
|
||||||
|
|
||||||
var config = ToastConfiguration(title: "Jump to present")
|
var config = ToastConfiguration(title: "Jump to present")
|
||||||
config.edge = .top
|
config.edge = .top
|
||||||
|
@ -366,12 +369,34 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
config.action = { [unowned self] toast in
|
config.action = { [unowned self] toast in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
|
|
||||||
|
let origSnapshot = self.dataSource.snapshot()
|
||||||
|
let origItemAtTop: (Item, CGFloat)?
|
||||||
|
if let statusesSection = origSnapshot.indexOfSection(.statuses),
|
||||||
|
let indexPath = self.collectionView.indexPathsForVisibleItems.sorted().first(where: { $0.section == statusesSection }),
|
||||||
|
let cell = self.collectionView.cellForItem(at: indexPath),
|
||||||
|
let item = self.dataSource.itemIdentifier(for: indexPath) {
|
||||||
|
origItemAtTop = (item, cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top)
|
||||||
|
} else {
|
||||||
|
origItemAtTop = nil
|
||||||
|
}
|
||||||
|
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: true) {
|
self.dataSource.apply(snapshot, animatingDifferences: true) {
|
||||||
// TODO: we can't set prevScrollOffsetBeforeScrollToTop here to allow undoing the scroll-to-top
|
|
||||||
// because that would involve scrolling through unmeasured-cell which fucks up the content offset values.
|
|
||||||
// we probably need a data-source aware implementation of scrollToTop which uses item & offset w/in item
|
|
||||||
// to track the restore position
|
|
||||||
self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
|
self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
|
||||||
|
|
||||||
|
var config = ToastConfiguration(title: "Go back")
|
||||||
|
config.edge = .top
|
||||||
|
config.systemImageName = "arrow.down"
|
||||||
|
config.dismissAutomaticallyAfter = 4
|
||||||
|
config.action = { [unowned self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
// todo: it would be nice if we could animate this, but that doesn't work with the screen-position-maintaining stuff
|
||||||
|
if let (item, offset) = origItemAtTop {
|
||||||
|
self.applySnapshot(snapshot, maintainingScreenPosition: offset, ofItem: item)
|
||||||
|
} else {
|
||||||
|
self.dataSource.apply(origSnapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.showToast(configuration: config, animated: true)
|
self.showToast(configuration: config, animated: true)
|
||||||
|
@ -380,32 +405,35 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
|
|
||||||
// NOTE: this only works when items are being inserted ABOVE the item to maintain
|
// NOTE: this only works when items are being inserted ABOVE the item to maintain
|
||||||
private func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, maintainingBottomRelativeScrollPositionOf itemToMaintain: Item) {
|
private func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, maintainingBottomRelativeScrollPositionOf itemToMaintain: Item) {
|
||||||
// use a snapshot of the collection view to hide the flicker as the content offset changes and then changes back
|
|
||||||
let snapshotView = collectionView.snapshotView(afterScreenUpdates: false)!
|
|
||||||
snapshotView.layer.zPosition = 1000
|
|
||||||
snapshotView.frame = view.bounds
|
|
||||||
view.addSubview(snapshotView)
|
|
||||||
|
|
||||||
var firstItemAfterOriginalGapOffsetFromTop: CGFloat = 0
|
var firstItemAfterOriginalGapOffsetFromTop: CGFloat = 0
|
||||||
if let indexPath = dataSource.indexPath(for: itemToMaintain),
|
if let indexPath = dataSource.indexPath(for: itemToMaintain),
|
||||||
let cell = collectionView.cellForItem(at: indexPath) {
|
let cell = collectionView.cellForItem(at: indexPath) {
|
||||||
// subtract top safe area inset b/c scrollToItem at .top aligns the top of the cell to the top of the safe area
|
// subtract top safe area inset b/c scrollToItem at .top aligns the top of the cell to the top of the safe area
|
||||||
firstItemAfterOriginalGapOffsetFromTop = cell.convert(.zero, to: view).y - view.safeAreaInsets.top
|
firstItemAfterOriginalGapOffsetFromTop = cell.convert(.zero, to: view).y - view.safeAreaInsets.top
|
||||||
}
|
}
|
||||||
|
applySnapshot(snapshot, maintainingScreenPosition: firstItemAfterOriginalGapOffsetFromTop, ofItem: itemToMaintain)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, maintainingScreenPosition offsetFromTop: CGFloat, ofItem itemToMaintain: Item) {
|
||||||
|
// use a snapshot of the collection view to hide the flicker as the content offset changes and then changes back
|
||||||
|
let snapshotView = collectionView.snapshotView(afterScreenUpdates: false)!
|
||||||
|
snapshotView.layer.zPosition = 1000
|
||||||
|
snapshotView.frame = view.bounds
|
||||||
|
view.addSubview(snapshotView)
|
||||||
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false) {
|
dataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
if let indexPathOfItemAfterOriginalGap = self.dataSource.indexPath(for: itemToMaintain) {
|
if let indexPathOfItemToMaintain = self.dataSource.indexPath(for: itemToMaintain) {
|
||||||
// scroll up until we've accumulated enough MEASURED height that we can put the
|
// scroll up until we've accumulated enough MEASURED height that we can put the
|
||||||
// firstItemAfterOriginalGapCell at the top of the screen and then scroll down by
|
// firstItemAfterOriginalGapCell at the top of the screen and then scroll down by
|
||||||
// firstItemAfterOriginalGapOffsetFromTop without intruding into unmeasured area
|
// firstItemAfterOriginalGapOffsetFromTop without intruding into unmeasured area
|
||||||
var cur = indexPathOfItemAfterOriginalGap
|
var cur = indexPathOfItemToMaintain
|
||||||
var amountScrolledUp: CGFloat = 0
|
var amountScrolledUp: CGFloat = 0
|
||||||
while true {
|
while true {
|
||||||
if cur.row <= 0 {
|
if cur.row <= 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if let cell = self.collectionView.cellForItem(at: indexPathOfItemAfterOriginalGap),
|
if let cell = self.collectionView.cellForItem(at: indexPathOfItemToMaintain),
|
||||||
cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > firstItemAfterOriginalGapOffsetFromTop {
|
cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > offsetFromTop {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
cur = IndexPath(row: cur.row - 1, section: cur.section)
|
cur = IndexPath(row: cur.row - 1, section: cur.section)
|
||||||
|
@ -415,7 +443,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
amountScrolledUp += attrs.size.height
|
amountScrolledUp += attrs.size.height
|
||||||
}
|
}
|
||||||
self.collectionView.contentOffset.y += amountScrolledUp
|
self.collectionView.contentOffset.y += amountScrolledUp
|
||||||
self.collectionView.contentOffset.y -= firstItemAfterOriginalGapOffsetFromTop
|
self.collectionView.contentOffset.y -= offsetFromTop
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotView.removeFromSuperview()
|
snapshotView.removeFromSuperview()
|
||||||
|
|
|
@ -39,15 +39,15 @@ extension MenuActionProvider {
|
||||||
|
|
||||||
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
|
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
|
||||||
|
|
||||||
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIMenuElement] {
|
func actionsForProfile(accountID: String, source: PopoverSource) -> [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 [] }
|
||||||
|
|
||||||
var shareSection = [
|
var shareSection = [
|
||||||
openInSafariAction(url: account.url),
|
openInSafariAction(url: account.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] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, source: source)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -95,24 +95,27 @@ extension MenuActionProvider {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
func actionsForURL(_ url: URL, source: PopoverSource) -> [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] (_) 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, source: source)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIMenuElement] {
|
func actionsForHashtag(_ hashtag: Hashtag, source: PopoverSource) -> [UIMenuElement] {
|
||||||
let actionsSection: [UIMenuElement]
|
var actionsSection: [UIMenuElement] = []
|
||||||
if let mastodonController = mastodonController,
|
if let mastodonController = mastodonController,
|
||||||
mastodonController.loggedIn {
|
mastodonController.loggedIn {
|
||||||
|
let name = hashtag.name.lowercased()
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first
|
let existing = try? context.fetch(SavedHashtag.fetchRequest(name: name)).first
|
||||||
|
let saveSubtitle = "Saved hashtags appear in the Explore section of Tusker"
|
||||||
|
let saveImage = UIImage(systemName: existing != nil ? "minus" : "plus")
|
||||||
actionsSection = [
|
actionsSection = [
|
||||||
createAction(identifier: "save", title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", systemImageName: "number", handler: { (_) in
|
UIAction(title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", subtitle: saveSubtitle, image: saveImage, handler: { (_) in
|
||||||
if let existing = existing {
|
if let existing = existing {
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
} else {
|
} else {
|
||||||
|
@ -121,13 +124,21 @@ extension MenuActionProvider {
|
||||||
mastodonController.persistentContainer.save(context: context)
|
mastodonController.persistentContainer.save(context: context)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
} else {
|
if mastodonController.instanceFeatures.canFollowHashtags {
|
||||||
actionsSection = []
|
let existing = mastodonController.followedHashtags.first(where: { $0.name.lowercased() == name })
|
||||||
|
let subtitle = "Posts tagged with followed hashtags appear in your Home timeline"
|
||||||
|
let image = UIImage(systemName: existing != nil ? "person.badge.minus" : "person.badge.plus")
|
||||||
|
actionsSection.append(UIAction(title: existing != nil ? "Unfollow" : "Follow", subtitle: subtitle, image: image) { [unowned self] _ in
|
||||||
|
Task {
|
||||||
|
await ToggleFollowHashtagService(hashtag: hashtag, presenter: navigationDelegate!).toggleFollow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let shareSection: [UIMenuElement]
|
let shareSection: [UIMenuElement]
|
||||||
if let url = URL(hashtag.url) {
|
if let url = URL(hashtag.url) {
|
||||||
shareSection = actionsForURL(url, sourceView: sourceView)
|
shareSection = actionsForURL(url, source: source)
|
||||||
} else {
|
} else {
|
||||||
shareSection = []
|
shareSection = []
|
||||||
}
|
}
|
||||||
|
@ -138,16 +149,16 @@ extension MenuActionProvider {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForStatus(_ status: StatusMO, sourceView: UIView?, includeStatusButtonActions: Bool = true) -> [UIMenuElement] {
|
func actionsForStatus(_ status: StatusMO, source: PopoverSource, includeStatusButtonActions: Bool = true) -> [UIMenuElement] {
|
||||||
guard let mastodonController = mastodonController else { return [] }
|
guard let mastodonController = mastodonController else { return [] }
|
||||||
|
|
||||||
guard let accountID = mastodonController.accountInfo?.id else {
|
guard let accountID = mastodonController.accountInfo?.id else {
|
||||||
// user is logged out
|
// user is logged out
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: status.url!),
|
openInSafariAction(url: status.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] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, source: source)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -271,9 +282,9 @@ extension MenuActionProvider {
|
||||||
} else {
|
} else {
|
||||||
Logging.general.fault("Status missing URL: id=\(status.id, privacy: .public), reblog=\((status.reblog?.id).debugDescription, privacy: .public)")
|
Logging.general.fault("Status missing URL: id=\(status.id, privacy: .public), reblog=\((status.reblog?.id).debugDescription, privacy: .public)")
|
||||||
}
|
}
|
||||||
shareSection.append(createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
shareSection.append(createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, source: source)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
||||||
|
|
|
@ -159,21 +159,21 @@ extension TuskerNavigationDelegate {
|
||||||
return UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: nil)
|
return UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showMoreOptions(forStatus statusID: String, sourceView: UIView?) {
|
func showMoreOptions(forStatus statusID: String, source: PopoverSource) {
|
||||||
let vc = moreOptions(forStatus: statusID)
|
let vc = moreOptions(forStatus: statusID)
|
||||||
vc.popoverPresentationController?.sourceView = sourceView
|
source.apply(to: vc)
|
||||||
present(vc, animated: true)
|
present(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showMoreOptions(forURL url: URL, sourceView: UIView?) {
|
func showMoreOptions(forURL url: URL, source: PopoverSource) {
|
||||||
let vc = moreOptions(forURL: url)
|
let vc = moreOptions(forURL: url)
|
||||||
vc.popoverPresentationController?.sourceView = sourceView
|
source.apply(to: vc)
|
||||||
present(vc, animated: true)
|
present(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showMoreOptions(forAccount accountID: String, sourceView: UIView?) {
|
func showMoreOptions(forAccount accountID: String, source: PopoverSource) {
|
||||||
let vc = moreOptions(forAccount: accountID)
|
let vc = moreOptions(forAccount: accountID)
|
||||||
vc.popoverPresentationController?.sourceView = sourceView
|
source.apply(to: vc)
|
||||||
present(vc, animated: true)
|
present(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,3 +188,30 @@ extension TuskerNavigationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PopoverSource {
|
||||||
|
case none
|
||||||
|
case view(WeakHolder<UIView>)
|
||||||
|
case barButtonItem(WeakHolder<UIBarButtonItem>)
|
||||||
|
|
||||||
|
func apply(to viewController: UIViewController) {
|
||||||
|
if let popoverPresentationController = viewController.popoverPresentationController {
|
||||||
|
switch self {
|
||||||
|
case .none:
|
||||||
|
break
|
||||||
|
case .view(let view):
|
||||||
|
popoverPresentationController.sourceView = view.object
|
||||||
|
case .barButtonItem(let item):
|
||||||
|
popoverPresentationController.barButtonItem = item.object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func view(_ view: UIView?) -> Self {
|
||||||
|
.view(WeakHolder(view))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func barButtonItem(_ item: UIBarButtonItem?) -> Self {
|
||||||
|
.barButtonItem(WeakHolder(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ extension AccountTableViewCell: MenuPreviewProvider {
|
||||||
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.delegate?.actionsForProfile(accountID: self.accountID, source: .view(self.avatarImageView)) ?? [] }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -321,11 +321,11 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
|
||||||
let text = (self.text as NSString).substring(with: range)
|
let text = (self.text as NSString).substring(with: range)
|
||||||
let actions: [UIMenuElement]
|
let actions: [UIMenuElement]
|
||||||
if let mention = self.getMention(for: link, text: text) {
|
if let mention = self.getMention(for: link, text: text) {
|
||||||
actions = self.actionsForProfile(accountID: mention.id, sourceView: self)
|
actions = self.actionsForProfile(accountID: mention.id, source: .view(self))
|
||||||
} else if let tag = self.getHashtag(for: link, text: text) {
|
} else if let tag = self.getHashtag(for: link, text: text) {
|
||||||
actions = self.actionsForHashtag(tag, sourceView: self)
|
actions = self.actionsForHashtag(tag, source: .view(self))
|
||||||
} else {
|
} else {
|
||||||
actions = self.actionsForURL(link, sourceView: self)
|
actions = self.actionsForURL(link, source: .view(self))
|
||||||
}
|
}
|
||||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,7 +214,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.delegate?.actionsForProfile(accountID: accountIDs.first!, source: .view(self)) ?? []
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,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)
|
delegate.actionsForStatus(status, source: .view(self))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ extension StatusUpdatedNotificationTableViewCell: MenuPreviewProvider {
|
||||||
return (content: {
|
return (content: {
|
||||||
delegate.conversation(mainStatusID: statusID, state: .unknown)
|
delegate.conversation(mainStatusID: statusID, state: .unknown)
|
||||||
}, actions: {
|
}, actions: {
|
||||||
delegate.actionsForStatus(status, sourceView: self)
|
delegate.actionsForStatus(status, source: .view(self))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,7 +122,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: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton)) ?? [])
|
||||||
|
|
||||||
noteTextView.navigationDelegate = delegate
|
noteTextView.navigationDelegate = delegate
|
||||||
noteTextView.setTextFromHtml(account.note)
|
noteTextView.setTextFromHtml(account.note)
|
||||||
|
|
|
@ -209,7 +209,7 @@ 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, includeStatusButtonActions: false) ?? [])
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? [])
|
||||||
|
|
||||||
pollView.isHidden = status.poll == nil
|
pollView.isHidden = status.poll == nil
|
||||||
pollView.mastodonController = mastodonController
|
pollView.mastodonController = mastodonController
|
||||||
|
@ -408,7 +408,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func morePressed() {
|
@IBAction func morePressed() {
|
||||||
delegate?.showMoreOptions(forStatus: statusID, sourceView: moreButton)
|
delegate?.showMoreOptions(forStatus: statusID, source: .view(moreButton))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func accountPressed() {
|
@objc func accountPressed() {
|
||||||
|
|
|
@ -135,7 +135,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.delegate?.actionsForProfile(accountID: self.accountID, source: .view(self.avatarImageView)) ?? [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,7 +230,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.actionProvider?.actionsForURL(URL(card.url)!, source: .view(self)) ?? []
|
||||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,7 +195,7 @@ extension StatusCollectionViewCell {
|
||||||
|
|
||||||
// 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, includeStatusButtonActions: false) ?? [])
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? [])
|
||||||
|
|
||||||
contentContainer.pollView.isHidden = status.poll == nil
|
contentContainer.pollView.isHidden = status.poll == nil
|
||||||
contentContainer.pollView.mastodonController = mastodonController
|
contentContainer.pollView.mastodonController = mastodonController
|
||||||
|
@ -257,7 +257,7 @@ extension StatusCollectionViewCell {
|
||||||
return UIContextMenuConfiguration() {
|
return UIContextMenuConfiguration() {
|
||||||
ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
|
ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
|
||||||
} actionProvider: { _ in
|
} actionProvider: { _ in
|
||||||
return UIMenu(children: self.delegate?.actionsForProfile(accountID: self.accountID, sourceView: sourceView) ?? [])
|
return UIMenu(children: self.delegate?.actionsForProfile(accountID: self.accountID, source: .view(sourceView)) ?? [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,23 +10,26 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
private let reblogIcon = UIImage(systemName: "repeat")
|
||||||
|
private let hashtagIcon = UIImage(systemName: "number")
|
||||||
|
|
||||||
class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
|
class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
|
||||||
|
|
||||||
static let separatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
|
static let separatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
|
||||||
|
|
||||||
// MARK: Subviews
|
// MARK: Subviews
|
||||||
|
|
||||||
private lazy var rebloggerLabel = EmojiLabel().configure {
|
private lazy var timelineReasonLabel = EmojiLabel().configure {
|
||||||
$0.textColor = .secondaryLabel
|
$0.textColor = .secondaryLabel
|
||||||
$0.font = .preferredFont(forTextStyle: .body)
|
$0.font = .preferredFont(forTextStyle: .body)
|
||||||
$0.adjustsFontForContentSizeCategory = true
|
$0.adjustsFontForContentSizeCategory = true
|
||||||
}
|
}
|
||||||
private let reblogIcon = UIImageView(image: UIImage(systemName: "repeat")).configure {
|
private let timelineReasonIcon = UIImageView(image: reblogIcon).configure {
|
||||||
$0.tintColor = .secondaryLabel
|
$0.tintColor = .secondaryLabel
|
||||||
}
|
}
|
||||||
private lazy var reblogHStack = UIStackView(arrangedSubviews: [
|
private lazy var timelineReasonHStack = UIStackView(arrangedSubviews: [
|
||||||
reblogIcon,
|
timelineReasonIcon,
|
||||||
rebloggerLabel,
|
timelineReasonLabel,
|
||||||
]).configure {
|
]).configure {
|
||||||
$0.axis = .horizontal
|
$0.axis = .horizontal
|
||||||
$0.spacing = 8
|
$0.spacing = 8
|
||||||
|
@ -258,6 +261,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
var showPinned: Bool = false
|
var showPinned: Bool = false
|
||||||
|
var showFollowedHashtags: Bool = false
|
||||||
|
|
||||||
// alas these need to be internal so they're accessible from the protocol extensions
|
// alas these need to be internal so they're accessible from the protocol extensions
|
||||||
var statusID: String!
|
var statusID: String!
|
||||||
|
@ -275,12 +279,12 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
for subview in [reblogHStack, mainContainer, actionsContainer] {
|
for subview in [timelineReasonHStack, mainContainer, actionsContainer] {
|
||||||
subview.translatesAutoresizingMaskIntoConstraints = false
|
subview.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(subview)
|
contentView.addSubview(subview)
|
||||||
}
|
}
|
||||||
|
|
||||||
mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: reblogHStack.bottomAnchor, constant: 4)
|
mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: timelineReasonHStack.bottomAnchor, constant: 4)
|
||||||
mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8)
|
mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8)
|
||||||
mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor, constant: -4)
|
mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor, constant: -4)
|
||||||
mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6)
|
mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6)
|
||||||
|
@ -291,9 +295,9 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
// why is this 4 but the mainContainerTopSelfConstraint constant 8? because this looks more balanced
|
// why is this 4 but the mainContainerTopSelfConstraint constant 8? because this looks more balanced
|
||||||
reblogHStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4),
|
timelineReasonHStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4),
|
||||||
rebloggerLabel.leadingAnchor.constraint(equalTo: contentVStack.leadingAnchor),
|
timelineReasonLabel.leadingAnchor.constraint(equalTo: contentVStack.leadingAnchor),
|
||||||
reblogHStack.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -16),
|
timelineReasonHStack.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -16),
|
||||||
|
|
||||||
mainContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
mainContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||||
mainContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
mainContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||||
|
@ -427,23 +431,35 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
|
|
||||||
self.statusState = state
|
self.statusState = state
|
||||||
|
|
||||||
|
var hideTimelineReason = true
|
||||||
|
|
||||||
if let rebloggedStatus = status.reblog {
|
if let rebloggedStatus = status.reblog {
|
||||||
reblogStatusID = statusID
|
reblogStatusID = statusID
|
||||||
rebloggerID = status.account.id
|
rebloggerID = status.account.id
|
||||||
reblogHStack.isHidden = false
|
|
||||||
mainContainerTopToReblogLabelConstraint.isActive = true
|
|
||||||
mainContainerTopToSelfConstraint.isActive = false
|
|
||||||
updateRebloggerLabel(reblogger: status.account)
|
|
||||||
|
|
||||||
|
hideTimelineReason = false
|
||||||
|
timelineReasonIcon.image = reblogIcon
|
||||||
|
updateRebloggerLabel(reblogger: status.account)
|
||||||
status = rebloggedStatus
|
status = rebloggedStatus
|
||||||
} else {
|
} else {
|
||||||
reblogStatusID = nil
|
reblogStatusID = nil
|
||||||
rebloggerID = nil
|
rebloggerID = nil
|
||||||
reblogHStack.isHidden = true
|
|
||||||
mainContainerTopToReblogLabelConstraint.isActive = false
|
|
||||||
mainContainerTopToSelfConstraint.isActive = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if showFollowedHashtags {
|
||||||
|
let hashtags = mastodonController.followedHashtags.filter({ followed in status.hashtags.contains(where: { followed.name == $0.name }) })
|
||||||
|
if !hashtags.isEmpty {
|
||||||
|
hideTimelineReason = false
|
||||||
|
timelineReasonIcon.image = hashtagIcon
|
||||||
|
timelineReasonLabel.text = hashtags.map(\.name).formatted(.list(type: .and, width: .narrow))
|
||||||
|
timelineReasonLabel.removeEmojis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timelineReasonHStack.isHidden = hideTimelineReason
|
||||||
|
mainContainerTopToReblogLabelConstraint.isActive = !hideTimelineReason
|
||||||
|
mainContainerTopToSelfConstraint.isActive = hideTimelineReason
|
||||||
|
|
||||||
doUpdateUI(status: status)
|
doUpdateUI(status: status)
|
||||||
|
|
||||||
doUpdateTimestamp(status: status)
|
doUpdateTimestamp(status: status)
|
||||||
|
@ -523,11 +539,11 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
|
|
||||||
private func updateRebloggerLabel(reblogger: AccountMO) {
|
private func updateRebloggerLabel(reblogger: AccountMO) {
|
||||||
if Preferences.shared.hideCustomEmojiInUsernames {
|
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||||
rebloggerLabel.text = "\(reblogger.displayNameWithoutCustomEmoji) reblogged"
|
timelineReasonLabel.text = "\(reblogger.displayNameWithoutCustomEmoji) reblogged"
|
||||||
rebloggerLabel.removeEmojis()
|
timelineReasonLabel.removeEmojis()
|
||||||
} else {
|
} else {
|
||||||
rebloggerLabel.text = "\(reblogger.displayOrUserName) reblogged"
|
timelineReasonLabel.text = "\(reblogger.displayOrUserName) reblogged"
|
||||||
rebloggerLabel.setEmojis(reblogger.emojis, identifier: reblogger.id)
|
timelineReasonLabel.setEmojis(reblogger.emojis, identifier: reblogger.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -566,10 +582,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
// MARK: Interaction
|
// MARK: Interaction
|
||||||
|
|
||||||
@objc private func reblogLabelPressed() {
|
@objc private func reblogLabelPressed() {
|
||||||
guard let rebloggerID else {
|
if let rebloggerID {
|
||||||
return
|
delegate?.selected(account: rebloggerID)
|
||||||
|
} else if showFollowedHashtags,
|
||||||
|
let status = mastodonController.persistentContainer.status(for: statusID),
|
||||||
|
let hashtag = mastodonController.followedHashtags.first(where: { followed in status.hashtags.contains(where: { followed.name == $0.name }) }) {
|
||||||
|
delegate?.selected(tag: Hashtag(name: hashtag.name, url: hashtag.url))
|
||||||
}
|
}
|
||||||
delegate?.selected(account: rebloggerID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func accountPressed() {
|
@objc private func accountPressed() {
|
||||||
|
@ -634,7 +653,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: self.mastodonController)
|
ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: self.mastodonController)
|
||||||
} actionProvider: { _ in
|
} actionProvider: { _ in
|
||||||
UIMenu(children: self.delegate!.actionsForStatus(status, sourceView: self))
|
UIMenu(children: self.delegate!.actionsForStatus(status, source: .view(self)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -377,7 +377,7 @@ 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.delegate?.actionsForProfile(accountID: self.accountID, source: .view(self.avatarImageView)) ?? [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,7 +405,7 @@ extension TimelineStatusTableViewCell: MenuPreviewProvider {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) },
|
content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) },
|
||||||
actions: { self.delegate?.actionsForStatus(status, sourceView: self) ?? [] }
|
actions: { self.delegate?.actionsForStatus(status, source: .view(self)) ?? [] }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ extension ToastableViewController {
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if let newValue = newValue {
|
if let newValue = newValue {
|
||||||
objc_setAssociatedObject(self, currentToastKey, WeakHolder(object: newValue), .OBJC_ASSOCIATION_RETAIN)
|
objc_setAssociatedObject(self, currentToastKey, WeakHolder(newValue), .OBJC_ASSOCIATION_RETAIN)
|
||||||
} else {
|
} else {
|
||||||
objc_setAssociatedObject(self, currentToastKey, nil, .OBJC_ASSOCIATION_RETAIN)
|
objc_setAssociatedObject(self, currentToastKey, nil, .OBJC_ASSOCIATION_RETAIN)
|
||||||
}
|
}
|
||||||
|
@ -96,11 +96,3 @@ extension ToastableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate class WeakHolder<T: AnyObject> {
|
|
||||||
weak var object: T?
|
|
||||||
|
|
||||||
init(object: T) {
|
|
||||||
self.object = object
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// WeakArray.swift
|
// Weak.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/25/20.
|
// Created by Shadowfacts on 3/25/20.
|
||||||
|
@ -8,16 +8,16 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
fileprivate class WeakWrapper<T: AnyObject> {
|
class WeakHolder<T: AnyObject> {
|
||||||
weak var value: T?
|
weak var object: T?
|
||||||
|
|
||||||
init(_ value: T?) {
|
init(_ object: T?) {
|
||||||
self.value = value
|
self.object = object
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollection {
|
struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollection {
|
||||||
private var array: [WeakWrapper<Element>]
|
private var array: [WeakHolder<Element>]
|
||||||
|
|
||||||
var startIndex: Int { array.startIndex }
|
var startIndex: Int { array.startIndex }
|
||||||
var endIndex: Int { array.endIndex }
|
var endIndex: Int { array.endIndex }
|
||||||
|
@ -27,19 +27,19 @@ struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollect
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ elements: [Element]) {
|
init(_ elements: [Element]) {
|
||||||
array = elements.map { WeakWrapper($0) }
|
array = elements.map { WeakHolder($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ elements: [Element?]) {
|
init(_ elements: [Element?]) {
|
||||||
array = elements.map { WeakWrapper($0) }
|
array = elements.map { WeakHolder($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript(position: Int) -> Element? {
|
subscript(position: Int) -> Element? {
|
||||||
get {
|
get {
|
||||||
array[position].value
|
array[position].object
|
||||||
}
|
}
|
||||||
set(newValue) {
|
set(newValue) {
|
||||||
array[position] = WeakWrapper(newValue)
|
array[position] = WeakHolder(newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +48,6 @@ struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollect
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Self.Element == C.Element {
|
mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Self.Element == C.Element {
|
||||||
array.replaceSubrange(subrange, with: newElements.map { WeakWrapper($0) })
|
array.replaceSubrange(subrange, with: newElements.map { WeakHolder($0) })
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue