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 = private let updateQueue =
DispatchQueue(label: "com.metabolist.metatext.composition-attachments-data-source.update-queue") 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 let registration = UICollectionView.CellRegistration
<CompositionAttachmentCollectionViewCell, CompositionAttachmentViewModel> { <CompositionAttachmentCollectionViewCell, CompositionAttachmentViewModel> {
$0.viewModel = $2 $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 %@"; "report.forward-%@" = "Forward report to %@";
"share-extension-error.no-account-found" = "No account found"; "share-extension-error.no-account-found" = "No account found";
"status.bookmark" = "Bookmark"; "status.bookmark" = "Bookmark";
"status.content-warning-abbreviation" = "CW";
"status.reblogged-by" = "%@ boosted"; "status.reblogged-by" = "%@ boosted";
"status.pinned-post" = "Pinned post"; "status.pinned-post" = "Pinned post";
"status.show-more" = "Show More"; "status.show-more" = "Show More";
@ -146,10 +147,16 @@
"status.poll.time-left" = "%@ left"; "status.poll.time-left" = "%@ left";
"status.poll.refresh" = "Refresh"; "status.poll.refresh" = "Refresh";
"status.poll.closed" = "Closed"; "status.poll.closed" = "Closed";
"status.spoiler-text-placeholder" = "Write your warning here";
"status.unbookmark" = "Unbookmark"; "status.unbookmark" = "Unbookmark";
"status.visibility.public" = "Public"; "status.visibility.public" = "Public";
"status.visibility.unlisted" = "Unlisted"; "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"; "submit" = "Submit";
"timelines.home" = "Home"; "timelines.home" = "Home";
"timelines.local" = "Local"; "timelines.local" = "Local";

View file

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

View file

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

View file

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

View file

@ -6,29 +6,22 @@ import PhotosUI
import UIKit import UIKit
import ViewModels import ViewModels
class NewStatusViewController: UICollectionViewController { final class NewStatusViewController: UIViewController {
private let viewModel: NewStatusViewModel private let viewModel: NewStatusViewModel
private let isShareExtension: Bool private let scrollView = UIScrollView()
private let stackView = UIStackView()
private let postButton = UIBarButtonItem( private let postButton = UIBarButtonItem(
title: NSLocalizedString("post", comment: ""), title: NSLocalizedString("post", comment: ""),
style: .done, style: .done,
target: nil, target: nil,
action: nil) action: nil)
private var attachMediaTo: CompositionViewModel? private let mediaSelections = PassthroughSubject<[PHPickerResult], Never>()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private lazy var dataSource: NewStatusDataSource = { init(viewModel: NewStatusViewModel) {
.init(collectionView: collectionView, viewModelProvider: viewModel.viewModel(indexPath:))
}()
init(viewModel: NewStatusViewModel, isShareExtension: Bool) {
self.viewModel = viewModel self.viewModel = viewModel
self.isShareExtension = isShareExtension
let configuration = UICollectionLayoutListConfiguration(appearance: .plain) super.init(nibName: nil, bundle: nil)
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
super.init(collectionViewLayout: layout)
} }
@available(*, unavailable) @available(*, unavailable)
@ -39,15 +32,101 @@ class NewStatusViewController: UICollectionViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
collectionView.dataSource = dataSource
view.backgroundColor = .systemBackground 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 postButton.primaryAction = UIAction(title: NSLocalizedString("post", comment: "")) { [weak self] _ in
self?.viewModel.post() self?.viewModel.post()
} }
setupViewModelBindings()
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
setupBarButtonItems(identification: viewModel.identification) 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 viewModel.$identification
.sink { [weak self] in .sink { [weak self] in
@ -57,33 +136,6 @@ class NewStatusViewController: UICollectionViewController {
} }
.store(in: &cancellables) .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 viewModel.$alertItem
.compactMap { $0 } .compactMap { $0 }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@ -91,46 +143,37 @@ class NewStatusViewController: UICollectionViewController {
.store(in: &cancellables) .store(in: &cancellables)
} }
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
setupBarButtonItems(identification: viewModel.identification)
}
func setupBarButtonItems(identification: Identification) { func setupBarButtonItems(identification: Identification) {
let target = isShareExtension ? self : parent
let closeButton = UIBarButtonItem( let closeButton = UIBarButtonItem(
systemItem: .close, systemItem: .close,
primaryAction: UIAction { [weak self] _ in self?.dismiss() }) primaryAction: UIAction { [weak self] _ in self?.dismiss() })
target?.navigationItem.leftBarButtonItem = closeButton parent?.navigationItem.leftBarButtonItem = closeButton
target?.navigationItem.titleView = viewModel.canChangeIdentity parent?.navigationItem.titleView = viewModel.canChangeIdentity
? changeIdentityButton(identification: identification) ? changeIdentityButton(identification: identification)
: nil : nil
target?.navigationItem.rightBarButtonItem = postButton parent?.navigationItem.rightBarButtonItem = postButton
} }
func dismiss() { func presentMediaPicker(compositionViewModel: CompositionViewModel) {
if isShareExtension { mediaSelections.first().sink { [weak self] results in
extensionContext?.completeRequest(returningItems: nil) guard let self = self, let result = results.first else { return }
} else {
presentingViewController?.dismiss(animated: true) 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 { func changeIdentityButton(identification: Identification) -> UIButton {
let changeIdentityButton = UIButton() let changeIdentityButton = UIButton()
let downsampled = KingfisherOptionsInfo.downsampled( let downsampled = KingfisherOptionsInfo.downsampled(
@ -168,22 +211,4 @@ private extension NewStatusViewController {
return changeIdentityButton 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)) static let preview = DomainBlocksViewModel(service: .init(mastodonAPIClient: .preview))
} }
public extension NewStatusViewModel {
static let preview = RootViewModel.preview.newStatusViewModel(identification: .preview)
}
// swiftlint:enable force_try // swiftlint:enable force_try

View file

@ -5,25 +5,26 @@ import Foundation
import Mastodon import Mastodon
import ServiceLayer import ServiceLayer
public final class CompositionViewModel: ObservableObject { public final class CompositionViewModel: ObservableObject, Identifiable {
public let id = Id() public let id = Id()
public var isPosted = false public var isPosted = false
@Published public var text = "" @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 attachmentViewModels = [CompositionAttachmentViewModel]()
@Published public private(set) var isPostable = false @Published public private(set) var isPostable = false
@Published public private(set) var identification: Identification
@Published public private(set) var attachmentUpload: AttachmentUpload? @Published public private(set) var attachmentUpload: AttachmentUpload?
private let eventsSubject: PassthroughSubject<Event, Never>
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(identification: Identification, init() {
identificationPublisher: AnyPublisher<Identification, Never>, $text.map { !$0.isEmpty }
eventsSubject: PassthroughSubject<Event, Never>) { .removeDuplicates()
self.identification = identification .combineLatest($attachmentViewModels.map { !$0.isEmpty })
self.eventsSubject = eventsSubject .map { textPresent, attachmentPresent in
identificationPublisher.assign(to: &$identification) textPresent || attachmentPresent
$text.map { !$0.isEmpty }.removeDuplicates().assign(to: &$isPostable) }
.assign(to: &$isPostable)
} }
} }
@ -36,49 +37,41 @@ public extension CompositionViewModel {
case error(Error) case error(Error)
} }
func components(inReplyToId: Status.Id?) -> StatusComponents { func components(inReplyToId: Status.Id?, visibility: Status.Visibility) -> StatusComponents {
StatusComponents( StatusComponents(
inReplyToId: inReplyToId, inReplyToId: inReplyToId,
text: text, text: text,
mediaIds: attachmentViewModels.map(\.attachment.id)) spoilerText: displayContentWarning ? contentWarning : "",
} mediaIds: attachmentViewModels.map(\.attachment.id),
visibility: visibility)
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)
} }
func attachmentViewModel(indexPath: IndexPath) -> CompositionAttachmentViewModel { func attachmentViewModel(indexPath: IndexPath) -> CompositionAttachmentViewModel {
attachmentViewModels[indexPath.item] 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 import ServiceLayer
public final class NewStatusViewModel: ObservableObject { 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 identification: Identification
@Published public private(set) var authenticatedIdentities = [Identity]() @Published public private(set) var authenticatedIdentities = [Identity]()
@Published public var canPost = false @Published public var canPost = false
@Published public var canChangeIdentity = true @Published public var canChangeIdentity = true
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
@Published public private(set) var loading = false @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 allIdentitiesService: AllIdentitiesService
private let environment: AppEnvironment 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 let itemEventsSubject = PassthroughSubject<CompositionViewModel.Event, Never>()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@ -28,8 +29,7 @@ public final class NewStatusViewModel: ObservableObject {
self.identification = identification self.identification = identification
self.environment = environment self.environment = environment
events = eventsSubject.eraseToAnyPublisher() events = eventsSubject.eraseToAnyPublisher()
compositionViewModels = [newCompositionViewModel()] visibility = identification.identity.preferences.postingDefaultVisibility
itemEventsSubject.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables)
allIdentitiesService.authenticatedIdentitiesPublisher() allIdentitiesService.authenticatedIdentitiesPublisher()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$authenticatedIdentities) .assign(to: &$authenticatedIdentities)
@ -43,8 +43,8 @@ public final class NewStatusViewModel: ObservableObject {
} }
public extension NewStatusViewModel { public extension NewStatusViewModel {
func viewModel(indexPath: IndexPath) -> CompositionViewModel { enum Event {
compositionViewModels[indexPath.row] case presentMediaPicker(CompositionViewModel)
} }
func setIdentity(_ identity: Identity) { func setIdentity(_ identity: Identity) {
@ -66,6 +66,34 @@ public extension NewStatusViewModel {
environment: environment) 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() { func post() {
guard let unposted = compositionViewModels.first(where: { !$0.isPosted }) else { return } guard let unposted = compositionViewModels.first(where: { !$0.isPosted }) else { return }
@ -74,35 +102,11 @@ public extension NewStatusViewModel {
} }
private 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?) { func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
loading = true loading = true
identification.service.post(statusComponents: viewModel.components(inReplyToId: inReplyToId)) identification.service.post(statusComponents: viewModel.components(
inReplyToId: inReplyToId,
visibility: visibility))
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] in .sink { [weak self] in
guard let self = self else { return } 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. // Copyright © 2020 Metabolist. All rights reserved.
import Combine import Combine
import Mastodon
import UIKit import UIKit
import ViewModels import ViewModels
class CompositionInputAccessoryView: UIView { final class CompositionInputAccessoryView: UIView {
private let stackView = UIStackView() let visibilityButton = UIButton()
let addButton = UIButton()
let contentWarningButton = UIButton(type: .system)
private let viewModel: CompositionViewModel private let viewModel: CompositionViewModel
private let parentViewModel: NewStatusViewModel
private let stackView = UIStackView()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(viewModel: CompositionViewModel) { init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
self.parentViewModel = parentViewModel
super.init(frame: .zero) super.init(frame: .zero)
@ -28,6 +35,7 @@ class CompositionInputAccessoryView: UIView {
} }
private extension CompositionInputAccessoryView { private extension CompositionInputAccessoryView {
// swiftlint:disable:next function_body_length
func initialSetup() { func initialSetup() {
autoresizingMask = .flexibleHeight autoresizingMask = .flexibleHeight
backgroundColor = .secondarySystemFill backgroundColor = .secondarySystemFill
@ -44,7 +52,12 @@ private extension CompositionInputAccessoryView {
systemName: "photo", systemName: "photo",
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
for: .normal) 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() let pollButton = UIButton()
@ -55,9 +68,26 @@ private extension CompositionInputAccessoryView {
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
for: .normal) 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) stackView.addArrangedSubview(addButton)
addButton.setImage( addButton.setImage(
@ -65,20 +95,32 @@ private extension CompositionInputAccessoryView {
systemName: "plus.circle.fill", systemName: "plus.circle.fill",
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
for: .normal) 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.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
button.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true button.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
} }
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: leadingAnchor), stackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor), stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor), stackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor) 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 Combine
import Kingfisher import Kingfisher
import UIKit import UIKit
import ViewModels
final class CompositionView: UIView { final class CompositionView: UIView {
let avatarImageView = UIImageView() let avatarImageView = UIImageView()
let spoilerTextField = UITextField()
let textView = UITextView() let textView = UITextView()
let attachmentUploadView = AttachmentUploadView() let attachmentUploadView = AttachmentUploadView()
let attachmentsCollectionView: UICollectionView let attachmentsCollectionView: UICollectionView
private var compositionConfiguration: CompositionContentConfiguration private let viewModel: CompositionViewModel
private let parentViewModel: NewStatusViewModel
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private lazy var attachmentsDataSource: CompositionAttachmentsDataSource = { private lazy var attachmentsDataSource: CompositionAttachmentsDataSource = {
CompositionAttachmentsDataSource( CompositionAttachmentsDataSource(
collectionView: attachmentsCollectionView, collectionView: attachmentsCollectionView) { [weak self] in
viewModelProvider: compositionConfiguration.viewModel.attachmentViewModel(indexPath:)) self?.viewModel.attachmentViewModel(indexPath: $0)
}
}() }()
init(configuration: CompositionContentConfiguration) { init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) {
self.compositionConfiguration = configuration self.viewModel = viewModel
self.parentViewModel = parentViewModel
let itemSize = NSCollectionLayoutSize( let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.2), widthDimension: .fractionalWidth(0.2),
@ -42,7 +47,6 @@ final class CompositionView: UIView {
super.init(frame: .zero) super.init(frame: .zero)
initialSetup() initialSetup()
applyCompositionConfiguration()
} }
@available(*, unavailable) @available(*, unavailable)
@ -51,93 +55,124 @@ final class CompositionView: UIView {
} }
} }
extension CompositionView: UIContentView { extension CompositionView {
var configuration: UIContentConfiguration { var id: CompositionViewModel.Id { viewModel.id }
get { compositionConfiguration }
set {
guard let compositionConfiguration = newValue as? CompositionContentConfiguration else { return }
self.compositionConfiguration = compositionConfiguration
applyCompositionConfiguration()
}
}
} }
extension CompositionView: UITextViewDelegate { extension CompositionView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) { func textViewDidChange(_ textView: UITextView) {
compositionConfiguration.viewModel.text = textView.text viewModel.text = textView.text
} }
} }
private extension CompositionView { private extension CompositionView {
static let attachmentUploadViewHeight: CGFloat = 100 static let attachmentUploadViewHeight: CGFloat = 100
// swiftlint:disable:next function_body_length
func initialSetup() { func initialSetup() {
tag = viewModel.id.hashValue
addSubview(avatarImageView) addSubview(avatarImageView)
avatarImageView.translatesAutoresizingMaskIntoConstraints = false avatarImageView.translatesAutoresizingMaskIntoConstraints = false
avatarImageView.layer.cornerRadius = .avatarDimension / 2 avatarImageView.layer.cornerRadius = .avatarDimension / 2
avatarImageView.clipsToBounds = true avatarImageView.clipsToBounds = true
let stackView = UIStackView() let stackView = UIStackView()
let inputAccessoryView = CompositionInputAccessoryView(viewModel: viewModel, parentViewModel: parentViewModel)
addSubview(stackView) addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical 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) stackView.addArrangedSubview(textView)
textView.backgroundColor = .secondarySystemBackground
textView.layer.cornerRadius = .defaultCornerRadius
textView.isScrollEnabled = false textView.isScrollEnabled = false
textView.adjustsFontForContentSizeCategory = true textView.adjustsFontForContentSizeCategory = true
textView.font = .preferredFont(forTextStyle: .body) textView.font = .preferredFont(forTextStyle: .body)
textView.textContainer.lineFragmentPadding = 0 // textView.textContainer.lineFragmentPadding = 0
textView.inputAccessoryView = CompositionInputAccessoryView(viewModel: compositionConfiguration.viewModel) textView.inputAccessoryView = inputAccessoryView
textView.inputAccessoryView?.sizeToFit() textView.inputAccessoryView?.sizeToFit()
textView.delegate = self textView.delegate = self
textView.setContentHuggingPriority(.required, for: .vertical)
stackView.addArrangedSubview(attachmentsCollectionView) stackView.addArrangedSubview(attachmentsCollectionView)
attachmentsCollectionView.dataSource = attachmentsDataSource attachmentsCollectionView.dataSource = attachmentsDataSource
stackView.addArrangedSubview(attachmentUploadView) stackView.addArrangedSubview(attachmentUploadView)
let constraints = [ textView.text = viewModel.text
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension), spoilerTextField.text = viewModel.contentWarning
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)
]
for constraint in constraints { viewModel.$displayContentWarning
constraint.priority = .justBelowMax .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() { parentViewModel.$identification.map(\.identity.image)
cancellables.removeAll()
compositionConfiguration.viewModel.$identification.map(\.identity.image)
.sink { [weak self] in self?.avatarImageView.kf.setImage(with: $0) } .sink { [weak self] in self?.avatarImageView.kf.setImage(with: $0) }
.store(in: &cancellables) .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 .sink { [weak self] in
self?.attachmentsDataSource.apply([$0.map(\.attachment)].snapshot()) self?.attachmentsDataSource.apply([$0.map(\.attachment)].snapshot())
self?.attachmentsCollectionView.isHidden = $0.isEmpty self?.attachmentsCollectionView.isHidden = $0.isEmpty
} }
.store(in: &cancellables) .store(in: &cancellables)
compositionConfiguration.viewModel.$attachmentUpload viewModel.$attachmentUpload
.sink { [weak self] in self?.attachmentUploadView.attachmentUpload = $0 } .sink { [weak self] in self?.attachmentUploadView.attachmentUpload = $0 }
.store(in: &cancellables) .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 let viewModelClosure: () -> NewStatusViewModel
func makeUIViewController(context: Context) -> NewStatusViewController { func makeUIViewController(context: Context) -> NewStatusViewController {
NewStatusViewController(viewModel: viewModelClosure(), isShareExtension: false) NewStatusViewController(viewModel: viewModelClosure())
} }
func updateUIViewController(_ uiViewController: NewStatusViewController, context: Context) { 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() EmptyView()
.fullScreenCover(isPresented: $viewModel.presentingNewStatus) { .fullScreenCover(isPresented: $viewModel.presentingNewStatus) {
NavigationView { NavigationView {
NewStatusView { NewStatusView { rootViewModel.newStatusViewModel(identification: viewModel.identification) }
rootViewModel.newStatusViewModel(identification: viewModel.identification)
}
.edgesIgnoringSafeArea(.all)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
.navigationViewStyle(StackNavigationViewStyle()) .navigationViewStyle(StackNavigationViewStyle())