Merge pull request #727 from j-f1/status-cards
Support cards in status views
This commit is contained in:
commit
c37b8b5e74
@ -78,6 +78,7 @@
|
||||
"sign_up": "Create account",
|
||||
"see_more": "See More",
|
||||
"preview": "Preview",
|
||||
"copy": "Copy",
|
||||
"share": "Share",
|
||||
"share_user": "Share %s",
|
||||
"share_post": "Share Post",
|
||||
@ -132,6 +133,8 @@
|
||||
"sensitive_content": "Sensitive Content",
|
||||
"media_content_warning": "Tap anywhere to reveal",
|
||||
"tap_to_reveal": "Tap to reveal",
|
||||
"load_embed": "Load Embed",
|
||||
"link_via_user": "%s via %s",
|
||||
"poll": {
|
||||
"vote": "Vote",
|
||||
"closed": "Closed"
|
||||
@ -153,6 +156,7 @@
|
||||
"show_image": "Show image",
|
||||
"show_gif": "Show GIF",
|
||||
"show_video_player": "Show video player",
|
||||
"share_link_in_post": "Share Link in Post",
|
||||
"tap_then_hold_to_show_menu": "Tap then hold to show menu"
|
||||
},
|
||||
"tag": {
|
||||
|
@ -83,6 +83,7 @@
|
||||
"sign_up": "Create account",
|
||||
"see_more": "See More",
|
||||
"preview": "Preview",
|
||||
"copy": "Copy",
|
||||
"share": "Share",
|
||||
"share_user": "Share %s",
|
||||
"share_post": "Share Post",
|
||||
@ -141,6 +142,8 @@
|
||||
"sensitive_content": "Sensitive Content",
|
||||
"media_content_warning": "Tap anywhere to reveal",
|
||||
"tap_to_reveal": "Tap to reveal",
|
||||
"load_embed": "Load Embed",
|
||||
"link_via_user": "%s via %s",
|
||||
"poll": {
|
||||
"vote": "Vote",
|
||||
"closed": "Closed"
|
||||
@ -162,6 +165,7 @@
|
||||
"show_image": "Show image",
|
||||
"show_gif": "Show GIF",
|
||||
"show_video_player": "Show video player",
|
||||
"share_link_in_post": "Share Link in Post",
|
||||
"tap_then_hold_to_show_menu": "Tap then hold to show menu"
|
||||
},
|
||||
"tag": {
|
||||
|
@ -23,6 +23,7 @@
|
||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
||||
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; };
|
||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||
27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */; };
|
||||
2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */; };
|
||||
2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; };
|
||||
2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; };
|
||||
@ -96,6 +97,8 @@
|
||||
62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D02893707600B205C5 /* BookmarkViewController.swift */; };
|
||||
62FD27D32893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D22893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift */; };
|
||||
62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D42893708A00B205C5 /* BookmarkViewModel+Diffable.swift */; };
|
||||
85904C02293BC0EB0011C817 /* ImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C01293BC0EB0011C817 /* ImageProvider.swift */; };
|
||||
85904C04293BC1940011C817 /* URLActivityItemWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */; };
|
||||
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
||||
C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; };
|
||||
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
|
||||
@ -528,6 +531,7 @@
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
||||
164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = "<group>"; };
|
||||
1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
||||
27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+URL.swift"; sourceTree = "<group>"; };
|
||||
2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = "<group>"; };
|
||||
2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = "<group>"; };
|
||||
2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = "<group>"; };
|
||||
@ -616,6 +620,8 @@
|
||||
7CB58D292DA7ACEF179A9050 /* Pods-Mastodon.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.profile.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
7CEFFAE9AF9284B13C0A758D /* Pods-MastodonTests.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk - debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
||||
819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk.xcconfig"; sourceTree = "<group>"; };
|
||||
85904C01293BC0EB0011C817 /* ImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProvider.swift; sourceTree = "<group>"; };
|
||||
85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLActivityItemWithMetadata.swift; sourceTree = "<group>"; };
|
||||
8850E70A1D5FF51432E43653 /* Pods-Mastodon-MastodonUITests.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; sourceTree = "<group>"; };
|
||||
8E79CCBE51FBC3F7FE8CF49F /* Pods-MastodonTests.release snapshot.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release snapshot.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release snapshot.xcconfig"; sourceTree = "<group>"; };
|
||||
8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
@ -2118,6 +2124,7 @@
|
||||
DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */,
|
||||
DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */,
|
||||
DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */,
|
||||
27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */,
|
||||
DB0FCB79279576A2006C02E2 /* DataSourceFacade+Thread.swift */,
|
||||
DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */,
|
||||
DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */,
|
||||
@ -2549,6 +2556,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */,
|
||||
85904C01293BC0EB0011C817 /* ImageProvider.swift */,
|
||||
85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */,
|
||||
);
|
||||
path = Helper;
|
||||
sourceTree = "<group>";
|
||||
@ -3174,6 +3183,7 @@
|
||||
DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */,
|
||||
DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */,
|
||||
DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */,
|
||||
27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */,
|
||||
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
||||
DB5B54AE2833C15F00DEF8B2 /* UserListViewModel+Diffable.swift in Sources */,
|
||||
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */,
|
||||
@ -3200,6 +3210,7 @@
|
||||
62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */,
|
||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
|
||||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
||||
85904C04293BC1940011C817 /* URLActivityItemWithMetadata.swift in Sources */,
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
|
||||
DBEFCD80282A2AA900C0ABEA /* ReportServerRulesViewModel.swift in Sources */,
|
||||
@ -3340,6 +3351,7 @@
|
||||
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
||||
DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */,
|
||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
||||
85904C02293BC0EB0011C817 /* ImageProvider.swift in Sources */,
|
||||
DBDFF1932805554900557A48 /* DiscoveryPostsViewModel.swift in Sources */,
|
||||
DB3E6FE72806A7A200B035AE /* DiscoveryItem.swift in Sources */,
|
||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||
|
39
Mastodon/Helper/ImageProvider.swift
Normal file
39
Mastodon/Helper/ImageProvider.swift
Normal file
@ -0,0 +1,39 @@
|
||||
//
|
||||
// ImageProvider.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Jed Fox on 2022-12-03.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AlamofireImage
|
||||
import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
|
||||
class ImageProvider: NSObject, NSItemProviderWriting {
|
||||
let url: URL
|
||||
let filter: ImageFilter?
|
||||
|
||||
init(url: URL, filter: ImageFilter? = nil) {
|
||||
self.url = url
|
||||
self.filter = filter
|
||||
}
|
||||
|
||||
var itemProvider: NSItemProvider {
|
||||
NSItemProvider(object: self)
|
||||
}
|
||||
|
||||
static var writableTypeIdentifiersForItemProvider: [String] {
|
||||
[UTType.png.identifier]
|
||||
}
|
||||
|
||||
func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping @Sendable (Data?, Error?) -> Void) -> Progress? {
|
||||
let receipt = UIImageView.af.sharedImageDownloader.download(URLRequest(url: url), filter: filter, completion: { response in
|
||||
switch response.result {
|
||||
case .failure(let error): completionHandler(nil, error)
|
||||
case .success(let image): completionHandler(image.pngData(), nil)
|
||||
}
|
||||
})
|
||||
return receipt?.request.downloadProgress
|
||||
}
|
||||
}
|
33
Mastodon/Helper/URLActivityItemWithMetadata.swift
Normal file
33
Mastodon/Helper/URLActivityItemWithMetadata.swift
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// URLActivityItemWithMetadata.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Jed Fox on 2022-12-03.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import LinkPresentation
|
||||
|
||||
class URLActivityItemWithMetadata: NSObject, UIActivityItemSource {
|
||||
init(url: URL, configureMetadata: (LPLinkMetadata) -> Void) {
|
||||
self.url = url
|
||||
self.metadata = LPLinkMetadata()
|
||||
metadata.url = url
|
||||
configureMetadata(metadata)
|
||||
}
|
||||
|
||||
let url: URL
|
||||
let metadata: LPLinkMetadata
|
||||
|
||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||
url
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||
url
|
||||
}
|
||||
|
||||
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
|
||||
metadata
|
||||
}
|
||||
}
|
@ -48,18 +48,12 @@ extension DataSourceFacade {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let domain = provider.authContext.mastodonAuthenticationBox.domain
|
||||
if url.host == domain,
|
||||
url.pathComponents.count >= 4,
|
||||
url.pathComponents[0] == "/",
|
||||
url.pathComponents[1] == "web",
|
||||
url.pathComponents[2] == "statuses" {
|
||||
let statusID = url.pathComponents[3]
|
||||
let threadViewModel = RemoteThreadViewModel(context: provider.context, authContext: provider.authContext, statusID: statusID)
|
||||
_ = await provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
|
||||
} else {
|
||||
_ = await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
await responseToURLAction(
|
||||
provider: provider,
|
||||
status: status,
|
||||
url: url
|
||||
)
|
||||
case .hashtag(_, let hashtag, _):
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag)
|
||||
_ = await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
|
||||
|
@ -59,8 +59,18 @@ extension DataSourceFacade {
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async throws -> UIActivityViewController {
|
||||
var activityItems: [Any] = try await dependency.context.managedObjectContext.perform {
|
||||
guard let status = status.object(in: dependency.context.managedObjectContext) else { return [] }
|
||||
return [StatusActivityItem(status: status)].compactMap { $0 } as [Any]
|
||||
guard let status = status.object(in: dependency.context.managedObjectContext),
|
||||
let url = URL(string: status.url ?? status.uri)
|
||||
else { return [] }
|
||||
return [
|
||||
URLActivityItemWithMetadata(url: url) { metadata in
|
||||
metadata.title = "\(status.author.displayName) (@\(status.author.acctWithDomain))"
|
||||
metadata.iconProvider = ImageProvider(
|
||||
url: status.author.avatarImageURLWithFallback(domain: status.author.domain),
|
||||
filter: ScaledToSizeFilter(size: CGSize.authorAvatarButtonSize)
|
||||
).itemProvider
|
||||
}
|
||||
] as [Any]
|
||||
}
|
||||
var applicationActivities: [UIActivity] = [
|
||||
SafariActivity(sceneCoordinator: dependency.coordinator), // open URL
|
||||
@ -77,54 +87,6 @@ extension DataSourceFacade {
|
||||
)
|
||||
return activityViewController
|
||||
}
|
||||
|
||||
private class StatusActivityItem: NSObject, UIActivityItemSource {
|
||||
init?(status: Status) {
|
||||
guard let url = URL(string: status.url ?? status.uri) else { return nil }
|
||||
self.url = url
|
||||
self.metadata = LPLinkMetadata()
|
||||
metadata.url = url
|
||||
metadata.title = "\(status.author.displayName) (@\(status.author.acctWithDomain))"
|
||||
metadata.iconProvider = NSItemProvider(object: IconProvider(url: status.author.avatarImageURLWithFallback(domain: status.author.domain)))
|
||||
}
|
||||
|
||||
let url: URL
|
||||
let metadata: LPLinkMetadata
|
||||
|
||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||
url
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||
url
|
||||
}
|
||||
|
||||
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
|
||||
metadata
|
||||
}
|
||||
|
||||
private class IconProvider: NSObject, NSItemProviderWriting {
|
||||
let url: URL
|
||||
init(url: URL) {
|
||||
self.url = url
|
||||
}
|
||||
|
||||
static var writableTypeIdentifiersForItemProvider: [String] {
|
||||
[UTType.png.identifier]
|
||||
}
|
||||
|
||||
func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping @Sendable (Data?, Error?) -> Void) -> Progress? {
|
||||
let filter = ScaledToSizeFilter(size: CGSize.authorAvatarButtonSize)
|
||||
let receipt = UIImageView.af.sharedImageDownloader.download(URLRequest(url: url), filter: filter, completion: { response in
|
||||
switch response.result {
|
||||
case .failure(let error): completionHandler(nil, error)
|
||||
case .success(let image): completionHandler(image.pngData(), nil)
|
||||
}
|
||||
})
|
||||
return receipt?.request.downloadProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ActionToolBar
|
||||
@ -155,7 +117,7 @@ extension DataSourceFacade {
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: provider.context,
|
||||
authContext: provider.authContext,
|
||||
kind: .reply(status: status)
|
||||
destination: .reply(parent: status)
|
||||
)
|
||||
_ = provider.coordinator.present(
|
||||
scene: .compose(viewModel: composeViewModel),
|
||||
|
32
Mastodon/Protocol/Provider/DataSourceFacade+URL.swift
Normal file
32
Mastodon/Protocol/Provider/DataSourceFacade+URL.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// DataSourceFacade+URL.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Kyle Bashour on 11/24/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToURLAction(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
url: URL
|
||||
) async {
|
||||
let domain = provider.authContext.mastodonAuthenticationBox.domain
|
||||
if url.host == domain,
|
||||
url.pathComponents.count >= 4,
|
||||
url.pathComponents[0] == "/",
|
||||
url.pathComponents[1] == "web",
|
||||
url.pathComponents[2] == "statuses" {
|
||||
let statusID = url.pathComponents[3]
|
||||
let threadViewModel = RemoteThreadViewModel(context: provider.context, authContext: provider.authContext, statusID: statusID)
|
||||
_ = await provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
|
||||
} else {
|
||||
_ = await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,9 @@ import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
import MastodonAsset
|
||||
import LinkPresentation
|
||||
|
||||
// MARK: - header
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
@ -120,7 +123,127 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - card
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
statusView: StatusView,
|
||||
didTapCardWithURL url: URL
|
||||
) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard case let .status(status) = item else {
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
await DataSourceFacade.responseToURLAction(
|
||||
provider: self,
|
||||
status: status,
|
||||
url: url
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
statusView: StatusView,
|
||||
cardControl: StatusCardControl,
|
||||
didTapURL url: URL
|
||||
) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard case let .status(status) = item else {
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
await DataSourceFacade.responseToURLAction(
|
||||
provider: self,
|
||||
status: status,
|
||||
url: url
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
statusView: StatusView,
|
||||
cardControlMenu statusCardControl: StatusCardControl
|
||||
) -> UIMenu? {
|
||||
guard let card = statusView.viewModel.card,
|
||||
let url = card.url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIMenu(children: [
|
||||
UIAction(
|
||||
title: L10n.Common.Controls.Actions.copy,
|
||||
image: UIImage(systemName: "doc.on.doc")
|
||||
) { _ in
|
||||
UIPasteboard.general.url = url
|
||||
},
|
||||
|
||||
UIAction(
|
||||
title: L10n.Common.Controls.Actions.share,
|
||||
image: Asset.Arrow.squareAndArrowUp.image.withRenderingMode(.alwaysTemplate)
|
||||
) { _ in
|
||||
DispatchQueue.main.async {
|
||||
let activityViewController = UIActivityViewController(
|
||||
activityItems: [
|
||||
URLActivityItemWithMetadata(url: url) { metadata in
|
||||
metadata.title = card.title
|
||||
|
||||
if let image = card.imageURL {
|
||||
metadata.iconProvider = ImageProvider(url: image, filter: nil).itemProvider
|
||||
}
|
||||
}
|
||||
],
|
||||
applicationActivities: []
|
||||
)
|
||||
self.coordinator.present(
|
||||
scene: .activityViewController(
|
||||
activityViewController: activityViewController,
|
||||
sourceView: statusCardControl, barButtonItem: nil
|
||||
),
|
||||
from: self,
|
||||
transition: .activityViewControllerPresent(animated: true)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
UIAction(
|
||||
title: L10n.Common.Controls.Status.Actions.shareLinkInPost,
|
||||
image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate)
|
||||
) { _ in
|
||||
DispatchQueue.main.async {
|
||||
self.coordinator.present(
|
||||
scene: .compose(viewModel: ComposeViewModel(
|
||||
context: self.context,
|
||||
authContext: self.authContext,
|
||||
destination: .topLevel,
|
||||
initialContent: L10n.Common.Controls.Status.linkViaUser(url.absoluteString, "@" + (statusView.viewModel.authorUsername ?? ""))
|
||||
)),
|
||||
from: self,
|
||||
transition: .modal(animated: true)
|
||||
)
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - media
|
||||
|
@ -100,7 +100,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: self.context,
|
||||
authContext: authContext,
|
||||
kind: .reply(status: status)
|
||||
destination: .reply(parent: status)
|
||||
)
|
||||
_ = self.coordinator.present(
|
||||
scene: .compose(viewModel: composeViewModel),
|
||||
|
@ -34,7 +34,8 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
return ComposeContentViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
kind: viewModel.kind
|
||||
destination: viewModel.destination,
|
||||
initialContent: viewModel.initialContent
|
||||
)
|
||||
}()
|
||||
private(set) lazy var composeContentViewController: ComposeContentViewController = {
|
||||
|
@ -29,7 +29,8 @@ final class ComposeViewModel {
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let kind: ComposeContentViewModel.Kind
|
||||
let destination: ComposeContentViewModel.Destination
|
||||
let initialContent: String
|
||||
|
||||
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit
|
||||
|
||||
@ -41,17 +42,19 @@ final class ComposeViewModel {
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
kind: ComposeContentViewModel.Kind
|
||||
destination: ComposeContentViewModel.Destination,
|
||||
initialContent: String = ""
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.kind = kind
|
||||
self.destination = destination
|
||||
self.initialContent = initialContent
|
||||
// end init
|
||||
|
||||
self.title = {
|
||||
switch kind {
|
||||
case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost
|
||||
case .reply: return L10n.Scene.Compose.Title.newReply
|
||||
switch destination {
|
||||
case .topLevel: return L10n.Scene.Compose.Title.newPost
|
||||
case .reply: return L10n.Scene.Compose.Title.newReply
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -206,10 +206,13 @@ extension HashtagTimelineViewController {
|
||||
|
||||
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
let hashtag = "#" + viewModel.hashtag
|
||||
UITextChecker.learnWord(hashtag)
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
kind: .hashtag(hashtag: viewModel.hashtag)
|
||||
destination: .topLevel,
|
||||
initialContent: hashtag
|
||||
)
|
||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
|
@ -550,10 +550,13 @@ extension ProfileViewController {
|
||||
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let mastodonUser = viewModel.user else { return }
|
||||
let mention = "@" + mastodonUser.acct
|
||||
UITextChecker.learnWord(mention)
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
kind: .mention(user: mastodonUser.asRecord)
|
||||
destination: .topLevel,
|
||||
initialContent: mention
|
||||
)
|
||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
|
@ -379,7 +379,7 @@ extension MainTabBarController {
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
kind: .post
|
||||
destination: .topLevel
|
||||
)
|
||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
@ -803,7 +803,7 @@ extension MainTabBarController {
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
kind: .post
|
||||
destination: .topLevel
|
||||
)
|
||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
|
@ -227,7 +227,7 @@ extension SidebarViewController: UICollectionViewDelegate {
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
kind: .post
|
||||
destination: .topLevel
|
||||
)
|
||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||
default:
|
||||
|
@ -67,6 +67,21 @@ extension StatusTableViewCell {
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
statusView.viewModel.$card
|
||||
.removeDuplicates()
|
||||
.dropFirst()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak tableView, weak self] _ in
|
||||
guard let tableView = tableView else { return }
|
||||
guard let _ = self else { return }
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate {
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, contentSensitiveeToggleButtonDidPressed button: UIButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, didTapCardWithURL url: URL)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
@ -36,6 +37,8 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate {
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu?
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void)
|
||||
// sourcery:end
|
||||
}
|
||||
@ -61,6 +64,10 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell {
|
||||
delegate?.tableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, didTapCardWithURL url: URL) {
|
||||
delegate?.tableViewCell(self, statusView: statusView, didTapCardWithURL: url)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) {
|
||||
delegate?.tableViewCell(self, statusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaView: mediaView, didSelectMediaViewAt: index)
|
||||
}
|
||||
@ -97,6 +104,14 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell {
|
||||
delegate?.tableViewCell(self, statusView: statusView, statusMetricView: statusMetricView, favoriteButtonDidPressed: button)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) {
|
||||
delegate?.tableViewCell(self, statusView: statusView, cardControl: cardControl, didTapURL: url)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? {
|
||||
return delegate?.tableViewCell(self, statusView: statusView, cardControlMenu: cardControlMenu)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
|
||||
delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: accessibilityActivate)
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ extension ThreadViewController {
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
kind: .reply(status: threadContext.status)
|
||||
destination: .reply(parent: threadContext.status)
|
||||
)
|
||||
_ = coordinator.present(
|
||||
scene: .compose(viewModel: composeViewModel),
|
||||
|
@ -188,7 +188,7 @@ extension SceneDelegate {
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: AppContext.shared,
|
||||
authContext: authContext,
|
||||
kind: .post
|
||||
destination: .topLevel
|
||||
)
|
||||
_ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene")
|
||||
|
@ -1,3 +1,17 @@
|
||||
<%
|
||||
func methodDeclaration(_ method: SourceryRuntime.Method, newName: String) -> String {
|
||||
var result = newName
|
||||
if method.throws {
|
||||
result = result + " throws"
|
||||
} else if method.rethrows {
|
||||
result = result + " rethrows"
|
||||
}
|
||||
if method.returnTypeName.isVoid {
|
||||
return result
|
||||
}
|
||||
return result + " -> \(method.returnTypeName)"
|
||||
}
|
||||
-%>
|
||||
<% for type in types.implementing["AutoGenerateProtocolDelegate"] {
|
||||
guard let replaceOf = type.annotations["replaceOf"] as? String else { continue }
|
||||
guard let replaceWith = type.annotations["replaceWith"] as? String else { continue }
|
||||
@ -5,7 +19,7 @@
|
||||
guard let aProtocol = types.protocols.first(where: { $0.name == protocolToGenerate }) else { continue } -%>
|
||||
// sourcery:inline:<%= type.name %>.AutoGenerateProtocolDelegate
|
||||
<% for method in aProtocol.methods { -%>
|
||||
<%= method.name.replacingOccurrences(of: replaceOf, with: replaceWith) %>
|
||||
<%= methodDeclaration(method, newName: method.name.replacingOccurrences(of: replaceOf, with: replaceWith)) %>
|
||||
<% } -%>
|
||||
// sourcery:end
|
||||
<% } %>
|
||||
|
@ -6,6 +6,9 @@ func methodDeclaration(_ method: SourceryRuntime.Method) -> String {
|
||||
} else if method.rethrows {
|
||||
result = result + " rethrows"
|
||||
}
|
||||
if method.returnTypeName.isVoid {
|
||||
return result
|
||||
}
|
||||
return result + " -> \(method.returnTypeName)"
|
||||
}
|
||||
-%>
|
||||
@ -42,7 +45,7 @@ func methodCall(
|
||||
guard let aProtocol = types.protocols.first(where: { $0.name == protocolToGenerate }) else { continue } -%>
|
||||
// sourcery:inline:<%= type.name %>.AutoGenerateProtocolRelayDelegate
|
||||
<% for method in aProtocol.methods { -%>
|
||||
func <%= method.name -%> {
|
||||
func <%= methodDeclaration(method) -%> {
|
||||
<%= methodCall(method, replaceOf: replaceOf, replaceWith: replaceWith) %>
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,6 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>CoreData 6.xcdatamodel</string>
|
||||
<string>CoreData 7.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -0,0 +1,278 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21509" systemVersion="21G217" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Application" representedClassName="CoreDataStack.Application" syncable="YES">
|
||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="vapidKey" optional="YES" attributeType="String"/>
|
||||
<attribute name="website" optional="YES" attributeType="String"/>
|
||||
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="application" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="Card" representedClassName="CoreDataStack.Card" syncable="YES">
|
||||
<attribute name="authorName" optional="YES" attributeType="String"/>
|
||||
<attribute name="authorURLRaw" optional="YES" attributeType="String"/>
|
||||
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
||||
<attribute name="desc" attributeType="String"/>
|
||||
<attribute name="embedURLRaw" optional="YES" attributeType="String"/>
|
||||
<attribute name="height" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="html" optional="YES" attributeType="String"/>
|
||||
<attribute name="image" optional="YES" attributeType="String"/>
|
||||
<attribute name="providerName" optional="YES" attributeType="String"/>
|
||||
<attribute name="providerURLRaw" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<attribute name="typeRaw" attributeType="String"/>
|
||||
<attribute name="urlRaw" attributeType="String"/>
|
||||
<attribute name="width" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="card" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="DomainBlock" representedClassName="CoreDataStack.DomainBlock" syncable="YES">
|
||||
<attribute name="blockedDomain" attributeType="String"/>
|
||||
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="userID" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="userID"/>
|
||||
<constraint value="domain"/>
|
||||
<constraint value="blockedDomain"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Emoji" representedClassName="CoreDataStack.Emoji" syncable="YES">
|
||||
<attribute name="category" optional="YES" attributeType="String"/>
|
||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="shortcode" attributeType="String"/>
|
||||
<attribute name="staticURL" attributeType="String"/>
|
||||
<attribute name="url" attributeType="String"/>
|
||||
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<entity name="Feed" representedClassName="CoreDataStack.Feed" syncable="YES">
|
||||
<attribute name="acctRaw" optional="YES" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hasMore" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="isLoadingMore" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="kindRaw" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="notification" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Notification" inverseName="feeds" inverseEntity="Notification"/>
|
||||
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="feeds" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="Instance" representedClassName="CoreDataStack.Instance" syncable="YES">
|
||||
<attribute name="configurationRaw" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="configurationV2Raw" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="version" optional="YES" attributeType="String"/>
|
||||
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
|
||||
</entity>
|
||||
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthentication" syncable="YES">
|
||||
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="appAccessToken" attributeType="String"/>
|
||||
<attribute name="clientID" attributeType="String"/>
|
||||
<attribute name="clientSecret" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userAccessToken" attributeType="String"/>
|
||||
<attribute name="userID" attributeType="String"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="instance" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Instance" inverseName="authentications" inverseEntity="Instance"/>
|
||||
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="MastodonUser" representedClassName="CoreDataStack.MastodonUser" syncable="YES">
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" attributeType="String"/>
|
||||
<attribute name="avatarStatic" optional="YES" attributeType="String"/>
|
||||
<attribute name="bot" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="displayName" attributeType="String"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="emojis" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="fields" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="header" attributeType="String"/>
|
||||
<attribute name="headerStatic" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="note" optional="YES" attributeType="String"/>
|
||||
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="suspended" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="url" optional="YES" attributeType="String"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="blocking" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blockingBy" inverseEntity="MastodonUser"/>
|
||||
<relationship name="blockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blocking" inverseEntity="MastodonUser"/>
|
||||
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="bookmarkedBy" inverseEntity="Status"/>
|
||||
<relationship name="domainBlocking" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlockingBy" inverseEntity="MastodonUser"/>
|
||||
<relationship name="domainBlockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlocking" inverseEntity="MastodonUser"/>
|
||||
<relationship name="endorsed" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsedBy" inverseEntity="MastodonUser"/>
|
||||
<relationship name="endorsedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsed" inverseEntity="MastodonUser"/>
|
||||
<relationship name="favourite" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="favouritedBy" inverseEntity="Status"/>
|
||||
<relationship name="followedTags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="followedBy" inverseEntity="Tag"/>
|
||||
<relationship name="following" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followingBy" inverseEntity="MastodonUser"/>
|
||||
<relationship name="followingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="following" inverseEntity="MastodonUser"/>
|
||||
<relationship name="followRequested" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequestedBy" inverseEntity="MastodonUser"/>
|
||||
<relationship name="followRequestedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequested" inverseEntity="MastodonUser"/>
|
||||
<relationship name="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
|
||||
<relationship name="muted" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="mutedBy" inverseEntity="Status"/>
|
||||
<relationship name="muting" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mutingBy" inverseEntity="MastodonUser"/>
|
||||
<relationship name="mutingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muting" inverseEntity="MastodonUser"/>
|
||||
<relationship name="notifications" toMany="YES" deletionRule="Nullify" destinationEntity="Notification" inverseName="account" inverseEntity="Notification"/>
|
||||
<relationship name="pinnedStatus" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="pinnedBy" inverseEntity="Status"/>
|
||||
<relationship name="privateNotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="to" inverseEntity="PrivateNote"/>
|
||||
<relationship name="privateNotesTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="from" inverseEntity="PrivateNote"/>
|
||||
<relationship name="reblogged" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
|
||||
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="account" inverseEntity="SearchHistory"/>
|
||||
<relationship name="showingReblogs" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogsBy" inverseEntity="MastodonUser"/>
|
||||
<relationship name="showingReblogsBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogs" inverseEntity="MastodonUser"/>
|
||||
<relationship name="statuses" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="author" inverseEntity="Status"/>
|
||||
<relationship name="votePollOptions" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
|
||||
<relationship name="votePolls" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
|
||||
</entity>
|
||||
<entity name="Notification" representedClassName="CoreDataStack.Notification" syncable="YES">
|
||||
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="followRequestState" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="transientFollowRequestState" optional="YES" transient="YES" attributeType="Binary"/>
|
||||
<attribute name="typeRaw" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userID" attributeType="String"/>
|
||||
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="notifications" inverseEntity="MastodonUser"/>
|
||||
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="notification" inverseEntity="Feed"/>
|
||||
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="notifications" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="Poll" representedClassName="CoreDataStack.Poll" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="expired" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="isVoting" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="multiple" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="votersCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="options" toMany="YES" deletionRule="Cascade" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
|
||||
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="poll" inverseEntity="Status"/>
|
||||
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePolls" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="PollOption" representedClassName="CoreDataStack.PollOption" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isSelected" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="votesCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="poll" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
|
||||
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="PrivateNote" representedClassName="CoreDataStack.PrivateNote" syncable="YES">
|
||||
<attribute name="note" optional="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/>
|
||||
<relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="SearchHistory" representedClassName="CoreDataStack.SearchHistory" syncable="YES">
|
||||
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userID" attributeType="String" defaultValueString=""/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="searchHistories" inverseEntity="MastodonUser"/>
|
||||
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="searchHistories" inverseEntity="Tag"/>
|
||||
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="searchHistories" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="Setting" representedClassName="CoreDataStack.Setting" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="preferredStaticAvatar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="preferredStaticEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="preferredTrueBlackDarkMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="preferredUsingDefaultBrowser" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userID" attributeType="String"/>
|
||||
<relationship name="subscriptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Subscription" inverseName="setting" inverseEntity="Subscription"/>
|
||||
</entity>
|
||||
<entity name="Status" representedClassName="CoreDataStack.Status" syncable="YES">
|
||||
<attribute name="attachments" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="content" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="emojis" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
|
||||
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
||||
<attribute name="isSensitiveToggled" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="language" optional="YES" attributeType="String"/>
|
||||
<attribute name="mentions" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="repliesCount" optional="YES" attributeType="Integer 64" usesScalarValueType="NO"/>
|
||||
<attribute name="revealedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="spoilerText" optional="YES" attributeType="String"/>
|
||||
<attribute name="text" optional="YES" attributeType="String"/>
|
||||
<attribute name="translatedContent" optional="YES" transient="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uri" attributeType="String"/>
|
||||
<attribute name="url" optional="YES" attributeType="String"/>
|
||||
<attribute name="visibilityRaw" optional="YES" attributeType="String" elementID="visibility"/>
|
||||
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="status" inverseEntity="Application"/>
|
||||
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="statuses" inverseEntity="MastodonUser"/>
|
||||
<relationship name="bookmarkedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
|
||||
<relationship name="card" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Card" inverseName="status" inverseEntity="Card"/>
|
||||
<relationship name="favouritedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
|
||||
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="status" inverseEntity="Feed"/>
|
||||
<relationship name="mutedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
|
||||
<relationship name="notifications" toMany="YES" deletionRule="Cascade" destinationEntity="Notification" inverseName="status" inverseEntity="Notification"/>
|
||||
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedStatus" inverseEntity="MastodonUser"/>
|
||||
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Poll" inverseName="status" inverseEntity="Poll"/>
|
||||
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogFrom" inverseEntity="Status"/>
|
||||
<relationship name="reblogFrom" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
|
||||
<relationship name="rebloggedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
|
||||
<relationship name="replyFrom" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/>
|
||||
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
|
||||
<relationship name="searchHistories" toMany="YES" deletionRule="Cascade" destinationEntity="SearchHistory" inverseName="status" inverseEntity="SearchHistory"/>
|
||||
</entity>
|
||||
<entity name="Subscription" representedClassName="CoreDataStack.Subscription" syncable="YES">
|
||||
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="policyRaw" attributeType="String"/>
|
||||
<attribute name="serverKey" optional="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userToken" optional="YES" attributeType="String"/>
|
||||
<relationship name="alert" maxCount="1" deletionRule="Cascade" destinationEntity="SubscriptionAlerts" inverseName="subscription" inverseEntity="SubscriptionAlerts"/>
|
||||
<relationship name="setting" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Setting" inverseName="subscriptions" inverseEntity="Setting"/>
|
||||
</entity>
|
||||
<entity name="SubscriptionAlerts" representedClassName="CoreDataStack.SubscriptionAlerts" syncable="YES">
|
||||
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="favouriteRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="followRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="followRequestRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mentionRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="pollRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="reblogRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="subscription" maxCount="1" deletionRule="Nullify" destinationEntity="Subscription" inverseName="alert" inverseEntity="Subscription"/>
|
||||
</entity>
|
||||
<entity name="Tag" representedClassName="CoreDataStack.Tag" syncable="YES">
|
||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="following" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="histories" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="url" attributeType="String"/>
|
||||
<relationship name="followedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followedTags" inverseEntity="MastodonUser"/>
|
||||
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
|
||||
</entity>
|
||||
</model>
|
180
MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift
Normal file
180
MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift
Normal file
@ -0,0 +1,180 @@
|
||||
//
|
||||
// Card.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by Kyle Bashour on 11/23/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public final class Card: NSManagedObject {
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var urlRaw: String
|
||||
public var url: URL? {
|
||||
URL(string: urlRaw)
|
||||
}
|
||||
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var title: String
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var desc: String
|
||||
|
||||
@NSManaged public private(set) var typeRaw: String
|
||||
// sourcery: autoGenerateProperty
|
||||
public var type: MastodonCardType {
|
||||
get { MastodonCardType(rawValue: typeRaw) }
|
||||
set { typeRaw = newValue.rawValue }
|
||||
}
|
||||
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var authorName: String?
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var authorURLRaw: String?
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var providerName: String?
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var providerURLRaw: String?
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var width: Int64
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var height: Int64
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var image: String?
|
||||
public var imageURL: URL? {
|
||||
image.flatMap(URL.init)
|
||||
}
|
||||
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var embedURLRaw: String?
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var blurhash: String?
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var html: String?
|
||||
|
||||
// sourcery: autoGenerateRelationship
|
||||
@NSManaged public private(set) var status: Status
|
||||
}
|
||||
|
||||
extension Card {
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> Card {
|
||||
let object: Card = context.insertObject()
|
||||
|
||||
object.configure(property: property)
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Card: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AutoGenerateProperty
|
||||
extension Card: AutoGenerateProperty {
|
||||
// sourcery:inline:Card.AutoGenerateProperty
|
||||
|
||||
// Generated using Sourcery
|
||||
// DO NOT EDIT
|
||||
public struct Property {
|
||||
public let urlRaw: String
|
||||
public let title: String
|
||||
public let desc: String
|
||||
public let type: MastodonCardType
|
||||
public let authorName: String?
|
||||
public let authorURLRaw: String?
|
||||
public let providerName: String?
|
||||
public let providerURLRaw: String?
|
||||
public let width: Int64
|
||||
public let height: Int64
|
||||
public let image: String?
|
||||
public let embedURLRaw: String?
|
||||
public let blurhash: String?
|
||||
public let html: String?
|
||||
|
||||
public init(
|
||||
urlRaw: String,
|
||||
title: String,
|
||||
desc: String,
|
||||
type: MastodonCardType,
|
||||
authorName: String?,
|
||||
authorURLRaw: String?,
|
||||
providerName: String?,
|
||||
providerURLRaw: String?,
|
||||
width: Int64,
|
||||
height: Int64,
|
||||
image: String?,
|
||||
embedURLRaw: String?,
|
||||
blurhash: String?,
|
||||
html: String?
|
||||
) {
|
||||
self.urlRaw = urlRaw
|
||||
self.title = title
|
||||
self.desc = desc
|
||||
self.type = type
|
||||
self.authorName = authorName
|
||||
self.authorURLRaw = authorURLRaw
|
||||
self.providerName = providerName
|
||||
self.providerURLRaw = providerURLRaw
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.image = image
|
||||
self.embedURLRaw = embedURLRaw
|
||||
self.blurhash = blurhash
|
||||
self.html = html
|
||||
}
|
||||
}
|
||||
|
||||
public func configure(property: Property) {
|
||||
self.urlRaw = property.urlRaw
|
||||
self.title = property.title
|
||||
self.desc = property.desc
|
||||
self.type = property.type
|
||||
self.authorName = property.authorName
|
||||
self.authorURLRaw = property.authorURLRaw
|
||||
self.providerName = property.providerName
|
||||
self.providerURLRaw = property.providerURLRaw
|
||||
self.width = property.width
|
||||
self.height = property.height
|
||||
self.image = property.image
|
||||
self.embedURLRaw = property.embedURLRaw
|
||||
self.blurhash = property.blurhash
|
||||
self.html = property.html
|
||||
}
|
||||
|
||||
public func update(property: Property) {
|
||||
}
|
||||
|
||||
// sourcery:end
|
||||
}
|
||||
|
||||
// MARK: - AutoGenerateRelationship
|
||||
extension Card: AutoGenerateRelationship {
|
||||
// sourcery:inline:Card.AutoGenerateRelationship
|
||||
|
||||
// Generated using Sourcery
|
||||
// DO NOT EDIT
|
||||
public struct Relationship {
|
||||
public let status: Status
|
||||
|
||||
public init(
|
||||
status: Status
|
||||
) {
|
||||
self.status = status
|
||||
}
|
||||
}
|
||||
|
||||
public func configure(relationship: Relationship) {
|
||||
self.status = relationship.status
|
||||
}
|
||||
|
||||
// sourcery:end
|
||||
}
|
@ -84,7 +84,9 @@ public final class Status: NSManagedObject {
|
||||
@NSManaged public private(set) var pinnedBy: MastodonUser?
|
||||
// sourcery: autoGenerateRelationship
|
||||
@NSManaged public private(set) var poll: Poll?
|
||||
|
||||
// sourcery: autoGenerateRelationship
|
||||
@NSManaged public private(set) var card: Card?
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var feeds: Set<Feed>
|
||||
|
||||
@ -382,15 +384,18 @@ extension Status: AutoGenerateRelationship {
|
||||
public let author: MastodonUser
|
||||
public let reblog: Status?
|
||||
public let poll: Poll?
|
||||
public let card: Card?
|
||||
|
||||
public init(
|
||||
author: MastodonUser,
|
||||
reblog: Status?,
|
||||
poll: Poll?
|
||||
poll: Poll?,
|
||||
card: Card?
|
||||
) {
|
||||
self.author = author
|
||||
self.reblog = reblog
|
||||
self.poll = poll
|
||||
self.card = card
|
||||
}
|
||||
}
|
||||
|
||||
@ -398,6 +403,7 @@ extension Status: AutoGenerateRelationship {
|
||||
self.author = relationship.author
|
||||
self.reblog = relationship.reblog
|
||||
self.poll = relationship.poll
|
||||
self.card = relationship.card
|
||||
}
|
||||
// sourcery:end
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
//
|
||||
// MastodonCardType.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by Kyle Bashour on 11/23/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum MastodonCardType: RawRepresentable, Equatable {
|
||||
case link
|
||||
case photo
|
||||
case video
|
||||
|
||||
case _other(String)
|
||||
|
||||
public init(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "link": self = .link
|
||||
case "photo": self = .photo
|
||||
case "video": self = .video
|
||||
default: self = ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .link: return "link"
|
||||
case .photo: return "photo"
|
||||
case .video: return "video"
|
||||
case ._other(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
//
|
||||
// Persistence+Card.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK on 2021-12-9.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import os.log
|
||||
|
||||
extension Persistence.Card {
|
||||
|
||||
public struct PersistContext {
|
||||
public let domain: String
|
||||
public let entity: Mastodon.Entity.Card
|
||||
public let me: MastodonUser?
|
||||
public let log = Logger(subsystem: "Card", category: "Persistence")
|
||||
public init(
|
||||
domain: String,
|
||||
entity: Mastodon.Entity.Card,
|
||||
me: MastodonUser?
|
||||
) {
|
||||
self.domain = domain
|
||||
self.entity = entity
|
||||
self.me = me
|
||||
}
|
||||
}
|
||||
|
||||
public struct PersistResult {
|
||||
public let card: Card
|
||||
public let isNewInsertion: Bool
|
||||
|
||||
public init(
|
||||
card: Card,
|
||||
isNewInsertion: Bool
|
||||
) {
|
||||
self.card = card
|
||||
self.isNewInsertion = isNewInsertion
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
public let logger = Logger(subsystem: "Persistence.MastodonCard.PersistResult", category: "Persist")
|
||||
public func log() {
|
||||
let pollInsertionFlag = isNewInsertion ? "+" : "-"
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(pollInsertionFlag)](\(card.title)):")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public static func create(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
context: PersistContext
|
||||
) -> PersistResult {
|
||||
var type: MastodonCardType {
|
||||
switch context.entity.type {
|
||||
case .link: return .link
|
||||
case .photo: return .photo
|
||||
case .video: return .video
|
||||
case .rich: return ._other(context.entity.type.rawValue)
|
||||
case ._other(let rawValue): return ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
let property = Card.Property(
|
||||
urlRaw: context.entity.url,
|
||||
title: context.entity.title,
|
||||
desc: context.entity.description,
|
||||
type: type,
|
||||
authorName: context.entity.authorName,
|
||||
authorURLRaw: context.entity.authorURL,
|
||||
providerName: context.entity.providerName,
|
||||
providerURLRaw: context.entity.providerURL,
|
||||
width: Int64(context.entity.width ?? 0),
|
||||
height: Int64(context.entity.height ?? 0),
|
||||
image: context.entity.image,
|
||||
embedURLRaw: context.entity.embedURL,
|
||||
blurhash: context.entity.blurhash,
|
||||
html: context.entity.html.flatMap { $0.isEmpty ? nil : $0 }
|
||||
)
|
||||
|
||||
let card = Card.insert(
|
||||
into: managedObjectContext,
|
||||
property: property
|
||||
)
|
||||
|
||||
return PersistResult(
|
||||
card: card,
|
||||
isNewInsertion: true
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -87,7 +87,7 @@ extension Persistence.Status {
|
||||
}
|
||||
|
||||
if let oldStatus = fetch(in: managedObjectContext, context: context) {
|
||||
merge(mastodonStatus: oldStatus, context: context)
|
||||
merge(in: managedObjectContext, mastodonStatus: oldStatus, context: context)
|
||||
return PersistResult(
|
||||
status: oldStatus,
|
||||
isNewInsertion: false,
|
||||
@ -107,7 +107,9 @@ extension Persistence.Status {
|
||||
)
|
||||
return result.poll
|
||||
}()
|
||||
|
||||
|
||||
let card = createCard(in: managedObjectContext, context: context)
|
||||
|
||||
let authorResult = Persistence.MastodonUser.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.MastodonUser.PersistContext(
|
||||
@ -122,7 +124,8 @@ extension Persistence.Status {
|
||||
let relationship = Status.Relationship(
|
||||
author: author,
|
||||
reblog: reblog,
|
||||
poll: poll
|
||||
poll: poll,
|
||||
card: card
|
||||
)
|
||||
let status = create(
|
||||
in: managedObjectContext,
|
||||
@ -182,6 +185,7 @@ extension Persistence.Status {
|
||||
}
|
||||
|
||||
public static func merge(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
mastodonStatus status: Status,
|
||||
context: PersistContext
|
||||
) {
|
||||
@ -203,8 +207,31 @@ extension Persistence.Status {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if status.card == nil, context.entity.card != nil {
|
||||
let card = createCard(in: managedObjectContext, context: context)
|
||||
let relationship = Card.Relationship(status: status)
|
||||
card?.configure(relationship: relationship)
|
||||
}
|
||||
|
||||
update(status: status, context: context)
|
||||
}
|
||||
|
||||
private static func createCard(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
context: PersistContext
|
||||
) -> Card? {
|
||||
guard let entity = context.entity.card else { return nil }
|
||||
let result = Persistence.Card.create(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.Card.PersistContext(
|
||||
domain: context.domain,
|
||||
entity: entity,
|
||||
me: context.me
|
||||
)
|
||||
)
|
||||
return result.card
|
||||
}
|
||||
|
||||
private static func update(
|
||||
status: Status,
|
||||
|
@ -15,6 +15,7 @@ extension Persistence {
|
||||
public enum MastodonUser { }
|
||||
public enum Status { }
|
||||
public enum Poll { }
|
||||
public enum Card { }
|
||||
public enum PollOption { }
|
||||
public enum Tag { }
|
||||
public enum SearchHistory { }
|
||||
|
@ -17,4 +17,16 @@ extension NSLayoutConstraint {
|
||||
self.identifier = identifier
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func activate() -> Self {
|
||||
self.isActive = true
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func deactivate() -> Self {
|
||||
self.isActive = false
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
@ -48,18 +48,22 @@ extension UIView {
|
||||
}
|
||||
|
||||
public extension UIView {
|
||||
|
||||
func pinToParent() {
|
||||
|
||||
@discardableResult
|
||||
func pinToParent() -> [NSLayoutConstraint] {
|
||||
pinTo(to: self.superview)
|
||||
}
|
||||
|
||||
func pinTo(to view: UIView?) {
|
||||
guard let pinToView = view else { return }
|
||||
NSLayoutConstraint.activate([
|
||||
|
||||
@discardableResult
|
||||
func pinTo(to view: UIView?) -> [NSLayoutConstraint] {
|
||||
guard let pinToView = view else { return [] }
|
||||
let constraints = [
|
||||
topAnchor.constraint(equalTo: pinToView.topAnchor),
|
||||
leadingAnchor.constraint(equalTo: pinToView.leadingAnchor),
|
||||
trailingAnchor.constraint(equalTo: pinToView.trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: pinToView.bottomAnchor),
|
||||
])
|
||||
]
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
return constraints
|
||||
}
|
||||
}
|
||||
|
@ -120,6 +120,8 @@ public enum L10n {
|
||||
public static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm", fallback: "Confirm")
|
||||
/// Continue
|
||||
public static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue", fallback: "Continue")
|
||||
/// Copy
|
||||
public static let copy = L10n.tr("Localizable", "Common.Controls.Actions.Copy", fallback: "Copy")
|
||||
/// Copy Photo
|
||||
public static let copyPhoto = L10n.tr("Localizable", "Common.Controls.Actions.CopyPhoto", fallback: "Copy Photo")
|
||||
/// Delete
|
||||
@ -288,6 +290,12 @@ public enum L10n {
|
||||
public enum Status {
|
||||
/// Content Warning
|
||||
public static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning", fallback: "Content Warning")
|
||||
/// %@ via %@
|
||||
public static func linkViaUser(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.LinkViaUser", String(describing: p1), String(describing: p2), fallback: "%@ via %@")
|
||||
}
|
||||
/// Load Embed
|
||||
public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed")
|
||||
/// Tap anywhere to reveal
|
||||
public static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning", fallback: "Tap anywhere to reveal")
|
||||
/// Sensitive Content
|
||||
@ -317,6 +325,8 @@ public enum L10n {
|
||||
public static let reblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reblog", fallback: "Reblog")
|
||||
/// Reply
|
||||
public static let reply = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reply", fallback: "Reply")
|
||||
/// Share Link in Post
|
||||
public static let shareLinkInPost = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareLinkInPost", fallback: "Share Link in Post")
|
||||
/// Show GIF
|
||||
public static let showGif = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowGif", fallback: "Show GIF")
|
||||
/// Show image
|
||||
|
@ -34,6 +34,7 @@ Please check your internet connection.";
|
||||
"Common.Controls.Actions.Compose" = "Compose";
|
||||
"Common.Controls.Actions.Confirm" = "Confirm";
|
||||
"Common.Controls.Actions.Continue" = "Continue";
|
||||
"Common.Controls.Actions.Copy" = "Copy";
|
||||
"Common.Controls.Actions.CopyPhoto" = "Copy Photo";
|
||||
"Common.Controls.Actions.Delete" = "Delete";
|
||||
"Common.Controls.Actions.Discard" = "Discard";
|
||||
@ -105,6 +106,7 @@ Please check your internet connection.";
|
||||
"Common.Controls.Status.Actions.Menu" = "Menu";
|
||||
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
||||
"Common.Controls.Status.Actions.Reply" = "Reply";
|
||||
"Common.Controls.Status.Actions.ShareLinkInPost" = "Share Link in Post";
|
||||
"Common.Controls.Status.Actions.ShowGif" = "Show GIF";
|
||||
"Common.Controls.Status.Actions.ShowImage" = "Show image";
|
||||
"Common.Controls.Status.Actions.ShowVideoPlayer" = "Show video player";
|
||||
@ -112,6 +114,8 @@ Please check your internet connection.";
|
||||
"Common.Controls.Status.Actions.Unfavorite" = "Unfavorite";
|
||||
"Common.Controls.Status.Actions.Unreblog" = "Undo reblog";
|
||||
"Common.Controls.Status.ContentWarning" = "Content Warning";
|
||||
"Common.Controls.Status.LinkViaUser" = "%@ via %@";
|
||||
"Common.Controls.Status.LoadEmbed" = "Load Embed";
|
||||
"Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal";
|
||||
"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
|
||||
"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
|
||||
|
18
MastodonSDK/Sources/MastodonUI/Extension/UIScreen.swift
Normal file
18
MastodonSDK/Sources/MastodonUI/Extension/UIScreen.swift
Normal file
@ -0,0 +1,18 @@
|
||||
//
|
||||
// UIScreen.swift
|
||||
//
|
||||
//
|
||||
// Created by Jed Fox on 2022-12-15.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIScreen {
|
||||
public var pixelSize: CGFloat {
|
||||
if scale > 0 {
|
||||
return 1 / scale
|
||||
}
|
||||
// should never happen but just in case
|
||||
return 1
|
||||
}
|
||||
}
|
@ -47,10 +47,7 @@ extension ComposeContentViewModel {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
switch kind {
|
||||
case .post:
|
||||
break
|
||||
case .reply(let status):
|
||||
if case .reply(let status) = destination {
|
||||
let cell = composeReplyToTableViewCell
|
||||
// bind frame publisher
|
||||
cell.$framePublisher
|
||||
@ -66,10 +63,6 @@ extension ComposeContentViewModel {
|
||||
guard let replyTo = status.object(in: context.managedObjectContext) else { return }
|
||||
cell.statusView.configure(status: replyTo)
|
||||
}
|
||||
case .hashtag:
|
||||
break
|
||||
case .mention:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -83,7 +76,7 @@ extension ComposeContentViewModel: UITableViewDataSource {
|
||||
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch Section.allCases[section] {
|
||||
case .replyTo:
|
||||
switch kind {
|
||||
switch destination {
|
||||
case .reply: return 1
|
||||
default: return 0
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let kind: Kind
|
||||
let destination: Destination
|
||||
weak var delegate: ComposeContentViewModelDelegate?
|
||||
|
||||
@Published var viewLayoutFrame = ViewLayoutFrame()
|
||||
@ -59,8 +59,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||
customEmojiPickerInputViewModel.configure(textInput: textView)
|
||||
}
|
||||
}
|
||||
// for hashtag: "#<hashtag> "
|
||||
// for mention: "@<mention> "
|
||||
// allow dismissing the compose view without confirmation if content == intialContent
|
||||
@Published public var initialContent = ""
|
||||
@Published public var content = ""
|
||||
@Published public var contentWeightedLength = 0
|
||||
@ -138,11 +137,12 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||
public init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
kind: Kind
|
||||
destination: Destination,
|
||||
initialContent: String
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.kind = kind
|
||||
self.destination = destination
|
||||
self.visibility = {
|
||||
// default private when user locked
|
||||
var visibility: Mastodon.Entity.Status.Visibility = {
|
||||
@ -152,8 +152,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||
return author.locked ? .private : .public
|
||||
}()
|
||||
// set visibility for reply post
|
||||
switch kind {
|
||||
case .reply(let record):
|
||||
if case .reply(let record) = destination {
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else {
|
||||
assertionFailure()
|
||||
@ -173,8 +172,6 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return visibility
|
||||
}()
|
||||
@ -185,7 +182,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||
// end init
|
||||
|
||||
// setup initial value
|
||||
switch kind {
|
||||
let initialContentWithSpace = initialContent.isEmpty ? "" : initialContent + " "
|
||||
switch destination {
|
||||
case .reply(let record):
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else {
|
||||
@ -214,29 +212,15 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
let initialComposeContent = mentionAccts.joined(separator: " ")
|
||||
let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
|
||||
self.initialContent = preInsertedContent ?? ""
|
||||
self.content = preInsertedContent ?? ""
|
||||
let preInsertedContent = initialComposeContent.isEmpty ? "" : initialComposeContent + " "
|
||||
self.initialContent = preInsertedContent + initialContentWithSpace
|
||||
self.content = preInsertedContent + initialContentWithSpace
|
||||
}
|
||||
case .hashtag(let hashtag):
|
||||
let initialComposeContent = "#" + hashtag
|
||||
UITextChecker.learnWord(initialComposeContent)
|
||||
let preInsertedContent = initialComposeContent + " "
|
||||
self.initialContent = preInsertedContent
|
||||
self.content = preInsertedContent
|
||||
case .mention(let record):
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
let initialComposeContent = "@" + user.acct
|
||||
UITextChecker.learnWord(initialComposeContent)
|
||||
let preInsertedContent = initialComposeContent + " "
|
||||
self.initialContent = preInsertedContent
|
||||
self.content = preInsertedContent
|
||||
}
|
||||
case .post:
|
||||
break
|
||||
case .topLevel:
|
||||
self.initialContent = initialContentWithSpace
|
||||
self.content = initialContentWithSpace
|
||||
}
|
||||
|
||||
|
||||
// set limit
|
||||
let _configuration: Mastodon.Entity.Instance.Configuration? = {
|
||||
var configuration: Mastodon.Entity.Instance.Configuration? = nil
|
||||
@ -443,11 +427,9 @@ extension ComposeContentViewModel {
|
||||
}
|
||||
|
||||
extension ComposeContentViewModel {
|
||||
public enum Kind {
|
||||
case post
|
||||
case hashtag(hashtag: String)
|
||||
case mention(user: ManagedObjectRecord<MastodonUser>)
|
||||
case reply(status: ManagedObjectRecord<Status>)
|
||||
public enum Destination {
|
||||
case topLevel
|
||||
case reply(parent: ManagedObjectRecord<Status>)
|
||||
}
|
||||
|
||||
public enum ScrollViewState {
|
||||
@ -530,10 +512,10 @@ extension ComposeContentViewModel {
|
||||
return MastodonStatusPublisher(
|
||||
author: author,
|
||||
replyTo: {
|
||||
switch self.kind {
|
||||
case .reply(let status): return status
|
||||
default: return nil
|
||||
if case .reply(let status) = destination {
|
||||
return status
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
isContentWarningComposing: isContentWarningActive,
|
||||
contentWarning: contentWarning,
|
||||
|
@ -496,7 +496,10 @@ extension NotificationView {
|
||||
|
||||
// MARK: - StatusViewDelegate
|
||||
extension NotificationView: StatusViewDelegate {
|
||||
|
||||
public func statusView(_ statusView: StatusView, didTapCardWithURL url: URL) {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, headerDidPressed header: UIView) {
|
||||
// do nothing
|
||||
}
|
||||
@ -599,6 +602,15 @@ extension NotificationView: StatusViewDelegate {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MastodonMenuDelegate
|
||||
|
@ -0,0 +1,338 @@
|
||||
//
|
||||
// OpenGraphView.swift
|
||||
//
|
||||
//
|
||||
// Created by Kyle Bashour on 11/11/22.
|
||||
//
|
||||
|
||||
import AlamofireImage
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import CoreDataStack
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
public protocol StatusCardControlDelegate: AnyObject {
|
||||
func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL)
|
||||
func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> UIMenu?
|
||||
}
|
||||
|
||||
public final class StatusCardControl: UIControl {
|
||||
public weak var delegate: StatusCardControlDelegate?
|
||||
|
||||
private var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
private let containerStackView = UIStackView()
|
||||
private let labelStackView = UIStackView()
|
||||
|
||||
private let highlightView = UIView()
|
||||
private let dividerView = UIView()
|
||||
private let imageView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let linkLabel = UILabel()
|
||||
private lazy var showEmbedButton: UIButton = {
|
||||
if #available(iOS 15.0, *) {
|
||||
var configuration = UIButton.Configuration.gray()
|
||||
configuration.background.visualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
||||
configuration.baseBackgroundColor = .clear
|
||||
configuration.cornerStyle = .capsule
|
||||
configuration.buttonSize = .large
|
||||
configuration.title = L10n.Common.Controls.Status.loadEmbed
|
||||
configuration.image = UIImage(systemName: "play.fill")
|
||||
configuration.imagePadding = 12
|
||||
return UIButton(configuration: configuration, primaryAction: UIAction { [weak self] _ in
|
||||
self?.showWebView()
|
||||
})
|
||||
}
|
||||
|
||||
return UIButton(type: .system, primaryAction: UIAction { [weak self] _ in
|
||||
self?.showWebView()
|
||||
})
|
||||
}()
|
||||
private var html = ""
|
||||
|
||||
private static let cardContentPool = WKProcessPool()
|
||||
private var webView: WKWebView?
|
||||
|
||||
private var layout: Layout?
|
||||
private var layoutConstraints: [NSLayoutConstraint] = []
|
||||
private var dividerConstraint: NSLayoutConstraint?
|
||||
|
||||
public override var isHighlighted: Bool {
|
||||
didSet {
|
||||
// override UIKit behavior of highlighting subviews when cell is highlighted
|
||||
if isHighlighted,
|
||||
let cell = sequence(first: self, next: \.superview).first(where: { $0 is UITableViewCell }) as? UITableViewCell {
|
||||
highlightView.isHidden = cell.isHighlighted
|
||||
} else {
|
||||
highlightView.isHidden = !isHighlighted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
apply(theme: ThemeService.shared.currentTheme.value)
|
||||
|
||||
ThemeService.shared.currentTheme.sink { [weak self] theme in
|
||||
self?.apply(theme: theme)
|
||||
}.store(in: &disposeBag)
|
||||
|
||||
clipsToBounds = true
|
||||
layer.cornerCurve = .continuous
|
||||
layer.cornerRadius = 10
|
||||
|
||||
if #available(iOS 15, *) {
|
||||
maximumContentSizeCategory = .accessibilityLarge
|
||||
}
|
||||
|
||||
highlightView.backgroundColor = UIColor.label.withAlphaComponent(0.1)
|
||||
highlightView.isHidden = true
|
||||
|
||||
titleLabel.numberOfLines = 2
|
||||
titleLabel.textColor = Asset.Colors.Label.primary.color
|
||||
titleLabel.font = .preferredFont(forTextStyle: .body)
|
||||
|
||||
linkLabel.numberOfLines = 1
|
||||
linkLabel.textColor = Asset.Colors.Label.secondary.color
|
||||
linkLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.clipsToBounds = true
|
||||
imageView.setContentHuggingPriority(.zero, for: .horizontal)
|
||||
imageView.setContentHuggingPriority(.zero, for: .vertical)
|
||||
imageView.setContentCompressionResistancePriority(.zero, for: .horizontal)
|
||||
imageView.setContentCompressionResistancePriority(.zero, for: .vertical)
|
||||
|
||||
labelStackView.addArrangedSubview(titleLabel)
|
||||
labelStackView.addArrangedSubview(linkLabel)
|
||||
labelStackView.layoutMargins = .init(top: 10, left: 10, bottom: 10, right: 10)
|
||||
labelStackView.isLayoutMarginsRelativeArrangement = true
|
||||
labelStackView.axis = .vertical
|
||||
labelStackView.spacing = 2
|
||||
|
||||
containerStackView.addArrangedSubview(imageView)
|
||||
containerStackView.addArrangedSubview(dividerView)
|
||||
containerStackView.addArrangedSubview(labelStackView)
|
||||
containerStackView.isUserInteractionEnabled = false
|
||||
containerStackView.distribution = .fill
|
||||
|
||||
addSubview(containerStackView)
|
||||
addSubview(highlightView)
|
||||
addSubview(showEmbedButton)
|
||||
|
||||
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
highlightView.translatesAutoresizingMaskIntoConstraints = false
|
||||
showEmbedButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
dividerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
containerStackView.pinToParent()
|
||||
highlightView.pinToParent()
|
||||
NSLayoutConstraint.activate([
|
||||
showEmbedButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
|
||||
showEmbedButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
|
||||
])
|
||||
|
||||
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func configure(card: Card) {
|
||||
if let host = card.url?.host {
|
||||
accessibilityLabel = "\(card.title) \(host)"
|
||||
} else {
|
||||
accessibilityLabel = card.title
|
||||
}
|
||||
|
||||
titleLabel.text = card.title
|
||||
linkLabel.text = card.url?.host
|
||||
imageView.contentMode = .center
|
||||
|
||||
imageView.sd_setImage(
|
||||
with: card.imageURL,
|
||||
placeholderImage: icon(for: card.layout)
|
||||
) { [weak self] image, _, _, _ in
|
||||
if image != nil {
|
||||
self?.imageView.contentMode = .scaleAspectFill
|
||||
}
|
||||
|
||||
self?.containerStackView.setNeedsLayout()
|
||||
self?.containerStackView.layoutIfNeeded()
|
||||
}
|
||||
|
||||
if let html = card.html, !html.isEmpty {
|
||||
showEmbedButton.isHidden = false
|
||||
self.html = html
|
||||
} else {
|
||||
webView?.removeFromSuperview()
|
||||
webView = nil
|
||||
showEmbedButton.isHidden = true
|
||||
self.html = ""
|
||||
}
|
||||
|
||||
updateConstraints(for: card.layout)
|
||||
}
|
||||
|
||||
public override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
|
||||
if let window = window {
|
||||
layer.borderWidth = window.screen.pixelSize
|
||||
dividerConstraint?.constant = window.screen.pixelSize
|
||||
}
|
||||
}
|
||||
|
||||
private func updateConstraints(for layout: Layout) {
|
||||
guard layout != self.layout else { return }
|
||||
self.layout = layout
|
||||
|
||||
NSLayoutConstraint.deactivate(layoutConstraints)
|
||||
dividerConstraint?.deactivate()
|
||||
|
||||
let pixelSize = (window?.screen.pixelSize ?? 1)
|
||||
switch layout {
|
||||
case .large(let aspectRatio):
|
||||
containerStackView.alignment = .fill
|
||||
containerStackView.axis = .vertical
|
||||
layoutConstraints = [
|
||||
imageView.widthAnchor.constraint(
|
||||
equalTo: imageView.heightAnchor,
|
||||
multiplier: aspectRatio
|
||||
)
|
||||
// This priority is important or constraints break;
|
||||
// it still renders the card correctly.
|
||||
.priority(.defaultLow - 1),
|
||||
// set a reasonable max height for very tall images
|
||||
imageView.heightAnchor
|
||||
.constraint(lessThanOrEqualToConstant: 400),
|
||||
]
|
||||
dividerConstraint = dividerView.heightAnchor.constraint(equalToConstant: pixelSize).activate()
|
||||
case .compact:
|
||||
containerStackView.alignment = .center
|
||||
containerStackView.axis = .horizontal
|
||||
layoutConstraints = [
|
||||
imageView.heightAnchor.constraint(equalTo: heightAnchor),
|
||||
imageView.widthAnchor.constraint(equalToConstant: 85),
|
||||
heightAnchor.constraint(equalToConstant: 85).priority(.defaultLow - 1),
|
||||
heightAnchor.constraint(greaterThanOrEqualToConstant: 85),
|
||||
dividerView.heightAnchor.constraint(equalTo: containerStackView.heightAnchor),
|
||||
]
|
||||
dividerConstraint = dividerView.widthAnchor.constraint(equalToConstant: pixelSize).activate()
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate(layoutConstraints)
|
||||
}
|
||||
|
||||
private func icon(for layout: Layout) -> UIImage? {
|
||||
switch layout {
|
||||
case .compact:
|
||||
return UIImage(systemName: "newspaper.fill")
|
||||
case .large:
|
||||
let configuration = UIImage.SymbolConfiguration(pointSize: 32)
|
||||
return UIImage(systemName: "photo", withConfiguration: configuration)
|
||||
}
|
||||
}
|
||||
|
||||
private func apply(theme: Theme) {
|
||||
layer.borderColor = theme.separator.cgColor
|
||||
dividerView.backgroundColor = theme.separator
|
||||
imageView.backgroundColor = UIColor.tertiarySystemFill
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: WKWebView delegates
|
||||
extension StatusCardControl: WKNavigationDelegate, WKUIDelegate {
|
||||
fileprivate func showWebView() {
|
||||
let webView = setupWebView()
|
||||
webView.loadHTMLString("<meta name='viewport' content='width=device-width,user-scalable=no'><style>body { margin: 0; color-scheme: light dark; } body > :only-child { width: 100vw !important; height: 100vh !important }</style>" + html, baseURL: nil)
|
||||
if webView.superview == nil {
|
||||
addSubview(webView)
|
||||
webView.pinTo(to: imageView)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupWebView() -> WKWebView {
|
||||
if let webView { return webView }
|
||||
|
||||
let config = WKWebViewConfiguration()
|
||||
config.processPool = Self.cardContentPool
|
||||
config.websiteDataStore = .nonPersistent() // private/incognito mode
|
||||
config.suppressesIncrementalRendering = true
|
||||
config.allowsInlineMediaPlayback = true
|
||||
let webView = WKWebView(frame: .zero, configuration: config)
|
||||
webView.uiDelegate = self
|
||||
webView.navigationDelegate = self
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
webView.isOpaque = false
|
||||
webView.backgroundColor = .clear
|
||||
self.webView = webView
|
||||
return webView
|
||||
}
|
||||
|
||||
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
|
||||
let isTopLevelNavigation: Bool
|
||||
if let frame = navigationAction.targetFrame {
|
||||
isTopLevelNavigation = frame.isMainFrame
|
||||
} else {
|
||||
isTopLevelNavigation = true
|
||||
}
|
||||
|
||||
if isTopLevelNavigation,
|
||||
// ignore form submits and such
|
||||
navigationAction.navigationType == .linkActivated || navigationAction.navigationType == .other,
|
||||
let url = navigationAction.request.url,
|
||||
url.absoluteString != "about:blank" {
|
||||
delegate?.statusCardControl(self, didTapURL: url)
|
||||
return .cancel
|
||||
}
|
||||
return .allow
|
||||
}
|
||||
|
||||
public func webViewDidClose(_ webView: WKWebView) {
|
||||
webView.removeFromSuperview()
|
||||
self.webView = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIContextMenuInteractionDelegate
|
||||
extension StatusCardControl {
|
||||
public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { elements in
|
||||
self.delegate?.statusCardControlMenu(self)
|
||||
}
|
||||
}
|
||||
|
||||
public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
UITargetedPreview(view: self)
|
||||
}
|
||||
}
|
||||
|
||||
private extension StatusCardControl {
|
||||
enum Layout: Equatable {
|
||||
case compact
|
||||
case large(aspectRatio: CGFloat)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Card {
|
||||
var layout: StatusCardControl.Layout {
|
||||
var aspectRatio = CGFloat(width) / CGFloat(height)
|
||||
if !aspectRatio.isFinite {
|
||||
aspectRatio = 1
|
||||
}
|
||||
return (abs(aspectRatio - 1) < 0.05 || image == nil) && html == nil
|
||||
? .compact
|
||||
: .large(aspectRatio: aspectRatio)
|
||||
}
|
||||
}
|
||||
|
||||
private extension UILayoutPriority {
|
||||
static let zero = UILayoutPriority(rawValue: 0)
|
||||
}
|
@ -53,6 +53,7 @@ extension StatusView {
|
||||
configureContent(status: status)
|
||||
configureMedia(status: status)
|
||||
configurePoll(status: status)
|
||||
configureCard(status: status)
|
||||
configureToolbar(status: status)
|
||||
configureFilter(status: status)
|
||||
viewModel.originalStatus = status
|
||||
@ -403,6 +404,17 @@ extension StatusView {
|
||||
.assign(to: \.isVoting, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func configureCard(status: Status) {
|
||||
let status = status.reblog ?? status
|
||||
if viewModel.mediaViewConfigurations.isEmpty {
|
||||
status.publisher(for: \.card)
|
||||
.assign(to: \.card, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
} else {
|
||||
viewModel.card = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func configureToolbar(status: Status) {
|
||||
let status = status.reblog ?? status
|
||||
|
@ -76,7 +76,10 @@ extension StatusView {
|
||||
@Published public var voteCount = 0
|
||||
@Published public var expireAt: Date?
|
||||
@Published public var expired: Bool = false
|
||||
|
||||
|
||||
// Card
|
||||
@Published public var card: Card?
|
||||
|
||||
// Visibility
|
||||
@Published public var visibility: MastodonVisibility = .public
|
||||
|
||||
@ -194,6 +197,7 @@ extension StatusView.ViewModel {
|
||||
bindContent(statusView: statusView)
|
||||
bindMedia(statusView: statusView)
|
||||
bindPoll(statusView: statusView)
|
||||
bindCard(statusView: statusView)
|
||||
bindToolbar(statusView: statusView)
|
||||
bindMetric(statusView: statusView)
|
||||
bindMenu(statusView: statusView)
|
||||
@ -314,12 +318,14 @@ extension StatusView.ViewModel {
|
||||
)
|
||||
statusView.contentMetaText.textView.accessibilityTraits = [.staticText]
|
||||
statusView.contentMetaText.textView.accessibilityElementsHidden = false
|
||||
|
||||
} else {
|
||||
statusView.contentMetaText.reset()
|
||||
statusView.contentMetaText.textView.accessibilityLabel = ""
|
||||
}
|
||||
|
||||
statusView.contentMetaText.textView.alpha = isContentReveal ? 1 : 0 // keep the frame size and only display when revealing
|
||||
statusView.statusCardControl.alpha = isContentReveal ? 1 : 0
|
||||
|
||||
statusView.setSpoilerOverlayViewHidden(isHidden: isContentReveal)
|
||||
|
||||
@ -489,6 +495,15 @@ extension StatusView.ViewModel {
|
||||
.assign(to: \.isEnabled, on: statusView.pollVoteButton)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func bindCard(statusView: StatusView) {
|
||||
$card.sink { card in
|
||||
guard let card = card else { return }
|
||||
statusView.statusCardControl.configure(card: card)
|
||||
statusView.setStatusCardControlDisplay()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func bindToolbar(statusView: StatusView) {
|
||||
$replyCount
|
||||
|
@ -23,6 +23,7 @@ public protocol StatusViewDelegate: AnyObject {
|
||||
func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
func statusView(_ statusView: StatusView, contentSensitiveeToggleButtonDidPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
func statusView(_ statusView: StatusView, didTapCardWithURL url: URL)
|
||||
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int)
|
||||
func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
@ -32,6 +33,8 @@ public protocol StatusViewDelegate: AnyObject {
|
||||
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
|
||||
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu?
|
||||
|
||||
// a11y
|
||||
func statusView(_ statusView: StatusView, accessibilityActivate: Void)
|
||||
@ -113,6 +116,8 @@ public final class StatusView: UIView {
|
||||
]
|
||||
return metaText
|
||||
}()
|
||||
|
||||
public let statusCardControl = StatusCardControl()
|
||||
|
||||
// content warning
|
||||
public let spoilerOverlayView = SpoilerOverlayView()
|
||||
@ -261,6 +266,7 @@ public final class StatusView: UIView {
|
||||
setMediaDisplay(isDisplay: false)
|
||||
setPollDisplay(isDisplay: false)
|
||||
setFilterHintLabelDisplay(isDisplay: false)
|
||||
setStatusCardControlDisplay(isDisplay: false)
|
||||
setupTranslationIndicator()
|
||||
}
|
||||
|
||||
@ -302,10 +308,14 @@ extension StatusView {
|
||||
// content
|
||||
contentMetaText.textView.delegate = self
|
||||
contentMetaText.textView.linkDelegate = self
|
||||
|
||||
|
||||
// card
|
||||
statusCardControl.addTarget(self, action: #selector(statusCardControlPressed), for: .touchUpInside)
|
||||
statusCardControl.delegate = self
|
||||
|
||||
// media
|
||||
mediaGridContainerView.delegate = self
|
||||
|
||||
|
||||
// poll
|
||||
pollTableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
pollTableViewHeightLayoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1)
|
||||
@ -340,6 +350,12 @@ extension StatusView {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
delegate?.statusView(self, spoilerOverlayViewDidPressed: spoilerOverlayView)
|
||||
}
|
||||
|
||||
@objc private func statusCardControlPressed(_ sender: StatusCardControl) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
guard let url = viewModel.card?.url else { return }
|
||||
delegate?.statusView(self, didTapCardWithURL: url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -415,11 +431,11 @@ extension StatusView.Style {
|
||||
statusView.authorAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin
|
||||
statusView.containerStackView.addArrangedSubview(statusView.authorAdaptiveMarginContainerView)
|
||||
|
||||
// content container: V - [ contentMetaText ]
|
||||
// content container: V - [ contentMetaText statusCardControl ]
|
||||
statusView.contentContainer.axis = .vertical
|
||||
statusView.contentContainer.spacing = 12
|
||||
statusView.contentContainer.distribution = .fill
|
||||
statusView.contentContainer.alignment = .top
|
||||
statusView.contentContainer.alignment = .fill
|
||||
|
||||
statusView.contentAdaptiveMarginContainerView.contentView = statusView.contentContainer
|
||||
statusView.contentAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin
|
||||
@ -429,7 +445,7 @@ extension StatusView.Style {
|
||||
|
||||
// status content
|
||||
statusView.contentContainer.addArrangedSubview(statusView.contentMetaText.textView)
|
||||
statusView.containerStackView.setCustomSpacing(16, after: statusView.contentMetaText.textView)
|
||||
statusView.contentContainer.addArrangedSubview(statusView.statusCardControl)
|
||||
|
||||
// translated info
|
||||
statusView.containerStackView.addArrangedSubview(statusView.isTranslatingLoadingView)
|
||||
@ -521,6 +537,7 @@ extension StatusView.Style {
|
||||
|
||||
statusView.headerAdaptiveMarginContainerView.removeFromSuperview()
|
||||
statusView.authorAdaptiveMarginContainerView.removeFromSuperview()
|
||||
statusView.statusCardControl.removeFromSuperview()
|
||||
}
|
||||
|
||||
func notificationQuote(statusView: StatusView) {
|
||||
@ -529,6 +546,7 @@ extension StatusView.Style {
|
||||
statusView.contentAdaptiveMarginContainerView.bottomLayoutConstraint?.constant = 16 // fix bottom margin missing issue
|
||||
statusView.pollAdaptiveMarginContainerView.bottomLayoutConstraint?.constant = 16 // fix bottom margin missing issue
|
||||
statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview()
|
||||
statusView.statusCardControl.removeFromSuperview()
|
||||
}
|
||||
|
||||
func composeStatusReplica(statusView: StatusView) {
|
||||
@ -574,6 +592,10 @@ extension StatusView {
|
||||
func setFilterHintLabelDisplay(isDisplay: Bool = true) {
|
||||
filterHintLabel.isHidden = !isDisplay
|
||||
}
|
||||
|
||||
func setStatusCardControlDisplay(isDisplay: Bool = true) {
|
||||
statusCardControl.isHidden = !isDisplay
|
||||
}
|
||||
|
||||
// container width
|
||||
public var contentMaxLayoutWidth: CGFloat {
|
||||
@ -712,7 +734,7 @@ extension StatusView {
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
viewModel.$translatedFromLanguage
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] translatedFromLanguage in
|
||||
@ -728,6 +750,17 @@ extension StatusView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: StatusCardControlDelegate
|
||||
extension StatusView: StatusCardControlDelegate {
|
||||
public func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL) {
|
||||
delegate?.statusView(self, cardControl: statusCardControl, didTapURL: url)
|
||||
}
|
||||
|
||||
public func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> UIMenu? {
|
||||
delegate?.statusView(self, cardControlMenu: statusCardControl)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
|
||||
|
@ -99,7 +99,8 @@ extension ShareViewController {
|
||||
let composeContentViewModel = ComposeContentViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
kind: .post
|
||||
destination: .topLevel,
|
||||
initialContent: ""
|
||||
)
|
||||
let composeContentViewController = ComposeContentViewController()
|
||||
composeContentViewController.viewModel = composeContentViewModel
|
||||
|
Loading…
x
Reference in New Issue
Block a user