This commit is contained in:
Justin Mazzocchi 2020-12-31 16:49:59 -08:00
parent ca2e8cf27e
commit 86b9e4c903
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
17 changed files with 430 additions and 358 deletions

View File

@ -8,7 +8,8 @@ final class CompositionAttachmentsDataSource: UICollectionViewDiffableDataSource
private let updateQueue =
DispatchQueue(label: "com.metabolist.metatext.composition-attachments-data-source.update-queue")
init(collectionView: UICollectionView, viewModelProvider: @escaping (IndexPath) -> CompositionAttachmentViewModel) {
init(collectionView: UICollectionView,
viewModelProvider: @escaping (IndexPath) -> CompositionAttachmentViewModel?) {
let registration = UICollectionView.CellRegistration
<CompositionAttachmentCollectionViewCell, CompositionAttachmentViewModel> {
$0.viewModel = $2

View File

@ -1,30 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import UIKit
import ViewModels
final class NewStatusDataSource: UICollectionViewDiffableDataSource<Int, CompositionViewModel.Id> {
private let updateQueue =
DispatchQueue(label: "com.metabolist.metatext.new-status-data-source.update-queue")
init(collectionView: UICollectionView, viewModelProvider: @escaping (IndexPath) -> CompositionViewModel) {
let registration = UICollectionView.CellRegistration<CompositionListCell, CompositionViewModel> {
$0.viewModel = $2
}
super.init(collectionView: collectionView) { collectionView, indexPath, _ in
collectionView.dequeueConfiguredReusableCell(
using: registration,
for: indexPath,
item: viewModelProvider(indexPath))
}
}
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<Int, CompositionViewModel.Id>,
animatingDifferences: Bool = true,
completion: (() -> Void)? = nil) {
updateQueue.async {
super.apply(snapshot, animatingDifferences: animatingDifferences, completion: completion)
}
}
}

View File

@ -0,0 +1,51 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import Mastodon
extension Status.Visibility {
var systemImageName: String {
switch self {
case .public:
return "network"
case .unlisted:
return "lock.open"
case .private:
return "lock"
case .direct:
return "envelope"
case .unknown:
return "questionmark"
}
}
var title: String? {
switch self {
case .public:
return NSLocalizedString("status.visibility.public", comment: "")
case .unlisted:
return NSLocalizedString("status.visibility.unlisted", comment: "")
case .private:
return NSLocalizedString("status.visibility.private", comment: "")
case .direct:
return NSLocalizedString("status.visibility.direct", comment: "")
case .unknown:
return nil
}
}
var description: String? {
switch self {
case .public:
return NSLocalizedString("status.visibility.public.description", comment: "")
case .unlisted:
return NSLocalizedString("status.visibility.unlisted.description", comment: "")
case .private:
return NSLocalizedString("status.visibility.private.description", comment: "")
case .direct:
return NSLocalizedString("status.visibility.direct.description", comment: "")
case .unknown:
return nil
}
}
}

View File

@ -138,6 +138,7 @@
"report.forward-%@" = "Forward report to %@";
"share-extension-error.no-account-found" = "No account found";
"status.bookmark" = "Bookmark";
"status.content-warning-abbreviation" = "CW";
"status.reblogged-by" = "%@ boosted";
"status.pinned-post" = "Pinned post";
"status.show-more" = "Show More";
@ -146,10 +147,16 @@
"status.poll.time-left" = "%@ left";
"status.poll.refresh" = "Refresh";
"status.poll.closed" = "Closed";
"status.spoiler-text-placeholder" = "Write your warning here";
"status.unbookmark" = "Unbookmark";
"status.visibility.public" = "Public";
"status.visibility.unlisted" = "Unlisted";
"status.visibility.private" = "Private";
"status.visibility.private" = "Followers-only";
"status.visibility.direct" = "Direct";
"status.visibility.public.description" = "Visible for all, shown in public timelines";
"status.visibility.unlisted.description" = "Visible for all, but not in public timelines";
"status.visibility.private.description" = "Visible for followers only";
"status.visibility.direct.description" = "Visible for mentioned users only";
"submit" = "Submit";
"timelines.home" = "Home";
"timelines.local" = "Local";

View File

@ -17,12 +17,21 @@ public extension StatusEndpoint {
struct Components {
public let inReplyToId: Status.Id?
public let text: String
public let spoilerText: String
public let mediaIds: [Attachment.Id]
public let visibility: Status.Visibility
public init(inReplyToId: Status.Id?, text: String, mediaIds: [Attachment.Id]) {
public init(
inReplyToId: Status.Id?,
text: String,
spoilerText: String,
mediaIds: [Attachment.Id],
visibility: Status.Visibility) {
self.inReplyToId = inReplyToId
self.text = text
self.spoilerText = spoilerText
self.mediaIds = mediaIds
self.visibility = visibility
}
}
}
@ -35,11 +44,16 @@ extension StatusEndpoint.Components {
params["status"] = text
}
if !spoilerText.isEmpty {
params["spoiler_text"] = spoilerText
}
if !mediaIds.isEmpty {
params["media_ids"] = mediaIds
}
params["in_reply_to_id"] = inReplyToId
params["visibility"] = visibility.rawValue
return params
}

View File

@ -19,12 +19,15 @@
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */; };
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
D036768E2593E6DE005DF15A /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; };
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA01254B6101009094DF /* NotificationListCell.swift */; };
D036AA07254B6118009094DF /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA06254B6118009094DF /* NotificationView.swift */; };
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */; };
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA16254CA823009094DF /* StatusBodyView.swift */; };
D038273C259EA38F00056E0F /* NewStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCC10F259C4F20000B67DF /* NewStatusView.swift */; };
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
D04F9E8E259E9C950081B0C9 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D04F9E8D259E9C950081B0C9 /* ViewModels */; };
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
D065965B25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065965A25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift */; };
@ -37,6 +40,7 @@
D0804134258D902900AD6139 /* CompositionAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0804132258D902900AD6139 /* CompositionAttachmentView.swift */; };
D080413E258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080413D258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift */; };
D080413F258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080413D258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift */; };
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; };
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */; };
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */; };
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */; };
@ -50,18 +54,10 @@
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */; };
D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */; };
D08E5276257C36CA00FA2C5F /* Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D08E526C257C36CA00FA2C5F /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D08E5292257C53B600FA2C5F /* NewStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E5291257C53B600FA2C5F /* NewStatusViewController.swift */; };
D08E529C257C58D600FA2C5F /* NewStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E529B257C58D600FA2C5F /* NewStatusView.swift */; };
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52A5257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift */; };
D08E52B8257C62D500FA2C5F /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D08E52B7257C62D500FA2C5F /* ViewModels */; };
D08E52BD257C635800FA2C5F /* NewStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E5291257C53B600FA2C5F /* NewStatusViewController.swift */; };
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */; };
D08E52CC257C80E300FA2C5F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45724F76169001EBDBB /* Localizable.strings */; };
D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52D1257C811200FA2C5F /* ShareExtensionError+Extensions.swift */; };
D08E52DC257D742B00FA2C5F /* CompositionListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */; };
D08E52DD257D742B00FA2C5F /* CompositionListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */; };
D08E52E3257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */; };
D08E52E4257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */; };
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; };
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; };
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.swift */; };
@ -119,12 +115,13 @@
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B125251A90F400942152 /* AccountListCell.swift */; };
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B12D251A97E400942152 /* TableViewController.swift */; };
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */; };
D0F2D4D1257EE84400986197 /* NewStatusDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */; };
D0F2D4D6257EED6100986197 /* NewStatusDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */; };
D0F2D4DB257F018300986197 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.swift */; };
D0F2D54025818C4B00986197 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = D0F2D53F25818C4B00986197 /* Kingfisher */; };
D0F2D5452581ABAB00986197 /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; };
D0F2D54B2581CF7D00986197 /* VisualEffectBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F2D54A2581CF7D00986197 /* VisualEffectBlur.swift */; };
D0FCC105259C4E61000B67DF /* NewStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */; };
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */; };
D0FCC110259C4F20000B67DF /* NewStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCC10F259C4F20000B67DF /* NewStatusView.swift */; };
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C8E253686F9003EF1EB /* PlayerView.swift */; };
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */; };
/* End PBXBuildFile section */
@ -197,6 +194,7 @@
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
D0804132258D902900AD6139 /* CompositionAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentView.swift; sourceTree = "<group>"; };
D080413D258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentContentConfiguration.swift; sourceTree = "<group>"; };
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Extensions.swift"; sourceTree = "<group>"; };
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePageViewController.swift; sourceTree = "<group>"; };
@ -213,13 +211,9 @@
D08E526C257C36CA00FA2C5F /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
D08E5273257C36CA00FA2C5F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D08E5277257C36CB00FA2C5F /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
D08E5291257C53B600FA2C5F /* NewStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusViewController.swift; sourceTree = "<group>"; };
D08E529B257C58D600FA2C5F /* NewStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusView.swift; sourceTree = "<group>"; };
D08E52A5257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionNavigationViewController.swift; sourceTree = "<group>"; };
D08E52C6257C7AEE00FA2C5F /* ShareErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareErrorViewController.swift; sourceTree = "<group>"; };
D08E52D1257C811200FA2C5F /* ShareExtensionError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShareExtensionError+Extensions.swift"; sourceTree = "<group>"; };
D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionListCell.swift; sourceTree = "<group>"; };
D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionContentConfiguration.swift; sourceTree = "<group>"; };
D08E52ED257D757100FA2C5F /* CompositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionView.swift; sourceTree = "<group>"; };
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
@ -280,8 +274,9 @@
D0F0B125251A90F400942152 /* AccountListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListCell.swift; sourceTree = "<group>"; };
D0F0B12D251A97E400942152 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = "<group>"; };
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionItem+Extensions.swift"; sourceTree = "<group>"; };
D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusDataSource.swift; sourceTree = "<group>"; };
D0F2D54A2581CF7D00986197 /* VisualEffectBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectBlur.swift; sourceTree = "<group>"; };
D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusViewController.swift; sourceTree = "<group>"; };
D0FCC10F259C4F20000B67DF /* NewStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusView.swift; sourceTree = "<group>"; };
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCache.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -309,7 +304,7 @@
buildActionMask = 2147483647;
files = (
D0F2D54025818C4B00986197 /* Kingfisher in Frameworks */,
D08E52B8257C62D500FA2C5F /* ViewModels in Frameworks */,
D04F9E8E259E9C950081B0C9 /* ViewModels in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -424,7 +419,6 @@
isa = PBXGroup;
children = (
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */,
D065965A25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift */,
);
path = "Data Sources";
@ -452,9 +446,7 @@
D065966025899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift */,
D080413D258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift */,
D0804132258D902900AD6139 /* CompositionAttachmentView.swift */,
D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */,
D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */,
D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */,
D08E52ED257D757100FA2C5F /* CompositionView.swift */,
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
@ -471,7 +463,7 @@
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */,
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */,
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
D08E529B257C58D600FA2C5F /* NewStatusView.swift */,
D0FCC10F259C4F20000B67DF /* NewStatusView.swift */,
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */,
D036AA01254B6101009094DF /* NotificationListCell.swift */,
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
@ -505,7 +497,7 @@
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
D08E5291257C53B600FA2C5F /* NewStatusViewController.swift */,
D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */,
D06BC5E525202AD90079541D /* ProfileViewController.swift */,
D0F0B12D251A97E400942152 /* TableViewController.swift */,
);
@ -538,6 +530,7 @@
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */,
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */,
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */,
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
@ -629,8 +622,8 @@
);
name = "Share Extension";
packageProductDependencies = (
D08E52B7257C62D500FA2C5F /* ViewModels */,
D0F2D53F25818C4B00986197 /* Kingfisher */,
D04F9E8D259E9C950081B0C9 /* ViewModels */,
);
productName = "Share Extension";
productReference = D08E526C257C36CA00FA2C5F /* Share Extension.appex */;
@ -769,7 +762,6 @@
files = (
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
D0F2D4D1257EE84400986197 /* NewStatusDataSource.swift in Sources */,
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
@ -781,7 +773,6 @@
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */,
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
D08E52E3257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */,
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
D0E7AD3925870B13005F5E2D /* UIVIewController+Extensions.swift in Sources */,
@ -789,7 +780,6 @@
D0BEB1F324F8EE8C001B0F04 /* StatusAttachmentView.swift in Sources */,
D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */,
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */,
D08E529C257C58D600FA2C5F /* NewStatusView.swift in Sources */,
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
@ -800,15 +790,14 @@
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
D0E9F9AA258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */,
D080413E258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift in Sources */,
D0625E59250F092900502611 /* StatusListCell.swift in Sources */,
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */,
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
D08E52DC257D742B00FA2C5F /* CompositionListCell.swift in Sources */,
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
D08E5292257C53B600FA2C5F /* NewStatusViewController.swift in Sources */,
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
@ -847,11 +836,13 @@
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */,
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
D0FCC105259C4E61000B67DF /* NewStatusViewController.swift in Sources */,
D0F2D54B2581CF7D00986197 /* VisualEffectBlur.swift in Sources */,
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */,
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
D0FCC110259C4F20000B67DF /* NewStatusView.swift in Sources */,
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
);
@ -869,23 +860,22 @@
buildActionMask = 2147483647;
files = (
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
D08E52BD257C635800FA2C5F /* NewStatusViewController.swift in Sources */,
D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */,
D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
D0F2D4DB257F018300986197 /* Array+Extensions.swift in Sources */,
D08E52DD257D742B00FA2C5F /* CompositionListCell.swift in Sources */,
D0E7AD4225870C79005F5E2D /* UIVIewController+Extensions.swift in Sources */,
D038273C259EA38F00056E0F /* NewStatusView.swift in Sources */,
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
D036768E2593E6DE005DF15A /* Status+Extensions.swift in Sources */,
D080413F258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift in Sources */,
D0F2D5452581ABAB00986197 /* KingfisherOptionsInfo+Extensions.swift in Sources */,
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
D0F2D4D6257EED6100986197 /* NewStatusDataSource.swift in Sources */,
D065966725899E910096AC5D /* CompositionAttachmentsDataSource.swift in Sources */,
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
D0804134258D902900AD6139 /* CompositionAttachmentView.swift in Sources */,
D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */,
D08E52E4257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */,
D065966225899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1293,15 +1283,15 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
D04F9E8D259E9C950081B0C9 /* ViewModels */ = {
isa = XCSwiftPackageProductDependency;
productName = ViewModels;
};
D06B492224D4611300642749 /* KingfisherSwiftUI */ = {
isa = XCSwiftPackageProductDependency;
package = D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = KingfisherSwiftUI;
};
D08E52B7257C62D500FA2C5F /* ViewModels */ = {
isa = XCSwiftPackageProductDependency;
productName = ViewModels;
};
D0BECB972501C0FC002C1B13 /* Secrets */ = {
isa = XCSwiftPackageProductDependency;
productName = Secrets;

View File

@ -2,7 +2,7 @@
import Combine
import ServiceLayer
import UIKit
import SwiftUI
import ViewModels
@objc(ShareExtensionNavigationViewController)
@ -25,7 +25,7 @@ class ShareExtensionNavigationViewController: UINavigationController {
}
setViewControllers(
[NewStatusViewController(viewModel: newStatusViewModel, isShareExtension: true)],
[UIHostingController(rootView: NewStatusView { newStatusViewModel })],
animated: false)
}

View File

@ -6,29 +6,22 @@ import PhotosUI
import UIKit
import ViewModels
class NewStatusViewController: UICollectionViewController {
final class NewStatusViewController: UIViewController {
private let viewModel: NewStatusViewModel
private let isShareExtension: Bool
private let scrollView = UIScrollView()
private let stackView = UIStackView()
private let postButton = UIBarButtonItem(
title: NSLocalizedString("post", comment: ""),
style: .done,
target: nil,
action: nil)
private var attachMediaTo: CompositionViewModel?
private let mediaSelections = PassthroughSubject<[PHPickerResult], Never>()
private var cancellables = Set<AnyCancellable>()
private lazy var dataSource: NewStatusDataSource = {
.init(collectionView: collectionView, viewModelProvider: viewModel.viewModel(indexPath:))
}()
init(viewModel: NewStatusViewModel, isShareExtension: Bool) {
init(viewModel: NewStatusViewModel) {
self.viewModel = viewModel
self.isShareExtension = isShareExtension
let configuration = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
super.init(collectionViewLayout: layout)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
@ -39,15 +32,101 @@ class NewStatusViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = dataSource
view.backgroundColor = .systemBackground
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .equalSpacing
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
])
postButton.primaryAction = UIAction(title: NSLocalizedString("post", comment: "")) { [weak self] _ in
self?.viewModel.post()
}
setupViewModelBindings()
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
setupBarButtonItems(identification: viewModel.identification)
}
}
extension NewStatusViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
mediaSelections.send(results)
dismiss(animated: true)
}
}
private extension NewStatusViewController {
func handle(event: NewStatusViewModel.Event) {
switch event {
case let .presentMediaPicker(compositionViewModel):
presentMediaPicker(compositionViewModel: compositionViewModel)
}
}
func dismiss() {
if let extensionContext = extensionContext {
extensionContext.completeRequest(returningItems: nil)
} else {
presentingViewController?.dismiss(animated: true)
}
}
func setupViewModelBindings() {
viewModel.events.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables)
viewModel.$canPost.sink { [weak self] in self?.postButton.isEnabled = $0 }.store(in: &cancellables)
viewModel.$compositionViewModels.sink { [weak self] in
guard let self = self else { return }
let diff = [$0.map(\.id)].snapshot().itemIdentifiers.difference(
from: [self.stackView.arrangedSubviews.compactMap { ($0 as? CompositionView)?.id }]
.snapshot().itemIdentifiers)
for insertion in diff.insertions {
guard case let .insert(index, id, _) = insertion,
let compositionViewModel = $0.first(where: { $0.id == id })
else { continue }
let compositionView = CompositionView(
viewModel: compositionViewModel,
parentViewModel: self.viewModel)
self.stackView.insertArrangedSubview(compositionView, at: index)
compositionView.textView.becomeFirstResponder()
DispatchQueue.main.async {
self.scrollView.scrollRectToVisible(
self.scrollView.convert(compositionView.frame, from: self.stackView),
animated: true)
}
}
for removal in diff.removals {
guard case let .remove(_, id, _) = removal else { continue }
self.stackView.arrangedSubviews.first { ($0 as? CompositionView)?.id == id }?.removeFromSuperview()
}
}
.store(in: &cancellables)
viewModel.$identification
.sink { [weak self] in
@ -57,33 +136,6 @@ class NewStatusViewController: UICollectionViewController {
}
.store(in: &cancellables)
viewModel.$compositionViewModels.sink { [weak self] in
guard let self = self else { return }
let oldSnapshot = self.dataSource.snapshot()
let newSnapshot = [$0.map(\.id)].snapshot()
let diff = newSnapshot.itemIdentifiers.difference(from: oldSnapshot.itemIdentifiers)
self.dataSource.apply(newSnapshot) {
if case let .insert(_, id, _) = diff.insertions.first,
let indexPath = self.dataSource.indexPath(for: id) {
self.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .top)
}
}
}
.store(in: &cancellables)
// Invalidate the collection view layout on anything that could change the height of a cell
viewModel.$compositionViewModels
.flatMap { Publishers.MergeMany($0.map(\.objectWillChange)) }
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.collectionView.collectionViewLayout.invalidateLayout() }
.store(in: &cancellables)
viewModel.$canPost.sink { [weak self] in self?.postButton.isEnabled = $0 }.store(in: &cancellables)
viewModel.events.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables)
viewModel.$alertItem
.compactMap { $0 }
.receive(on: DispatchQueue.main)
@ -91,46 +143,37 @@ class NewStatusViewController: UICollectionViewController {
.store(in: &cancellables)
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
setupBarButtonItems(identification: viewModel.identification)
}
func setupBarButtonItems(identification: Identification) {
let target = isShareExtension ? self : parent
let closeButton = UIBarButtonItem(
systemItem: .close,
primaryAction: UIAction { [weak self] _ in self?.dismiss() })
target?.navigationItem.leftBarButtonItem = closeButton
target?.navigationItem.titleView = viewModel.canChangeIdentity
parent?.navigationItem.leftBarButtonItem = closeButton
parent?.navigationItem.titleView = viewModel.canChangeIdentity
? changeIdentityButton(identification: identification)
: nil
target?.navigationItem.rightBarButtonItem = postButton
parent?.navigationItem.rightBarButtonItem = postButton
}
func dismiss() {
if isShareExtension {
extensionContext?.completeRequest(returningItems: nil)
} else {
presentingViewController?.dismiss(animated: true)
func presentMediaPicker(compositionViewModel: CompositionViewModel) {
mediaSelections.first().sink { [weak self] results in
guard let self = self, let result = results.first else { return }
self.viewModel.attach(itemProvider: result.itemProvider, to: compositionViewModel)
}
.store(in: &cancellables)
var configuration = PHPickerConfiguration()
configuration.preferredAssetRepresentationMode = .current
let picker = PHPickerViewController(configuration: configuration)
picker.modalPresentationStyle = .overFullScreen
picker.delegate = self
present(picker, animated: true)
}
}
extension NewStatusViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
dismiss(animated: true)
guard let result = results.first else { return }
attachMediaTo?.attach(itemProvider: result.itemProvider)
attachMediaTo = nil
}
}
private extension NewStatusViewController {
func changeIdentityButton(identification: Identification) -> UIButton {
let changeIdentityButton = UIButton()
let downsampled = KingfisherOptionsInfo.downsampled(
@ -168,22 +211,4 @@ private extension NewStatusViewController {
return changeIdentityButton
}
func handle(event: CompositionViewModel.Event) {
switch event {
case let .presentMediaPicker(compositionViewModel):
attachMediaTo = compositionViewModel
var configuration = PHPickerConfiguration()
configuration.preferredAssetRepresentationMode = .current
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true)
default:
break
}
}
}

View File

@ -90,8 +90,4 @@ public extension DomainBlocksViewModel {
static let preview = DomainBlocksViewModel(service: .init(mastodonAPIClient: .preview))
}
public extension NewStatusViewModel {
static let preview = RootViewModel.preview.newStatusViewModel(identification: .preview)
}
// swiftlint:enable force_try

View File

@ -5,25 +5,26 @@ import Foundation
import Mastodon
import ServiceLayer
public final class CompositionViewModel: ObservableObject {
public final class CompositionViewModel: ObservableObject, Identifiable {
public let id = Id()
public var isPosted = false
@Published public var text = ""
@Published public var contentWarning = ""
@Published public var displayContentWarning = false
@Published public private(set) var attachmentViewModels = [CompositionAttachmentViewModel]()
@Published public private(set) var isPostable = false
@Published public private(set) var identification: Identification
@Published public private(set) var attachmentUpload: AttachmentUpload?
private let eventsSubject: PassthroughSubject<Event, Never>
private var cancellables = Set<AnyCancellable>()
init(identification: Identification,
identificationPublisher: AnyPublisher<Identification, Never>,
eventsSubject: PassthroughSubject<Event, Never>) {
self.identification = identification
self.eventsSubject = eventsSubject
identificationPublisher.assign(to: &$identification)
$text.map { !$0.isEmpty }.removeDuplicates().assign(to: &$isPostable)
init() {
$text.map { !$0.isEmpty }
.removeDuplicates()
.combineLatest($attachmentViewModels.map { !$0.isEmpty })
.map { textPresent, attachmentPresent in
textPresent || attachmentPresent
}
.assign(to: &$isPostable)
}
}
@ -36,49 +37,41 @@ public extension CompositionViewModel {
case error(Error)
}
func components(inReplyToId: Status.Id?) -> StatusComponents {
func components(inReplyToId: Status.Id?, visibility: Status.Visibility) -> StatusComponents {
StatusComponents(
inReplyToId: inReplyToId,
text: text,
mediaIds: attachmentViewModels.map(\.attachment.id))
}
func presentMediaPicker() {
eventsSubject.send(.presentMediaPicker(self))
}
func insert() {
eventsSubject.send(.insertAfter(self))
}
func attach(itemProvider: NSItemProvider) {
let progress = Progress(totalUnitCount: 1)
MediaProcessingService.dataAndMimeType(itemProvider: itemProvider)
.flatMap { [weak self] data, mimeType -> AnyPublisher<Attachment, Error> in
guard let self = self else { return Empty().eraseToAnyPublisher() }
DispatchQueue.main.async {
self.attachmentUpload = AttachmentUpload(progress: progress, data: data, mimeType: mimeType)
}
return self.identification.service.uploadAttachment(data: data, mimeType: mimeType, progress: progress)
}
.print()
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.attachmentUpload = nil
if case let .failure(error) = $0 {
self?.eventsSubject.send(.error(error))
}
} receiveValue: { [weak self] in
self?.attachmentViewModels.append(CompositionAttachmentViewModel(attachment: $0))
}
.store(in: &cancellables)
spoilerText: displayContentWarning ? contentWarning : "",
mediaIds: attachmentViewModels.map(\.attachment.id),
visibility: visibility)
}
func attachmentViewModel(indexPath: IndexPath) -> CompositionAttachmentViewModel {
attachmentViewModels[indexPath.item]
}
}
extension CompositionViewModel {
func attach(itemProvider: NSItemProvider, service: IdentityService) -> AnyPublisher<Never, Error> {
MediaProcessingService.dataAndMimeType(itemProvider: itemProvider)
.flatMap { [weak self] data, mimeType -> AnyPublisher<Attachment, Error> in
guard let self = self else { return Empty().eraseToAnyPublisher() }
let progress = Progress(totalUnitCount: 1)
DispatchQueue.main.async {
self.attachmentUpload = AttachmentUpload(progress: progress, data: data, mimeType: mimeType)
}
return service.uploadAttachment(data: data, mimeType: mimeType, progress: progress)
}
.receive(on: DispatchQueue.main)
.handleEvents(
receiveOutput: { [weak self] in
self?.attachmentViewModels.append(CompositionAttachmentViewModel(attachment: $0))
},
receiveCompletion: { [weak self] _ in self?.attachmentUpload = nil })
.ignoreOutput()
.eraseToAnyPublisher()
}
}

View File

@ -6,18 +6,19 @@ import Mastodon
import ServiceLayer
public final class NewStatusViewModel: ObservableObject {
@Published public private(set) var compositionViewModels = [CompositionViewModel]()
@Published public var visibility: Status.Visibility
@Published public private(set) var compositionViewModels = [CompositionViewModel()]
@Published public private(set) var identification: Identification
@Published public private(set) var authenticatedIdentities = [Identity]()
@Published public var canPost = false
@Published public var canChangeIdentity = true
@Published public var alertItem: AlertItem?
@Published public private(set) var loading = false
public let events: AnyPublisher<CompositionViewModel.Event, Never>
public let events: AnyPublisher<Event, Never>
private let allIdentitiesService: AllIdentitiesService
private let environment: AppEnvironment
private let eventsSubject = PassthroughSubject<CompositionViewModel.Event, Never>()
private let eventsSubject = PassthroughSubject<Event, Never>()
private let itemEventsSubject = PassthroughSubject<CompositionViewModel.Event, Never>()
private var cancellables = Set<AnyCancellable>()
@ -28,8 +29,7 @@ public final class NewStatusViewModel: ObservableObject {
self.identification = identification
self.environment = environment
events = eventsSubject.eraseToAnyPublisher()
compositionViewModels = [newCompositionViewModel()]
itemEventsSubject.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables)
visibility = identification.identity.preferences.postingDefaultVisibility
allIdentitiesService.authenticatedIdentitiesPublisher()
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$authenticatedIdentities)
@ -43,8 +43,8 @@ public final class NewStatusViewModel: ObservableObject {
}
public extension NewStatusViewModel {
func viewModel(indexPath: IndexPath) -> CompositionViewModel {
compositionViewModels[indexPath.row]
enum Event {
case presentMediaPicker(CompositionViewModel)
}
func setIdentity(_ identity: Identity) {
@ -66,6 +66,34 @@ public extension NewStatusViewModel {
environment: environment)
}
func presentMediaPicker(viewModel: CompositionViewModel) {
eventsSubject.send(.presentMediaPicker(viewModel))
}
func insert(after: CompositionViewModel) {
guard let index = compositionViewModels.firstIndex(where: { $0 === after })
else { return }
let newViewModel = CompositionViewModel()
newViewModel.contentWarning = after.contentWarning
newViewModel.displayContentWarning = after.displayContentWarning
if index >= compositionViewModels.count - 1 {
compositionViewModels.append(newViewModel)
} else {
compositionViewModels.insert(newViewModel, at: index + 1)
}
}
func attach(itemProvider: NSItemProvider, to compositionViewModel: CompositionViewModel) {
compositionViewModel.attach(itemProvider: itemProvider, service: identification.service)
.receive(on: DispatchQueue.main)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in }
.store(in: &cancellables)
}
func post() {
guard let unposted = compositionViewModels.first(where: { !$0.isPosted }) else { return }
@ -74,35 +102,11 @@ public extension NewStatusViewModel {
}
private extension NewStatusViewModel {
func newCompositionViewModel() -> CompositionViewModel {
CompositionViewModel(
identification: identification,
identificationPublisher: $identification.eraseToAnyPublisher(),
eventsSubject: itemEventsSubject)
}
func handle(event: CompositionViewModel.Event) {
switch event {
case let .insertAfter(viewModel):
guard let index = compositionViewModels.firstIndex(where: { $0 === viewModel }) else { return }
let newViewModel = newCompositionViewModel()
if index >= compositionViewModels.count - 1 {
compositionViewModels.append(newViewModel)
} else {
compositionViewModels.insert(newViewModel, at: index + 1)
}
case let .error(error):
alertItem = AlertItem(error: error)
default:
eventsSubject.send(event)
}
}
func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
loading = true
identification.service.post(statusComponents: viewModel.components(inReplyToId: inReplyToId))
identification.service.post(statusComponents: viewModel.components(
inReplyToId: inReplyToId,
visibility: visibility))
.receive(on: DispatchQueue.main)
.sink { [weak self] in
guard let self = self else { return }

View File

@ -1,18 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import UIKit
import ViewModels
struct CompositionContentConfiguration {
let viewModel: CompositionViewModel
}
extension CompositionContentConfiguration: UIContentConfiguration {
func makeContentView() -> UIView & UIContentView {
CompositionView(configuration: self)
}
func updated(for state: UIConfigurationState) -> CompositionContentConfiguration {
self
}
}

View File

@ -1,16 +1,23 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Mastodon
import UIKit
import ViewModels
class CompositionInputAccessoryView: UIView {
private let stackView = UIStackView()
final class CompositionInputAccessoryView: UIView {
let visibilityButton = UIButton()
let addButton = UIButton()
let contentWarningButton = UIButton(type: .system)
private let viewModel: CompositionViewModel
private let parentViewModel: NewStatusViewModel
private let stackView = UIStackView()
private var cancellables = Set<AnyCancellable>()
init(viewModel: CompositionViewModel) {
init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) {
self.viewModel = viewModel
self.parentViewModel = parentViewModel
super.init(frame: .zero)
@ -28,6 +35,7 @@ class CompositionInputAccessoryView: UIView {
}
private extension CompositionInputAccessoryView {
// swiftlint:disable:next function_body_length
func initialSetup() {
autoresizingMask = .flexibleHeight
backgroundColor = .secondarySystemFill
@ -44,7 +52,12 @@ private extension CompositionInputAccessoryView {
systemName: "photo",
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
for: .normal)
mediaButton.addAction(UIAction { [weak self] _ in self?.viewModel.presentMediaPicker() }, for: .touchUpInside)
mediaButton.addAction(UIAction { [weak self] _ in
guard let self = self else { return }
self.parentViewModel.presentMediaPicker(viewModel: self.viewModel)
},
for: .touchUpInside)
let pollButton = UIButton()
@ -55,9 +68,26 @@ private extension CompositionInputAccessoryView {
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
for: .normal)
stackView.addArrangedSubview(UIView())
stackView.addArrangedSubview(visibilityButton)
visibilityButton.showsMenuAsPrimaryAction = true
visibilityButton.menu = UIMenu(children: Status.Visibility.allCasesExceptUnknown.reversed().map { visibility in
UIAction(
title: visibility.title ?? "",
image: UIImage(systemName: visibility.systemImageName),
discoverabilityTitle: visibility.description) { [weak self] _ in
self?.parentViewModel.visibility = visibility
}
})
let addButton = UIButton()
stackView.addArrangedSubview(contentWarningButton)
contentWarningButton.setTitle(
NSLocalizedString("status.content-warning-abbreviation", comment: ""),
for: .normal)
contentWarningButton.addAction(
UIAction { [weak self] _ in self?.viewModel.displayContentWarning.toggle() },
for: .touchUpInside)
stackView.addArrangedSubview(UIView())
stackView.addArrangedSubview(addButton)
addButton.setImage(
@ -65,20 +95,32 @@ private extension CompositionInputAccessoryView {
systemName: "plus.circle.fill",
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
for: .normal)
addButton.addAction(UIAction { [weak self] _ in self?.viewModel.insert() }, for: .touchUpInside)
addButton.addAction(UIAction { [weak self] _ in
guard let self = self else { return }
for button in [mediaButton, pollButton, addButton] {
self.parentViewModel.insert(after: self.viewModel)
}, for: .touchUpInside)
viewModel.$isPostable
.sink { [weak self] in self?.addButton.isEnabled = $0 }
.store(in: &cancellables)
parentViewModel.$visibility
.sink { [weak self] in
self?.visibilityButton.setImage(UIImage(systemName: $0.systemImageName), for: .normal)
}
.store(in: &cancellables)
for button in [mediaButton, pollButton, visibilityButton, contentWarningButton, addButton] {
button.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
button.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
}
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
viewModel.$isPostable.sink { addButton.isEnabled = $0 }.store(in: &cancellables)
}
}

View File

@ -1,29 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import UIKit
import ViewModels
class CompositionListCell: UICollectionViewListCell {
var viewModel: CompositionViewModel?
override func updateConfiguration(using state: UICellConfigurationState) {
guard let viewModel = viewModel else { return }
contentConfiguration = CompositionContentConfiguration(viewModel: viewModel).updated(for: state)
backgroundConfiguration = UIBackgroundConfiguration.clear().updated(for: state)
}
override var isSelected: Bool {
didSet {
if isSelected {
(contentView as? CompositionView)?.textView.becomeFirstResponder()
}
}
}
override func updateConstraints() {
super.updateConstraints()
separatorLayoutGuide.trailingAnchor.constraint(equalTo: separatorLayoutGuide.leadingAnchor).isActive = true
}
}

View File

@ -3,24 +3,29 @@
import Combine
import Kingfisher
import UIKit
import ViewModels
final class CompositionView: UIView {
let avatarImageView = UIImageView()
let spoilerTextField = UITextField()
let textView = UITextView()
let attachmentUploadView = AttachmentUploadView()
let attachmentsCollectionView: UICollectionView
private var compositionConfiguration: CompositionContentConfiguration
private let viewModel: CompositionViewModel
private let parentViewModel: NewStatusViewModel
private var cancellables = Set<AnyCancellable>()
private lazy var attachmentsDataSource: CompositionAttachmentsDataSource = {
CompositionAttachmentsDataSource(
collectionView: attachmentsCollectionView,
viewModelProvider: compositionConfiguration.viewModel.attachmentViewModel(indexPath:))
collectionView: attachmentsCollectionView) { [weak self] in
self?.viewModel.attachmentViewModel(indexPath: $0)
}
}()
init(configuration: CompositionContentConfiguration) {
self.compositionConfiguration = configuration
init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) {
self.viewModel = viewModel
self.parentViewModel = parentViewModel
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.2),
@ -42,7 +47,6 @@ final class CompositionView: UIView {
super.init(frame: .zero)
initialSetup()
applyCompositionConfiguration()
}
@available(*, unavailable)
@ -51,93 +55,124 @@ final class CompositionView: UIView {
}
}
extension CompositionView: UIContentView {
var configuration: UIContentConfiguration {
get { compositionConfiguration }
set {
guard let compositionConfiguration = newValue as? CompositionContentConfiguration else { return }
self.compositionConfiguration = compositionConfiguration
applyCompositionConfiguration()
}
}
extension CompositionView {
var id: CompositionViewModel.Id { viewModel.id }
}
extension CompositionView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
compositionConfiguration.viewModel.text = textView.text
viewModel.text = textView.text
}
}
private extension CompositionView {
static let attachmentUploadViewHeight: CGFloat = 100
// swiftlint:disable:next function_body_length
func initialSetup() {
tag = viewModel.id.hashValue
addSubview(avatarImageView)
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
avatarImageView.layer.cornerRadius = .avatarDimension / 2
avatarImageView.clipsToBounds = true
let stackView = UIStackView()
let inputAccessoryView = CompositionInputAccessoryView(viewModel: viewModel, parentViewModel: parentViewModel)
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = .defaultSpacing
stackView.addArrangedSubview(spoilerTextField)
spoilerTextField.backgroundColor = .secondarySystemBackground
spoilerTextField.layer.cornerRadius = .defaultCornerRadius
spoilerTextField.adjustsFontForContentSizeCategory = true
spoilerTextField.font = .preferredFont(forTextStyle: .body)
spoilerTextField.placeholder = NSLocalizedString("status.spoiler-text-placeholder", comment: "")
spoilerTextField.inputAccessoryView = inputAccessoryView
spoilerTextField.addAction(
UIAction { [weak self] _ in
guard let self = self, let text = self.spoilerTextField.text else { return }
self.viewModel.contentWarning = text
},
for: .editingChanged)
stackView.addArrangedSubview(textView)
textView.backgroundColor = .secondarySystemBackground
textView.layer.cornerRadius = .defaultCornerRadius
textView.isScrollEnabled = false
textView.adjustsFontForContentSizeCategory = true
textView.font = .preferredFont(forTextStyle: .body)
textView.textContainer.lineFragmentPadding = 0
textView.inputAccessoryView = CompositionInputAccessoryView(viewModel: compositionConfiguration.viewModel)
// textView.textContainer.lineFragmentPadding = 0
textView.inputAccessoryView = inputAccessoryView
textView.inputAccessoryView?.sizeToFit()
textView.delegate = self
textView.setContentHuggingPriority(.required, for: .vertical)
stackView.addArrangedSubview(attachmentsCollectionView)
attachmentsCollectionView.dataSource = attachmentsDataSource
stackView.addArrangedSubview(attachmentUploadView)
let constraints = [
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),
avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension),
avatarImageView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
avatarImageView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
avatarImageView.bottomAnchor.constraint(lessThanOrEqualTo: readableContentGuide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing),
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
attachmentsCollectionView.heightAnchor.constraint(
equalTo: attachmentsCollectionView.widthAnchor,
multiplier: 1 / 4),
attachmentUploadView.heightAnchor.constraint(equalToConstant: Self.attachmentUploadViewHeight)
]
textView.text = viewModel.text
spoilerTextField.text = viewModel.contentWarning
for constraint in constraints {
constraint.priority = .justBelowMax
viewModel.$displayContentWarning
.sink { [weak self] in
guard let self = self else { return }
if self.spoilerTextField.isHidden && self.textView.isFirstResponder && $0 {
self.spoilerTextField.becomeFirstResponder()
} else if !self.spoilerTextField.isHidden && self.spoilerTextField.isFirstResponder && !$0 {
self.textView.becomeFirstResponder()
}
NSLayoutConstraint.activate(constraints)
self.spoilerTextField.isHidden = !$0
}
.store(in: &cancellables)
func applyCompositionConfiguration() {
cancellables.removeAll()
compositionConfiguration.viewModel.$identification.map(\.identity.image)
parentViewModel.$identification.map(\.identity.image)
.sink { [weak self] in self?.avatarImageView.kf.setImage(with: $0) }
.store(in: &cancellables)
compositionConfiguration.viewModel.$attachmentViewModels
viewModel.$attachmentViewModels
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
.sink { [weak self] in
self?.attachmentsDataSource.apply([$0.map(\.attachment)].snapshot())
self?.attachmentsCollectionView.isHidden = $0.isEmpty
}
.store(in: &cancellables)
compositionConfiguration.viewModel.$attachmentUpload
viewModel.$attachmentUpload
.sink { [weak self] in self?.attachmentUploadView.attachmentUpload = $0 }
.store(in: &cancellables)
let guide = UIDevice.current.userInterfaceIdiom == .pad ? readableContentGuide : layoutMarginsGuide
let constraints = [
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),
avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension),
avatarImageView.topAnchor.constraint(equalTo: guide.topAnchor),
avatarImageView.leadingAnchor.constraint(equalTo: guide.leadingAnchor),
avatarImageView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing),
stackView.topAnchor.constraint(equalTo: guide.topAnchor),
stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor),
attachmentsCollectionView.heightAnchor.constraint(
equalTo: attachmentsCollectionView.widthAnchor,
multiplier: 1 / 4),
attachmentUploadView.heightAnchor.constraint(equalToConstant: Self.attachmentUploadViewHeight)
]
if UIDevice.current.userInterfaceIdiom == .pad {
for constraint in constraints {
constraint.priority = .justBelowMax
}
}
NSLayoutConstraint.activate(constraints)
}
}

View File

@ -7,16 +7,10 @@ struct NewStatusView: UIViewControllerRepresentable {
let viewModelClosure: () -> NewStatusViewModel
func makeUIViewController(context: Context) -> NewStatusViewController {
NewStatusViewController(viewModel: viewModelClosure(), isShareExtension: false)
NewStatusViewController(viewModel: viewModelClosure())
}
func updateUIViewController(_ uiViewController: NewStatusViewController, context: Context) {
}
}
struct NewStatusView_Previews: PreviewProvider {
static var previews: some View {
NewStatusView { .preview }
}
}

View File

@ -41,10 +41,7 @@ struct TabNavigationView: View {
EmptyView()
.fullScreenCover(isPresented: $viewModel.presentingNewStatus) {
NavigationView {
NewStatusView {
rootViewModel.newStatusViewModel(identification: viewModel.identification)
}
.edgesIgnoringSafeArea(.all)
NewStatusView { rootViewModel.newStatusViewModel(identification: viewModel.identification) }
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())