From 252c58ad2c248bc73ef81f445c4affd64e3a4fdf Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 17:32:42 +0800 Subject: [PATCH 01/10] fix: remove section inset hacking and set header footer with overflow layout constraint --- Mastodon/Diffiable/Section/ProfileFieldSection.swift | 8 -------- .../Profile/Header/ProfileHeaderViewController.swift | 8 -------- .../View/ProfileFieldCollectionViewHeaderFooterView.swift | 5 +++-- .../Scene/Profile/Header/View/ProfileHeaderView.swift | 5 +++-- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Mastodon/Diffiable/Section/ProfileFieldSection.swift b/Mastodon/Diffiable/Section/ProfileFieldSection.swift index 0b67b00a7..82f88f657 100644 --- a/Mastodon/Diffiable/Section/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Section/ProfileFieldSection.swift @@ -28,10 +28,6 @@ extension ProfileFieldSection { case .field(let field, let attribute): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell - let margin = max(0, collectionView.frame.width - collectionView.readableContentGuide.layoutFrame.width) - cell.containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - cell.separatorLineToMarginLeadingLayoutConstraint.constant = margin - // set key cell.fieldView.titleActiveLabel.configure(field: field.name.value, emojiDict: attribute.emojiDict.value) cell.fieldView.titleTextField.text = field.name.value @@ -99,10 +95,6 @@ extension ProfileFieldSection { case .addEntry(let attribute): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldAddEntryCollectionViewCell.self), for: indexPath) as! ProfileFieldAddEntryCollectionViewCell - let margin = max(0, collectionView.frame.width - collectionView.readableContentGuide.layoutFrame.width) - cell.containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - cell.separatorLineToMarginLeadingLayoutConstraint.constant = margin - cell.bottomSeparatorLine.isHidden = attribute.isLast cell.delegate = profileFieldAddEntryCollectionViewCellDelegate diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 0de6f47e3..c3495b7f1 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -228,14 +228,6 @@ extension ProfileHeaderViewController { super.viewDidAppear(animated) viewModel.viewDidAppear.value = true - - // Deprecated: - // not needs this tweak due to force layout update in the parent - // if !isAdjustBannerImageViewForSafeAreaInset { - // isAdjustBannerImageViewForSafeAreaInset = true - // profileHeaderView.bannerImageView.frame.origin.y = -containerSafeAreaInset.top - // profileHeaderView.bannerImageView.frame.size.height += containerSafeAreaInset.top - // } } override func viewDidLayoutSubviews() { diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift index be61691c0..83fec9bcf 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift @@ -32,8 +32,9 @@ extension ProfileFieldCollectionViewHeaderFooterView { addSubview(separatorLine) NSLayoutConstraint.activate([ separatorLine.topAnchor.constraint(equalTo: topAnchor), - separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor), - separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor), + // workaround SDK supplementariesFollowContentInsets not works issue + separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -9999), + separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 9999), separatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), ]) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 89859c982..54c57139d 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -159,13 +159,14 @@ final class ProfileHeaderView: UIView { let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) - // note: manually set layout inset to workaround header footer layout issue - // section.contentInsetsReference = .readableContent + section.contentInsetsReference = .readableContent let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(1)) let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerFooterSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerFooterSize, elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom) section.boundarySupplementaryItems = [header, footer] + // note: toggle this not take effect + // section.supplementariesFollowContentInsets = false return UICollectionViewCompositionalLayout(section: section) } From 90d8de1625da0ae694cf4912fd0aa27420be0c3c Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 28 May 2021 15:55:58 +0800 Subject: [PATCH 02/10] fix: compose scene memory leaking issue --- .../Section/ComposeStatusSection.swift | 23 ++++++++++++------- ...seStatusAttachmentCollectionViewCell.swift | 4 ++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 0940ddfa4..46d00dbef 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -101,21 +101,24 @@ extension ComposeStatusSection { cell.composeContent .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { text in + .sink { [weak collectionView] text in + guard let collectionView = collectionView else { return } // self size input cell // needs restore content offset to resolve issue #83 let oldContentOffset = collectionView.contentOffset collectionView.collectionViewLayout.invalidateLayout() collectionView.layoutIfNeeded() collectionView.contentOffset = oldContentOffset - + // bind input data attribute.composeContent.value = text } .store(in: &cell.disposeBag) attribute.isContentWarningComposing .receive(on: DispatchQueue.main) - .sink { isContentWarningComposing in + .sink { [weak cell, weak collectionView] isContentWarningComposing in + guard let cell = cell else { return } + guard let collectionView = collectionView else { return } // self size input cell collectionView.collectionViewLayout.invalidateLayout() cell.statusContentWarningEditorView.containerView.isHidden = !isContentWarningComposing @@ -130,7 +133,8 @@ extension ComposeStatusSection { cell.contentWarningContent .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { text in + .sink { [weak collectionView] text in + guard let collectionView = collectionView else { return } // self size input cell collectionView.collectionViewLayout.invalidateLayout() // bind input data @@ -145,9 +149,10 @@ extension ComposeStatusSection { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value cell.delegate = composeStatusAttachmentTableViewCellDelegate - attachmentService.imageData + attachmentService.data .receive(on: DispatchQueue.main) - .sink { imageData in + .sink { [weak cell] imageData in + guard let cell = cell else { return } let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) guard let imageData = imageData, let image = UIImage(data: imageData) else { @@ -171,7 +176,8 @@ extension ComposeStatusSection { attachmentService.error.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) - .sink { uploadState, error in + .sink { [weak cell] uploadState, error in + guard let cell = cell else { return } cell.attachmentContainerView.emptyStateView.isHidden = error == nil cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil if let _ = error { @@ -220,7 +226,8 @@ extension ComposeStatusSection { cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal) attribute.expiresOption .receive(on: DispatchQueue.main) - .sink { expiresOption in + .sink { [weak cell] expiresOption in + guard let cell = cell else { return } cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal) } .store(in: &cell.disposeBag) diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift index 87fe0efaf..0fa9319d3 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift @@ -57,6 +57,10 @@ final class ComposeStatusAttachmentCollectionViewCell: UICollectionViewCell { _init() } + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + } extension ComposeStatusAttachmentCollectionViewCell { From a9744146cefaf57e4b8ad0c672a33936fee5d9ca Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 16:42:49 +0800 Subject: [PATCH 03/10] feat: add video attachment post supports --- Localization/app.json | 6 +- Mastodon.xcodeproj/project.pbxproj | 236 +++++++++--------- .../xcschemes/xcschememanagement.plist | 4 +- .../Section/ComposeStatusSection.swift | 25 +- Mastodon/Generated/Strings.swift | 6 + .../Resources/ar.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 2 + .../Scene/Compose/ComposeViewController.swift | 38 ++- Mastodon/Scene/Compose/ComposeViewModel.swift | 55 ++++ ...tachmentContainerView+EmptyStateView.swift | 2 + ...meTimelineViewController+DebugAction.swift | 10 + .../Header/ProfileHeaderViewController.swift | 4 +- ...astodonAttachmentService+UploadState.swift | 12 +- .../MastodonAttachmentService.swift | 69 ++++- Mastodon/Vender/PHPickerResultLoader.swift | 35 ++- .../MastodonSDK/API/Mastodon+API+Media.swift | 78 +++++- .../Sources/MastodonSDK/Extension/Data.swift | 7 +- .../MastodonSDK/Query/MediaAttachment.swift | 19 +- .../Query/MultipartFormValue.swift | 5 + .../MastodonSDK/Query/SerialStream.swift | 147 +++++++++++ Podfile | 17 +- Podfile.lock | 6 +- 22 files changed, 606 insertions(+), 179 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift diff --git a/Localization/app.json b/Localization/app.json index 920ec0d27..69d513600 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -21,7 +21,11 @@ }, "publish_post_failure": { "title": "Publish Failure", - "message": "Failed to publish the post.\nPlease check your internet connection." + "message": "Failed to publish the post.\nPlease check your internet connection.", + "attchments_message": { + "video_attach_with_photo": "Cannot attach a video to a status that already contains images.", + "more_than_one_video": "Cannot attach more than one video." + } }, "sign_out": { "title": "Sign out", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 28f113616..b1aaa14b4 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -67,7 +67,7 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; - 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; + 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */; }; 2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; }; @@ -80,7 +80,7 @@ 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; - 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; }; + 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */; }; 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */; }; 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; @@ -90,7 +90,7 @@ 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; }; 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; + 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; @@ -116,7 +116,7 @@ 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; - 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* CropViewController */; }; + 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */; }; 2D9DB969263A833E007C1D71 /* DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB968263A833E007C1D71 /* DomainBlock.swift */; }; @@ -164,7 +164,7 @@ 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; }; 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; - 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; + 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */; }; 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */; }; 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; @@ -178,14 +178,13 @@ 5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; - 7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; - D86919F5080C3F228CCD17D1 /* Pods_Mastodon_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */; }; - DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; + B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */; }; + DB00CA972632DDB600A54956 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */; }; DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; - DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; + DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; @@ -217,7 +216,7 @@ DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; }; DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; }; DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; }; - DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; + DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; @@ -258,7 +257,7 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; + DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; @@ -294,7 +293,7 @@ DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; }; DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; }; - DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; }; + DB6805102637D0F800430867 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* SwiftPackageProductDependency */; }; DB6805262637D7DD00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; }; @@ -307,7 +306,7 @@ DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; }; DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; }; DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; }; - DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; }; + DB6D9F42263527CE008423CD /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */; }; DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4826353FD6008423CD /* Subscription.swift */; }; DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */; }; DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */; }; @@ -318,8 +317,8 @@ DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; }; DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; }; DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */; }; - DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; }; - DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; }; + DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; }; @@ -383,7 +382,7 @@ DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; }; DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */; }; DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; }; - DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; }; + DB9A487E2603456B008B817C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* SwiftPackageProductDependency */; }; DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; }; DB9A489026035963008B817C /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; }; DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */; }; @@ -417,7 +416,7 @@ DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; + DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; }; @@ -464,10 +463,11 @@ DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; }; DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; }; DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; }; + DBF8AE862632992800C9C23C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */; }; DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; }; DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */; }; DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */; }; + EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -552,7 +552,7 @@ files = ( DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */, DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */, - DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */, + DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -704,10 +704,10 @@ 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Searching.swift"; sourceTree = ""; }; 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingTableViewCell.swift; sourceTree = ""; }; 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; + 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = ""; }; - 46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = ""; }; 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Diffable.swift"; sourceTree = ""; }; 5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = ""; }; @@ -741,9 +741,11 @@ 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = ""; }; 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = ""; }; 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = ""; }; - 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = ""; }; + 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = ""; }; + 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = ""; }; 9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = ""; }; + 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.debug.xcconfig"; sourceTree = ""; }; A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.release.xcconfig"; sourceTree = ""; }; B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.release.xcconfig"; sourceTree = ""; }; @@ -1036,6 +1038,8 @@ DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewHeaderFooterView.swift; sourceTree = ""; }; DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldAddEntryCollectionViewCell.swift; sourceTree = ""; }; EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = ""; }; + ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.release.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.release.xcconfig"; sourceTree = ""; }; + F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1043,20 +1047,20 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */, - DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, + DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */, + DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, - 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, - DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */, - 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, - 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, + 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */, + DB9A487E2603456B008B817C /* BuildFile in Frameworks */, + 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */, + DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */, + 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, + DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, - DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, - 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, + 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */, + DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */, + 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */, ); @@ -1083,8 +1087,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6805102637D0F800430867 /* KeychainAccess in Frameworks */, - D86919F5080C3F228CCD17D1 /* Pods_Mastodon_AppShared.framework in Frameworks */, + DB6805102637D0F800430867 /* BuildFile in Frameworks */, + EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1108,11 +1112,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */, - DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */, - DBF8AE862632992800C9C23C /* Base85 in Frameworks */, + DB00CA972632DDB600A54956 /* BuildFile in Frameworks */, + DB6D9F42263527CE008423CD /* BuildFile in Frameworks */, + DBF8AE862632992800C9C23C /* BuildFile in Frameworks */, DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */, - 7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */, + B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1199,6 +1203,10 @@ B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */, D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */, B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */, + 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */, + ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */, + 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */, + 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -1544,8 +1552,8 @@ A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */, 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */, 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */, - 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */, - 46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */, + F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */, + 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */, ); name = Frameworks; sourceTree = ""; @@ -2489,17 +2497,17 @@ ); name = Mastodon; packageProductDependencies = ( - DB3D0FF225BAA61700EAA174 /* AlamofireImage */, - 5D526FE125BE9AC400460CB9 /* MastodonSDK */, - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, - 2D42FF6025C8177C004A627A /* ActiveLabel */, - DB0140BC25C40D7500F9F3CF /* CommonOSLog */, - DB5086B725CC0D6400C2C187 /* Kingfisher */, - 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, - 2D939AC725EE14620076FA61 /* CropViewController */, - DB9A487D2603456B008B817C /* UITextView+Placeholder */, - DBB525072611EAC0002F1F29 /* Tabman */, - DB6F5E31264E7410009108F4 /* TwitterTextEditor */, + DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */, + 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */, + 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */, + 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */, + DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */, + DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */, + 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */, + 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */, + DB9A487D2603456B008B817C /* SwiftPackageProductDependency */, + DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */, + DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -2560,7 +2568,7 @@ ); name = AppShared; packageProductDependencies = ( - DB68050F2637D0F800430867 /* KeychainAccess */, + DB68050F2637D0F800430867 /* SwiftPackageProductDependency */, ); productName = AppShared; productReference = DB68047F2637CD4C00430867 /* AppShared.framework */; @@ -2620,9 +2628,9 @@ ); name = NotificationService; packageProductDependencies = ( - DBF8AE852632992800C9C23C /* Base85 */, - DB00CA962632DDB600A54956 /* CommonOSLog */, - DB6D9F41263527CE008423CD /* AlamofireImage */, + DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */, + DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */, + DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */, ); productName = NotificationService; productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */; @@ -2677,18 +2685,18 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, - 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, - 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, - 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, - DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, - DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */, - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, - DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, + DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */, + 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */, + 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */, + DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */, + DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */, + 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */, + 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */, + DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */, + DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */, + DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */, + DB6804722637CC1200430867 /* RemoteSwiftPackageReference */, + DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -2780,7 +2788,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Mastodon-NotificationService-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-NotificationService-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -2863,7 +2871,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Mastodon-AppShared-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-AppShared-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -3746,7 +3754,7 @@ }; DB6804892637CD4C00430867 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */; + baseConfigurationReference = 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; @@ -3777,7 +3785,7 @@ }; DB68048A2637CD4C00430867 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */; + baseConfigurationReference = ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; @@ -3904,7 +3912,7 @@ }; DBF8AE1C263293E400C9C23C /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */; + baseConfigurationReference = 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; @@ -3927,7 +3935,7 @@ }; DBF8AE1D263293E400C9C23C /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */; + baseConfigurationReference = 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; @@ -4026,7 +4034,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = { + 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { @@ -4034,7 +4042,7 @@ version = 5.0.2; }; }; - 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { + 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git"; requirement = { @@ -4042,7 +4050,7 @@ minimumVersion = 1.7.1; }; }; - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */ = { + 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; requirement = { @@ -4050,7 +4058,7 @@ minimumVersion = 3.1.0; }; }; - 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */ = { + 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TimOliver/TOCropViewController.git"; requirement = { @@ -4058,7 +4066,7 @@ minimumVersion = 2.6.0; }; }; - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { + DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/CommonOSLog"; requirement = { @@ -4066,7 +4074,7 @@ minimumVersion = 0.1.1; }; }; - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { + DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; requirement = { @@ -4074,7 +4082,7 @@ minimumVersion = 4.1.0; }; }; - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { @@ -4082,7 +4090,7 @@ minimumVersion = 6.1.0; }; }; - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + DB6804722637CC1200430867 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; requirement = { @@ -4090,7 +4098,7 @@ minimumVersion = 4.2.2; }; }; - DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = { + DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/TwitterTextEditor.git"; requirement = { @@ -4098,7 +4106,7 @@ kind = branch; }; }; - DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = { + DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder"; requirement = { @@ -4106,7 +4114,7 @@ minimumVersion = 1.4.1; }; }; - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = { + DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/uias/Tabman"; requirement = { @@ -4114,7 +4122,7 @@ minimumVersion = 2.11.0; }; }; - DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */ = { + DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/Base85.git"; requirement = { @@ -4125,78 +4133,78 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2D42FF6025C8177C004A627A /* ActiveLabel */ = { + 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */; + package = 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */; productName = ActiveLabel; }; - 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = { + 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */; + package = 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */; productName = ThirdPartyMailer; }; - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */ = { + 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; + package = 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */; productName = AlamofireNetworkActivityIndicator; }; - 2D939AC725EE14620076FA61 /* CropViewController */ = { + 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */; + package = 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */; productName = CropViewController; }; - 5D526FE125BE9AC400460CB9 /* MastodonSDK */ = { + 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB00CA962632DDB600A54956 /* CommonOSLog */ = { + DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; + package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; productName = CommonOSLog; }; - DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { + DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; + package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; productName = CommonOSLog; }; - DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { + DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; + package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; productName = AlamofireImage; }; - DB5086B725CC0D6400C2C187 /* Kingfisher */ = { + DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; + package = DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */; productName = Kingfisher; }; - DB68050F2637D0F800430867 /* KeychainAccess */ = { + DB68050F2637D0F800430867 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; + package = DB6804722637CC1200430867 /* RemoteSwiftPackageReference */; productName = KeychainAccess; }; - DB6D9F41263527CE008423CD /* AlamofireImage */ = { + DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; + package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; productName = AlamofireImage; }; - DB6F5E31264E7410009108F4 /* TwitterTextEditor */ = { + DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; + package = DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */; productName = TwitterTextEditor; }; - DB9A487D2603456B008B817C /* UITextView+Placeholder */ = { + DB9A487D2603456B008B817C /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; + package = DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */; productName = "UITextView+Placeholder"; }; - DBB525072611EAC0002F1F29 /* Tabman */ = { + DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */; + package = DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */; productName = Tabman; }; - DBF8AE852632992800C9C23C /* Base85 */ = { + DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */; + package = DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */; productName = Base85; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 326857269..217fe1993 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 14 + 15 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 15 + 16 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 46d00dbef..a1e6170e8 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -149,13 +149,12 @@ extension ComposeStatusSection { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value cell.delegate = composeStatusAttachmentTableViewCellDelegate - attachmentService.data + attachmentService.thumbnailImage .receive(on: DispatchQueue.main) - .sink { [weak cell] imageData in + .sink { [weak cell] thumbnailImage in guard let cell = cell else { return } let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) - guard let imageData = imageData, - let image = UIImage(data: imageData) else { + guard let image = thumbnailImage else { let placeholder = UIImage.placeholder( size: size, color: Asset.Colors.Background.systemGroupedBackground.color @@ -176,18 +175,32 @@ extension ComposeStatusSection { attachmentService.error.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) - .sink { [weak cell] uploadState, error in + .sink { [weak cell, weak attachmentService] uploadState, error in guard let cell = cell else { return } + guard let attachmentService = attachmentService else { return } cell.attachmentContainerView.emptyStateView.isHidden = error == nil cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil - if let _ = error { + if let error = error { cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription } else { guard let uploadState = uploadState else { return } switch uploadState { case is MastodonAttachmentService.UploadState.Finish, is MastodonAttachmentService.UploadState.Fail: cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.emptyStateView.label.text = { + if let file = attachmentService.file.value { + switch file { + case .jpeg, .png, .gif: + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) + case .other: + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) + } + } else { + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) + } + }() default: break } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index de8403ac0..5790f7134 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -44,6 +44,12 @@ internal enum L10n { internal static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message") /// Publish Failure internal static let title = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Title") + internal enum AttchmentsMessage { + /// Cannot attach more than one video. + internal static let moreThanOneVideo = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo") + /// Cannot attach a video to a status that already contains images. + internal static let videoAttachWithPhoto = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto") + } } internal enum SavePhotoFailure { /// Please enable photo libaray access permission to save photo. diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 80fce62fa..81bf772e1 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -6,6 +6,8 @@ "Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?"; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status that already contains images."; "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. Please check your internet connection."; "Common.Alerts.PublishPostFailure.Title" = "Publish Failure"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 80fce62fa..81bf772e1 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -6,6 +6,8 @@ "Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?"; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status that already contains images."; "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. Please check your internet connection."; "Common.Alerts.PublishPostFailure.Title" = "Publish Failure"; diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 80a64064b..2e3e5fe7c 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -75,12 +75,15 @@ final class ComposeViewController: UIViewController, NeedsDependency { var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! let composeToolbarBackgroundView = UIView() - private(set) lazy var imagePicker: PHPickerViewController = { + static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration { var configuration = PHPickerConfiguration() - configuration.filter = .images - configuration.selectionLimit = 4 - - let imagePicker = PHPickerViewController(configuration: configuration) + configuration.filter = .any(of: [.images, .videos]) + configuration.selectionLimit = selectionLimit + return configuration + } + + private(set) lazy var photoLibraryPicker: PHPickerViewController = { + let imagePicker = PHPickerViewController(configuration: ComposeViewController.createPhotoLibraryPickerConfiguration()) imagePicker.delegate = self return imagePicker }() @@ -567,12 +570,9 @@ extension ComposeViewController { } private func resetImagePicker() { - var configuration = PHPickerConfiguration() - configuration.filter = .images let selectionLimit = max(1, 4 - viewModel.attachmentServices.value.count) - configuration.selectionLimit = selectionLimit - - imagePicker = createImagePicker(configuration: configuration) + let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit) + photoLibraryPicker = createImagePicker(configuration: configuration) } private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController { @@ -610,6 +610,16 @@ extension ComposeViewController { @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + do { + try viewModel.checkAttachmentPrecondition() + } catch { + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) + return + } + guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else { // TODO: handle error return @@ -913,7 +923,7 @@ extension ComposeViewController: ComposeToolbarViewDelegate { func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) { switch type { case .photoLibrary: - present(imagePicker, animated: true, completion: nil) + present(photoLibraryPicker, animated: true, completion: nil) case .camera: present(imagePickerController, animated: true, completion: nil) case .browse: @@ -1120,8 +1130,12 @@ extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelega var attachmentServices = viewModel.attachmentServices.value guard let index = attachmentServices.firstIndex(of: attachmentService) else { return } + let removedItem = attachmentServices[index] attachmentServices.remove(at: index) viewModel.attachmentServices.value = attachmentServices + + // cancel task + removedItem.disposeBag.removeAll() } } @@ -1365,7 +1379,7 @@ extension ComposeViewController { case .mediaBrowse: present(documentPickerController, animated: true, completion: nil) case .mediaPhotoLibrary: - present(imagePicker, animated: true, completion: nil) + present(photoLibraryPicker, animated: true, completion: nil) case .mediaCamera: guard UIImagePickerController.isSourceTypeAvailable(.camera) else { return diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 097dd9fdd..0ac124586 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -291,6 +291,8 @@ final class ComposeViewModel { ) .receive(on: DispatchQueue.main) .sink { [weak self] attachmentServices, isPollComposing, pollAttributes in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: trigger attachments upload…", ((#file as NSString).lastPathComponent), #line, #function) + guard let self = self else { return } guard let diffableDataSource = self.diffableDataSource else { return } var snapshot = diffableDataSource.snapshot() @@ -405,6 +407,59 @@ extension ComposeViewModel { } } +extension ComposeViewModel { + + enum AttachmentPrecondition: Error, LocalizedError { + case videoAttachWithPhoto + case moreThanOneVideo + + var errorDescription: String? { + return L10n.Common.Alerts.PublishPostFailure.title + } + + var failureReason: String? { + switch self { + case .videoAttachWithPhoto: + return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.videoAttachWithPhoto + case .moreThanOneVideo: + return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.moreThanOneVideo + } + } + } + + // check exclusive limit: + // - up to 1 video + // - up to 4 photos + func checkAttachmentPrecondition() throws { + let attachmentServices = self.attachmentServices.value + guard !attachmentServices.isEmpty else { return } + var photoAttachmentServices: [MastodonAttachmentService] = [] + var videoAttachmentServices: [MastodonAttachmentService] = [] + attachmentServices.forEach { service in + guard let file = service.file.value else { + assertionFailure() + return + } + switch file { + case .jpeg, .png, .gif: + photoAttachmentServices.append(service) + case .other: + videoAttachmentServices.append(service) + } + } + + if !videoAttachmentServices.isEmpty { + guard videoAttachmentServices.count == 1 else { + throw AttachmentPrecondition.moreThanOneVideo + } + guard photoAttachmentServices.isEmpty else { + throw AttachmentPrecondition.videoAttachWithPhoto + } + } + } + +} + // MARK: - MastodonAttachmentServiceDelegate extension ComposeViewModel: MastodonAttachmentServiceDelegate { func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) { diff --git a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift index 353fe7497..ca45e33d5 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift @@ -29,6 +29,8 @@ extension AttachmentContainerView { label.textAlignment = .center label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) label.numberOfLines = 2 + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.3 return label }() diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 152bb62f0..69eff4a82 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -11,6 +11,8 @@ import CoreData import CoreDataStack #if DEBUG +import FLEX + extension HomeTimelineViewController { var debugMenu: UIMenu { let menu = UIMenu( @@ -19,6 +21,10 @@ extension HomeTimelineViewController { identifier: nil, options: .displayInline, children: [ + UIAction(title: "Show FLEX", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.showFLEXAction(action) + }), moveMenu, dropMenu, UIAction(title: "Show Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in @@ -115,6 +121,10 @@ extension HomeTimelineViewController { extension HomeTimelineViewController { + @objc private func showFLEXAction(_ sender: UIAction) { + FLEXManager.shared.showExplorer() + } + @objc private func moveToTopGapAction(_ sender: UIAction) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index c3495b7f1..fbc8be624 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -448,9 +448,9 @@ extension ProfileHeaderViewController: PHPickerViewControllerDelegate { case .finished: break } - } receiveValue: { [weak self] imageData in + } receiveValue: { [weak self] file in guard let self = self else { return } - guard let imageData = imageData else { return } + guard let imageData = file?.data else { return } guard let image = UIImage(data: imageData) else { return } self.cropImage(image: image, pickerViewController: picker) } diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift index 9fd4b1298..23233ec3f 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift @@ -35,7 +35,7 @@ extension MastodonAttachmentService.UploadState { return true } - if service?.imageData.value != nil { + if service?.file.value != nil { return stateClass == Uploading.self } else { return stateClass == Fail.self @@ -53,15 +53,8 @@ extension MastodonAttachmentService.UploadState { guard let service = service, let stateMachine = stateMachine else { return } guard let authenticationBox = service.authenticationBox else { return } - guard let imageData = service.imageData.value else { return } + guard let file = service.file.value else { return } - let file: Mastodon.Query.MediaAttachment = { - if imageData.kf.imageFormat == .PNG { - return .png(imageData) - } else { - return .jpeg(imageData) - } - }() let description = service.description.value let query = Mastodon.API.Media.UploadMeidaQuery( file: file, @@ -81,6 +74,7 @@ extension MastodonAttachmentService.UploadState { case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) service.error.send(error) + stateMachine.enter(Fail.self) case .finished: os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment success", ((#file as NSString).lastPathComponent), #line, #function) break diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index fd95d2634..4b843ef40 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -5,11 +5,13 @@ // Created by MainasuK Cirno on 2021-3-17. // +import os.log import UIKit import Combine import PhotosUI import Kingfisher import GameplayKit +import MobileCoreServices import MastodonSDK protocol MastodonAttachmentServiceDelegate: AnyObject { @@ -26,12 +28,12 @@ final class MastodonAttachmentService { // input let context: AppContext var authenticationBox: AuthenticationService.MastodonAuthenticationBox? + let file = CurrentValueSubject(nil) + let description = CurrentValueSubject(nil) // output - // TODO: handle video/GIF/Audio data - let imageData = CurrentValueSubject(nil) + let thumbnailImage = CurrentValueSubject(nil) let attachment = CurrentValueSubject(nil) - let description = CurrentValueSubject(nil) let error = CurrentValueSubject(nil) private(set) lazy var uploadStateMachine: GKStateMachine = { @@ -58,7 +60,16 @@ final class MastodonAttachmentService { setupServiceObserver() - PHPickerResultLoader.loadImageData(from: pickerResult) + Just(pickerResult) + .flatMap { result -> AnyPublisher in + if result.itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) { + return PHPickerResultLoader.loadImageData(from: result).eraseToAnyPublisher() + } + if result.itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) { + return PHPickerResultLoader.loadVideoData(from: result).eraseToAnyPublisher() + } + return Fail(error: AttachmentError.invalidAttachmentType).eraseToAnyPublisher() + } .sink { [weak self] completion in guard let self = self else { return } switch completion { @@ -68,12 +79,42 @@ final class MastodonAttachmentService { case .finished: break } - } receiveValue: { [weak self] imageData in + } receiveValue: { [weak self] file in guard let self = self else { return } - self.imageData.value = imageData + self.file.value = file self.uploadStateMachine.enter(UploadState.Initial.self) } .store(in: &disposeBag) + + file + .map { file -> UIImage? in + guard let file = file else { + return nil + } + + switch file { + case .jpeg(let data), .png(let data): + return data.flatMap { UIImage(data: $0) } + case .gif: + // TODO: + return nil + case .other(let url, _, _): + guard let url = url, FileManager.default.fileExists(atPath: url.path) else { return nil } + let asset = AVURLAsset(url: url) + let assetImageGenerator = AVAssetImageGenerator(asset: asset) + assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation + do { + let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) + let image = UIImage(cgImage: cgImage) + return image + } catch { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: thumbnail generate fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + return nil + } + } + } + .assign(to: \.value, on: thumbnailImage) + .store(in: &disposeBag) } init( @@ -87,7 +128,7 @@ final class MastodonAttachmentService { setupServiceObserver() - imageData.value = image.jpegData(compressionQuality: 0.75) + file.value = .jpeg(image.jpegData(compressionQuality: 0.75)) uploadStateMachine.enter(UploadState.Initial.self) } @@ -102,7 +143,7 @@ final class MastodonAttachmentService { setupServiceObserver() - self.imageData.value = imageData + self.file.value = .jpeg(imageData) uploadStateMachine.enter(UploadState.Initial.self) } @@ -115,6 +156,18 @@ final class MastodonAttachmentService { .store(in: &disposeBag) } + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension MastodonAttachmentService { + enum AttachmentError: Error { + case invalidAttachmentType + case attachmentTooLarge + } + } extension MastodonAttachmentService { diff --git a/Mastodon/Vender/PHPickerResultLoader.swift b/Mastodon/Vender/PHPickerResultLoader.swift index 7e083001c..88a9a7554 100644 --- a/Mastodon/Vender/PHPickerResultLoader.swift +++ b/Mastodon/Vender/PHPickerResultLoader.swift @@ -10,12 +10,13 @@ import Foundation import Combine import MobileCoreServices import PhotosUI +import MastodonSDK // load image with low memory usage // Refs: https://christianselig.com/2020/09/phpickerviewcontroller-efficiently/ enum PHPickerResultLoader { - static func loadImageData(from result: PHPickerResult) -> Future { + static func loadImageData(from result: PHPickerResult) -> Future { Future { promise in result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, error in if let error = error { @@ -64,7 +65,37 @@ enum PHPickerResultLoader { let dataSize = ByteCountFormatter.string(fromByteCount: Int64(data.length), countStyle: .memory) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: load image %s", ((#file as NSString).lastPathComponent), #line, #function, dataSize) - promise(.success(data as Data)) + let file = Mastodon.Query.MediaAttachment.jpeg(data as Data) + promise(.success(file)) + } + } + } + + static func loadVideoData(from result: PHPickerResult) -> Future { + Future { promise in + result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in + if let error = error { + promise(.failure(error)) + return + } + + guard let url = url else { + promise(.success(nil)) + return + } + + let fileName = UUID().uuidString + let tempDirectoryURL = FileManager.default.temporaryDirectory + let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension) + do { + try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil) + try FileManager.default.copyItem(at: url, to: fileURL) + let file = Mastodon.Query.MediaAttachment.other(fileURL, fileExtension: fileURL.pathExtension, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4") + promise(.success(file)) + } catch { + promise(.failure(error)) + } + } } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift index 5ae344b3d..0918cbd07 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift @@ -42,11 +42,17 @@ extension Mastodon.API.Media { authorization: authorization ) request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment + let serialStream = query.serialStream + request.httpBodyStream = serialStream.boundStreams.input return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } + .handleEvents(receiveCancel: { + // retain and handle cancel task + serialStream.boundStreams.output.close() + }) .eraseToAnyPublisher() } @@ -73,15 +79,30 @@ extension Mastodon.API.Media { } var body: Data? { - var data = Data() - - file.flatMap { data.append(Data.multipart(key: "file", value: $0)) } - thumbnail.flatMap { data.append(Data.multipart(key: "thumbnail", value: $0)) } - description.flatMap { data.append(Data.multipart(key: "description", value: $0)) } - focus.flatMap { data.append(Data.multipart(key: "focus", value: $0)) } + // using stream data + return nil + } + + var serialStream: SerialStream { + var streams: [InputStream] = [] - data.append(Data.multipartEnd()) - return data + file.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "file", value: value))) + value.multipartStreamValue.flatMap { streams.append($0) } + } + thumbnail.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "thumbnail", value: value))) + value.multipartStreamValue.flatMap { streams.append($0) } + } + description.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "description", value: value))) + } + focus.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "focus", value: value))) + } + streams.append(InputStream(data: Data.multipartEnd())) + + return SerialStream(streams: streams) } } @@ -129,8 +150,45 @@ extension Mastodon.API.Media { } .eraseToAnyPublisher() } - - public typealias UpdateMediaQuery = UploadMeidaQuery + + public struct UpdateMediaQuery: PutQuery { + + public let file: Mastodon.Query.MediaAttachment? + public let thumbnail: Mastodon.Query.MediaAttachment? + public let description: String? + public let focus: String? + + public init( + file: Mastodon.Query.MediaAttachment?, + thumbnail: Mastodon.Query.MediaAttachment?, + description: String?, + focus: String? + ) { + self.file = file + self.thumbnail = thumbnail + self.description = description + self.focus = focus + } + + var contentType: String? { + return Self.multipartContentType() + } + + var queryItems: [URLQueryItem]? { + return nil + } + + var body: Data? { + var data = Data() + + // not modify uploaded binary data + description.flatMap { data.append(Data.multipart(key: "description", value: $0)) } + focus.flatMap { data.append(Data.multipart(key: "focus", value: $0)) } + + data.append(Data.multipartEnd()) + return data + } + } } diff --git a/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift index 48e442b9d..35c66176d 100644 --- a/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift +++ b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift @@ -26,7 +26,12 @@ extension Data { data.append("Content-Type: \(contentType)\r\n".data(using: .utf8)!) } data.append("\r\n".data(using: .utf8)!) - data.append(value.multipartValue) + if value.multipartStreamValue == nil { + data.append(value.multipartValue) + } else { + // needs append stream multipart value outside + // seealso: SerialStream + } return data } diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift index f3bd88832..ca9388cac 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift @@ -16,21 +16,22 @@ extension Mastodon.Query { /// PNG (Portable Network Graphics) image case png(Data?) /// Other media file - case other(Data?, fileExtension: String, mimeType: String) + /// e.g video + case other(URL?, fileExtension: String, mimeType: String) } } extension Mastodon.Query.MediaAttachment { - var data: Data? { + public var data: Data? { switch self { case .jpeg(let data): return data case .gif(let data): return data case .png(let data): return data - case .other(let data, _, _): return data + case .other: return nil } } - var fileName: String { + public var fileName: String { let name = UUID().uuidString switch self { case .jpeg: return "\(name).jpg" @@ -40,7 +41,7 @@ extension Mastodon.Query.MediaAttachment { } } - var mimeType: String { + public var mimeType: String { switch self { case .jpeg: return "image/jpg" case .gif: return "image/gif" @@ -56,6 +57,14 @@ extension Mastodon.Query.MediaAttachment { extension Mastodon.Query.MediaAttachment: MultipartFormValue { var multipartValue: Data { return data ?? Data() } + var multipartStreamValue: InputStream? { + switch self { + case .other(let url, _, _): + return url.flatMap { InputStream(url: $0) } + default: + return nil + } + } var multipartContentType: String? { return mimeType } var multipartFilename: String? { return fileName } } diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift b/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift index f86a71c8e..73a0e9ed9 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift @@ -13,10 +13,15 @@ enum Multipart { protocol MultipartFormValue { var multipartValue: Data { get } + var multipartStreamValue: InputStream? { get } var multipartContentType: String? { get } var multipartFilename: String? { get } } +extension MultipartFormValue { + var multipartStreamValue: InputStream? { nil } +} + extension Bool: MultipartFormValue { var multipartValue: Data { switch self { diff --git a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift new file mode 100644 index 000000000..ad86033e7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift @@ -0,0 +1,147 @@ +// +// SerialStream.swift +// +// +// Created by MainasuK Cirno on 2021-5-28. +// + +import os.log +import Foundation +import Combine + +// ref: +// - https://developer.apple.com/documentation/foundation/url_loading_system/uploading_streams_of_data#3037342 +// - https://forums.swift.org/t/extension-write-to-outputstream/42817/4 +// - https://gist.github.com/khanlou/b5e07f963bedcb6e0fcc5387b46991c3 + +final class SerialStream: NSObject { + var writingTimerSubscriber: AnyCancellable? + + // serial stream source + private var streams: [InputStream] + private var currentStreamIndex = 0 + + private static let bufferSize = 5 * 1024 * 1024 // 5MiB + + private var buffer: UnsafeMutablePointer + private var canWrite = false + + private let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.SerialStream.\(UUID().uuidString)") + + // bound pair stream + private(set) lazy var boundStreams: Streams = { + var inputStream: InputStream? + var outputStream: OutputStream? + Stream.getBoundStreams(withBufferSize: SerialStream.bufferSize, inputStream: &inputStream, outputStream: &outputStream) + guard let input = inputStream, let output = outputStream else { + fatalError() + } + + output.delegate = self + output.schedule(in: .current, forMode: .default) + output.open() + + return Streams(input: input, output: output) + }() + + init(streams: [InputStream]) { + self.streams = streams + self.buffer = UnsafeMutablePointer.allocate(capacity: SerialStream.bufferSize) + self.buffer.initialize(repeating: 0, count: SerialStream.bufferSize) + super.init() + + // Stream worker + writingTimerSubscriber = Timer.publish(every: 0.5, on: .current, in: .default) + .autoconnect() + .receive(on: workingQueue) + .sink { [weak self] timer in + guard let self = self else { return } + guard self.canWrite else { return } + os_log(.debug, "%{public}s[%{public}ld], %{public}s: writing…", ((#file as NSString).lastPathComponent), #line, #function) + + guard self.currentStreamIndex < self.streams.count else { + self.boundStreams.output.close() + self.writingTimerSubscriber = nil // cancel timer after task completed + return + } + + var readBytesCount = 0 + defer { + var baseAddress = 0 + var remainsBytes = readBytesCount + while remainsBytes > 0 { + let result = self.boundStreams.output.write(&self.buffer[baseAddress], maxLength: remainsBytes) + baseAddress += result + remainsBytes -= result + os_log(.debug, "%{public}s[%{public}ld], %{public}s: write %ld/%ld bytes. write result: %ld", ((#file as NSString).lastPathComponent), #line, #function, baseAddress, readBytesCount, result) + } + } + + while readBytesCount < SerialStream.bufferSize { + // close when no more source streams + guard self.currentStreamIndex < self.streams.count else { + break + } + + let inputStream = self.streams[self.currentStreamIndex] + // open input if needs + if inputStream.streamStatus != .open { + inputStream.open() + } + // read next source stream when current drain + guard inputStream.hasBytesAvailable else { + self.currentStreamIndex += 1 + continue + } + + let reaminsCount = SerialStream.bufferSize - readBytesCount + let readCount = inputStream.read(&self.buffer[readBytesCount], maxLength: reaminsCount) + os_log(.debug, "%{public}s[%{public}ld], %{public}s: read source %ld bytes", ((#file as NSString).lastPathComponent), #line, #function, readCount) + + switch readCount { + case 0: + self.currentStreamIndex += 1 + continue + case -1: + self.boundStreams.output.close() + return + default: + self.canWrite = false + readBytesCount += readCount + } + } + } + } + + deinit { + os_log(.debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension SerialStream { + struct Streams { + let input: InputStream + let output: OutputStream + } +} + +// MARK: - StreamDelegate +extension SerialStream: StreamDelegate { + func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + os_log(.debug, "%{public}s[%{public}ld], %{public}s: eventCode %s", ((#file as NSString).lastPathComponent), #line, #function, String(eventCode.rawValue)) + + guard aStream == boundStreams.output else { + return + } + + if eventCode.contains(.hasSpaceAvailable) { + canWrite = true + } + + if eventCode.contains(.errorOccurred) { + // Close the streams and alert the user that the upload failed. + boundStreams.output.close() + } + } +} diff --git a/Podfile b/Podfile index ea7075ea8..d9f295c29 100644 --- a/Podfile +++ b/Podfile @@ -13,6 +13,9 @@ target 'Mastodon' do pod 'SwiftGen', '~> 6.4.0' pod 'DateToolsSwift', '~> 5.0.0' pod 'Kanna', '~> 5.2.2' + + # DEBUG + pod 'FLEX', '~> 4.4.0', :configurations => ['Debug'] target 'MastodonTests' do inherit! :search_paths @@ -23,14 +26,16 @@ target 'Mastodon' do # Pods for testing end - target 'NotificationService' do +end - end - - target 'AppShared' do - - end +target 'NotificationService' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! +end +target 'AppShared' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! end plugin 'cocoapods-keys', { diff --git a/Podfile.lock b/Podfile.lock index e341a2420..ea8ecc821 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,5 +1,6 @@ PODS: - DateToolsSwift (5.0.0) + - FLEX (4.4.1) - Kanna (5.2.4) - Keys (1.0.1) - SwiftGen (6.4.0) @@ -7,6 +8,7 @@ PODS: DEPENDENCIES: - DateToolsSwift (~> 5.0.0) + - FLEX (~> 4.4.0) - Kanna (~> 5.2.2) - Keys (from `Pods/CocoaPodsKeys`) - SwiftGen (~> 6.4.0) @@ -15,6 +17,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - DateToolsSwift + - FLEX - Kanna - SwiftGen - "UITextField+Shake" @@ -25,11 +28,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 + FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 -PODFILE CHECKSUM: a8dbae22e6e0bfb84f7db59aef1aa1716793d287 +PODFILE CHECKSUM: 0daad1778e56099e7a7e7ebe3d292d20051840fa COCOAPODS: 1.10.1 From 26b48957cd19c78894e0fe6d6f713c11e190c5a3 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 16:57:27 +0800 Subject: [PATCH 04/10] fix: profile field alignment issue --- .../Header/View/ProfileFieldAddEntryCollectionViewCell.swift | 5 ++--- .../Profile/Header/View/ProfileFieldCollectionViewCell.swift | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift index 1801d4222..d8ecb198d 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift @@ -45,7 +45,6 @@ final class ProfileFieldAddEntryCollectionViewCell: UICollectionViewCell { override func prepareForReuse() { super.prepareForReuse() - //resetStackView() disposeBag.removeAll() } @@ -71,8 +70,8 @@ extension ProfileFieldAddEntryCollectionViewCell { contentView.addSubview(containerStackView) NSLayoutConstraint.activate([ containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), ]) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift index 58af0ce7e..6efa92e18 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift @@ -77,8 +77,8 @@ extension ProfileFieldCollectionViewCell { contentView.addSubview(containerStackView) NSLayoutConstraint.activate([ containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), ]) From 3ed43a3575eb1e20ad13be9f42f2af509956564f Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 16:57:48 +0800 Subject: [PATCH 05/10] fix: profile field edit update logic issue --- .../Scene/Profile/Header/View/ProfileFieldView.swift | 1 - .../API/Mastodon+API+Account+Credentials.swift | 12 +++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift index 14e5e4b33..e008e1bce 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift @@ -30,7 +30,6 @@ final class ProfileFieldView: UIView { textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) textField.textColor = Asset.Colors.Label.primary.color textField.placeholder = L10n.Scene.Profile.Fields.Placeholder.label - textField.isEnabled = false return textField }() diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift index cbc4fb8e2..e0980165d 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift @@ -217,9 +217,15 @@ extension Mastodon.API.Account { source.sensitive.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) } source.language.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) } } - for (i, fieldsAttribute) in (fieldsAttributes ?? []).enumerated() { - data.append(Data.multipart(key: "fields_attributes[\(i)][name]", value: fieldsAttribute.name)) - data.append(Data.multipart(key: "fields_attributes[\(i)][value]", value: fieldsAttribute.value)) + if let fieldsAttributes = fieldsAttributes { + if fieldsAttributes.isEmpty { + data.append(Data.multipart(key: "fields_attributes[]", value: "")) + } else { + for (i, fieldsAttribute) in fieldsAttributes.enumerated() { + data.append(Data.multipart(key: "fields_attributes[\(i)][name]", value: fieldsAttribute.name)) + data.append(Data.multipart(key: "fields_attributes[\(i)][value]", value: fieldsAttribute.value)) + } + } } data.append(Data.multipartEnd()) From b9c262c84e3e23e59785971b235c5b3dbf0d74e8 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 17:07:32 +0800 Subject: [PATCH 06/10] chore: suppression project warning --- Mastodon/Coordinator/SceneCoordinator.swift | 4 ---- Mastodon/Diffiable/Section/StatusSection.swift | 3 ++- .../HashtagTimelineViewModel+LoadMiddleState.swift | 2 +- .../Profile/Header/ProfileHeaderViewController.swift | 8 -------- .../PublicTimeline/PublicTimelineViewModel+Diffable.swift | 5 ----- .../Share/View/Container/MosaicImageViewContainer.swift | 6 +++--- .../Scene/Thread/ThreadViewModel+LoadThreadState.swift | 2 +- ...oMediaPreviewViewControllerAnimatedTransitioning.swift | 2 +- Mastodon/Service/EmojiService/Trie.swift | 2 +- 9 files changed, 9 insertions(+), 25 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 3768f7d3d..f9da785ad 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -261,10 +261,6 @@ private extension SceneCoordinator { let _viewController = FavoriteViewController() _viewController.viewModel = viewModel viewController = _viewController - case .settings(let viewModel): - let _viewController = SettingsViewController() - _viewController.viewModel = viewModel - viewController = _viewController case .suggestionAccount(let viewModel): let _viewController = SuggestionAccountViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index d139e061f..ed24db929 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -274,7 +274,8 @@ extension StatusSection { } else { meta.blurhashImagePublisher() .receive(on: DispatchQueue.main) - .sink { [weak cell] image in + .sink { [weak blurhashImageCache] image in + guard let blurhashImageCache = blurhashImageCache else { return } blurhashOverlayImageView.image = image image?.pngData().flatMap { blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey) diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift index dcd3f81ac..f458b86a5 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift @@ -58,7 +58,7 @@ extension HashtagTimelineViewModel.LoadMiddleState { stateMachine.enter(Fail.self) return } - let statusIDs = (viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects ?? []).compactMap { status in + _ = (viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects ?? []).compactMap { status in status.id } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index fbc8be624..199903792 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -212,14 +212,6 @@ extension ProfileHeaderViewController { } .store(in: &disposeBag) - viewModel.isEditing - .receive(on: RunLoop.main) - .sink { [weak self] isEditing in - guard let self = self else { return } - // self.profileHeaderView.fieldCollectionView. - } - .store(in: &disposeBag) - profileHeaderView.editAvatarButton.menu = createAvatarContextMenu() profileHeaderView.editAvatarButton.showsMenuAsPrimaryAction = true } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift index 27336dc58..3270302bb 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift @@ -59,11 +59,6 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate { var items = [Item]() for (_, status) in indexStatusTuples { - let targetStatus = status.reblog ?? status - let isStatusTextSensitive: Bool = { - guard let spoilerText = targetStatus.spoilerText, !spoilerText.isEmpty else { return false } - return true - }() let attribute = oldSnapshotAttributeDict[status.objectID] ?? Item.StatusAttribute() items.append(Item.status(objectID: status.objectID, attribute: attribute)) if statusIDsWhichHasGap.contains(status.id) { diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 0dccd5930..081e99af4 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -395,7 +395,7 @@ struct MosaicImageView_Previews: PreviewProvider { let images = self.images.prefix(2) let mosaics = view.setupImageViews(count: images.count, maxHeight: 162) for (i, mosiac) in mosaics.enumerated() { - let (imageView, blurhashOverlayImageView) = mosiac + let (imageView, _) = mosiac imageView.image = images[i] } return view @@ -407,7 +407,7 @@ struct MosaicImageView_Previews: PreviewProvider { let images = self.images.prefix(3) let mosaics = view.setupImageViews(count: images.count, maxHeight: 162) for (i, mosiac) in mosaics.enumerated() { - let (imageView, blurhashOverlayImageView) = mosiac + let (imageView, _) = mosiac imageView.image = images[i] } return view @@ -419,7 +419,7 @@ struct MosaicImageView_Previews: PreviewProvider { let images = self.images.prefix(4) let mosaics = view.setupImageViews(count: images.count, maxHeight: 162) for (i, mosiac) in mosaics.enumerated() { - let (imageView, blurhashOverlayImageView) = mosiac + let (imageView, _) = mosiac imageView.image = images[i] } return view diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 5327edc5c..827242644 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -111,7 +111,7 @@ extension ThreadViewModel.LoadThreadState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let _ = viewModel, let stateMachine = stateMachine else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 3) { stateMachine.enter(Loading.self) } diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 3c7b5b8ce..50a518dc7 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -245,7 +245,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { private func popInteractiveTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController, - let fromView = transitionContext.view(forKey: .from), + let _ = transitionContext.view(forKey: .from), let mediaPreviewImageViewController = fromVC.pagingViewConttroller.currentViewController as? MediaPreviewImageViewController, let index = fromVC.pagingViewConttroller.currentIndex else { fatalError() diff --git a/Mastodon/Service/EmojiService/Trie.swift b/Mastodon/Service/EmojiService/Trie.swift index 0eb490ae6..3bf9eeaf0 100644 --- a/Mastodon/Service/EmojiService/Trie.swift +++ b/Mastodon/Service/EmojiService/Trie.swift @@ -104,7 +104,7 @@ extension Trie { var values: NSSet { let valueSet = NSMutableSet(set: self.valueSet) - for (key, value) in children { + for (_, value) in children { valueSet.addObjects(from: Array(value.values)) } From 6211663508ae5a8b62875757ee13cea166067610 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 18:03:31 +0800 Subject: [PATCH 07/10] feat: add post video supports for document picker --- .../Scene/Compose/ComposeViewController.swift | 23 +-- .../MastodonAttachmentService.swift | 161 +++++++++++++----- Mastodon/Vender/PHPickerResultLoader.swift | 1 - 3 files changed, 129 insertions(+), 56 deletions(-) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 2e3e5fe7c..69fef0a51 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -95,7 +95,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { }() private(set) lazy var documentPickerController: UIDocumentPickerViewController = { - let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image]) + let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie]) documentPickerController.delegate = self return documentPickerController }() @@ -1102,20 +1102,13 @@ extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationC extension ComposeViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } - - do { - guard url.startAccessingSecurityScopedResource() else { return } - defer { url.stopAccessingSecurityScopedResource() } - let imageData = try Data(contentsOf: url) - let attachmentService = MastodonAttachmentService( - context: context, - imageData: imageData, - initalAuthenticationBox: viewModel.activeAuthenticationBox.value - ) - viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService] - } catch { - os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - } + + let attachmentService = MastodonAttachmentService( + context: context, + documentURL: url, + initalAuthenticationBox: viewModel.activeAuthenticationBox.value + ) + viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService] } } diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index 4b843ef40..4c541a8d0 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -24,7 +24,7 @@ final class MastodonAttachmentService { weak var delegate: MastodonAttachmentServiceDelegate? let identifier = UUID() - + // input let context: AppContext var authenticationBox: AuthenticationService.MastodonAuthenticationBox? @@ -85,6 +85,65 @@ final class MastodonAttachmentService { self.uploadStateMachine.enter(UploadState.Initial.self) } .store(in: &disposeBag) + } + + init( + context: AppContext, + image: UIImage, + initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? + ) { + self.context = context + self.authenticationBox = initalAuthenticationBox + // end init + + setupServiceObserver() + + file.value = .jpeg(image.jpegData(compressionQuality: 0.75)) + uploadStateMachine.enter(UploadState.Initial.self) + } + + init( + context: AppContext, + documentURL: URL, + initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? + ) { + self.context = context + self.authenticationBox = initalAuthenticationBox + // end init + + setupServiceObserver() + + Just(documentURL) + .flatMap { documentURL -> AnyPublisher in + return MastodonAttachmentService.loadAttachment(url: documentURL) + } + .sink { [weak self] completion in + guard let self = self else { return } + switch completion { + case .failure(let error): + self.error.value = error + self.uploadStateMachine.enter(UploadState.Fail.self) + case .finished: + break + } + } receiveValue: { [weak self] file in + guard let self = self else { return } + self.file.value = file + self.uploadStateMachine.enter(UploadState.Initial.self) + } + .store(in: &disposeBag) + + uploadStateMachine.enter(UploadState.Initial.self) + } + + private func setupServiceObserver() { + uploadStateMachineSubject + .sink { [weak self] state in + guard let self = self else { return } + self.delegate?.mastodonAttachmentService(self, uploadStateDidChange: state) + } + .store(in: &disposeBag) + file .map { file -> UIImage? in @@ -117,45 +176,6 @@ final class MastodonAttachmentService { .store(in: &disposeBag) } - init( - context: AppContext, - image: UIImage, - initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? - ) { - self.context = context - self.authenticationBox = initalAuthenticationBox - // end init - - setupServiceObserver() - - file.value = .jpeg(image.jpegData(compressionQuality: 0.75)) - uploadStateMachine.enter(UploadState.Initial.self) - } - - init( - context: AppContext, - imageData: Data, - initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? - ) { - self.context = context - self.authenticationBox = initalAuthenticationBox - // end init - - setupServiceObserver() - - self.file.value = .jpeg(imageData) - uploadStateMachine.enter(UploadState.Initial.self) - } - - private func setupServiceObserver() { - uploadStateMachineSubject - .sink { [weak self] state in - guard let self = self else { return } - self.delegate?.mastodonAttachmentService(self, uploadStateDidChange: state) - } - .store(in: &disposeBag) - } - deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } @@ -189,3 +209,64 @@ extension MastodonAttachmentService: Equatable, Hashable { } } + +extension MastodonAttachmentService { + + private static func createWorkingQueue() -> DispatchQueue { + return DispatchQueue(label: "org.joinmastodon.Mastodon.MastodonAttachmentService.\(UUID().uuidString)") + } + + static func loadAttachment(url: URL) -> AnyPublisher { + guard let uti = UTType(filenameExtension: url.pathExtension) else { + return Fail(error: AttachmentError.invalidAttachmentType).eraseToAnyPublisher() + } + + if uti.conforms(to: .image) { + return loadImageAttachment(url: url) + } else if uti.conforms(to: .movie) { + return loadVideoAttachment(url: url) + } else { + return Fail(error: AttachmentError.invalidAttachmentType).eraseToAnyPublisher() + } + } + + static func loadImageAttachment(url: URL) -> AnyPublisher { + Future { promise in + createWorkingQueue().async { + do { + guard url.startAccessingSecurityScopedResource() else { return } + defer { url.stopAccessingSecurityScopedResource() } + let imageData = try Data(contentsOf: url) + promise(.success(.jpeg(imageData))) + } catch { + os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + promise(.failure(error)) + } + } + } + .eraseToAnyPublisher() + } + + static func loadVideoAttachment(url: URL) -> AnyPublisher { + Future { promise in + createWorkingQueue().async { + guard url.startAccessingSecurityScopedResource() else { return } + defer { url.stopAccessingSecurityScopedResource() } + + let fileName = UUID().uuidString + let tempDirectoryURL = FileManager.default.temporaryDirectory + let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension) + do { + try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil) + try FileManager.default.copyItem(at: url, to: fileURL) + let file = Mastodon.Query.MediaAttachment.other(fileURL, fileExtension: fileURL.pathExtension, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4") + promise(.success(file)) + } catch { + promise(.failure(error)) + } + } + } + .eraseToAnyPublisher() + } + +} diff --git a/Mastodon/Vender/PHPickerResultLoader.swift b/Mastodon/Vender/PHPickerResultLoader.swift index 88a9a7554..8a3bca621 100644 --- a/Mastodon/Vender/PHPickerResultLoader.swift +++ b/Mastodon/Vender/PHPickerResultLoader.swift @@ -95,7 +95,6 @@ enum PHPickerResultLoader { } catch { promise(.failure(error)) } - } } } From 70b75c8ebaf9e3ce5d936a86fae1ed086c907b61 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 1 Jun 2021 14:07:44 +0800 Subject: [PATCH 08/10] feat: display clean cache result --- Localization/app.json | 4 + Mastodon/Generated/Strings.swift | 8 ++ .../Resources/ar.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 2 + .../Settings/SettingsViewController.swift | 63 ++++++----- Mastodon/State/AppContext.swift | 106 ++++++++++++++++++ 6 files changed, 157 insertions(+), 28 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 69d513600..1833e00e5 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -43,6 +43,10 @@ "delete_post": { "title": "Are you sure you want to delete this post?", "delete": "Delete" + }, + "clean_cache": { + "title": "Clean Cache", + "message": "Successfully clean %s cache." } }, "controls": { diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 5790f7134..dfae5e2dd 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -21,6 +21,14 @@ internal enum L10n { return L10n.tr("Localizable", "Common.Alerts.BlockDomain.Title", String(describing: p1)) } } + internal enum CleanCache { + /// Successfully clean %@ cache. + internal static func message(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Alerts.CleanCache.Message", String(describing: p1)) + } + /// Clean Cache + internal static let title = L10n.tr("Localizable", "Common.Alerts.CleanCache.Title") + } internal enum Common { /// Please try again. internal static let pleaseTryAgain = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgain") diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 81bf772e1..39c4c4fec 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -1,5 +1,7 @@ "Common.Alerts.BlockDomain.BlockEntireDomain" = "Block entire domain"; "Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed."; +"Common.Alerts.CleanCache.Message" = "Successfully clean %@ cache."; +"Common.Alerts.CleanCache.Title" = "Clean Cache"; "Common.Alerts.Common.PleaseTryAgain" = "Please try again."; "Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later."; "Common.Alerts.DeletePost.Delete" = "Delete"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 81bf772e1..39c4c4fec 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -1,5 +1,7 @@ "Common.Alerts.BlockDomain.BlockEntireDomain" = "Block entire domain"; "Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed."; +"Common.Alerts.CleanCache.Message" = "Successfully clean %@ cache."; +"Common.Alerts.CleanCache.Title" = "Clean Cache"; "Common.Alerts.Common.PleaseTryAgain" = "Please try again."; "Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later."; "Common.Alerts.DeletePost.Delete" = "Delete"; diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 0ffb09cfb..ad85bee8f 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -12,8 +12,7 @@ import ActiveLabel import CoreData import CoreDataStack import MastodonSDK -import AlamofireImage -import Kingfisher + class SettingsViewController: UIViewController, NeedsDependency { @@ -319,36 +318,44 @@ extension SettingsViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let dataSource = viewModel.dataSource else { return } - let item = dataSource.itemIdentifier(for: indexPath) + guard let item = dataSource.itemIdentifier(for: indexPath) else { return } switch item { - case .boringZone: - guard let url = viewModel.privacyURL else { break } - coordinator.present( - scene: .safari(url: url), - from: self, - transition: .safariPresent(animated: true, completion: nil) - ) - case .spicyZone(let link): - // clear media cache - if link.title == L10n.Scene.Settings.Section.Spicyzone.clear { - // clean image cache for AlamofireImage - let diskBytes = ImageDownloader.defaultURLCache().currentDiskUsage - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: diskBytes %d", ((#file as NSString).lastPathComponent), #line, #function, diskBytes) - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: clean image cache", ((#file as NSString).lastPathComponent), #line, #function) - ImageDownloader.defaultURLCache().removeAllCachedResponses() - let cleanedDiskBytes = ImageDownloader.defaultURLCache().currentDiskUsage - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: diskBytes %d", ((#file as NSString).lastPathComponent), #line, #function, cleanedDiskBytes) - - // clean Kingfisher Cache - KingfisherManager.shared.cache.clearDiskCache() - } - // logout - if link.title == L10n.Scene.Settings.Section.Spicyzone.signout { + case .apperance: + // do nothing + break + case .notification: + // do nothing + break + case .boringZone(let link), .spicyZone(let link): + switch link { + case .termsOfService, .privacyPolicy: + // same URL + guard let url = viewModel.privacyURL else { break } + coordinator.present( + scene: .safari(url: url), + from: self, + transition: .safariPresent(animated: true, completion: nil) + ) + case .clearMediaCache: + context.purgeCache() + .receive(on: RunLoop.main) + .sink { [weak self] byteCount in + guard let self = self else { return } + let byteCountformatted = AppContext.byteCountFormatter.string(fromByteCount: Int64(byteCount)) + let alertController = UIAlertController( + title: L10n.Common.Alerts.CleanCache.title, + message: L10n.Common.Alerts.CleanCache.message(byteCountformatted), + preferredStyle: .alert + ) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + self.coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) + } + .store(in: &disposeBag) + case .signOut: alertToSignout() } - default: - break } } } diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 55d5841f7..8c0fa364b 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -10,6 +10,8 @@ import UIKit import Combine import CoreData import CoreDataStack +import AlamofireImage +import Kingfisher class AppContext: ObservableObject { @@ -99,3 +101,107 @@ class AppContext: ObservableObject { } } + +extension AppContext { + + typealias ByteCount = Int + + static let byteCountFormatter: ByteCountFormatter = { + let formatter = ByteCountFormatter() + return formatter + }() + + private static let purgeCacheWorkingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.AppContext.purgeCacheWorkingQueue") + + func purgeCache() -> AnyPublisher { + Publishers.MergeMany([ + AppContext.purgeAlamofireImageCache(), + AppContext.purgeKingfisherCache(), + AppContext.purgeTemporaryDirectory(), + ]) + .reduce(0, +) + .eraseToAnyPublisher() + } + + private static func purgeAlamofireImageCache() -> AnyPublisher { + Future { promise in + AppContext.purgeCacheWorkingQueue.async { + // clean image cache for AlamofireImage + let diskBytes = ImageDownloader.defaultURLCache().currentDiskUsage + ImageDownloader.defaultURLCache().removeAllCachedResponses() + let currentDiskBytes = ImageDownloader.defaultURLCache().currentDiskUsage + let purgedDiskBytes = max(0, diskBytes - currentDiskBytes) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: purge AlamofireImage cache bytes: %ld -> %ld (%ld)", ((#file as NSString).lastPathComponent), #line, #function, diskBytes, currentDiskBytes, purgedDiskBytes) + promise(.success(purgedDiskBytes)) + } + } + .eraseToAnyPublisher() + } + + private static func purgeKingfisherCache() -> AnyPublisher { + Future { promise in + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + switch result { + case .success(let diskBytes): + KingfisherManager.shared.cache.clearCache() + KingfisherManager.shared.cache.calculateDiskStorageSize { currentResult in + switch currentResult { + case .success(let currentDiskBytes): + let purgedDiskBytes = max(0, Int(diskBytes) - Int(currentDiskBytes)) + promise(.success(purgedDiskBytes)) + case .failure: + promise(.success(0)) + } + } + case .failure: + promise(.success(0)) + } + } + } + .eraseToAnyPublisher() + } + + private static func purgeTemporaryDirectory() -> AnyPublisher { + Future { promise in + AppContext.purgeCacheWorkingQueue.async { + let fileManager = FileManager.default + let temporaryDirectoryURL = fileManager.temporaryDirectory + + let resourceKeys = Set([.fileSizeKey, .isDirectoryKey]) + guard let directoryEnumerator = fileManager.enumerator( + at: temporaryDirectoryURL, + includingPropertiesForKeys: Array(resourceKeys), + options: .skipsHiddenFiles + ) else { + promise(.success(0)) + return + } + + var fileURLs: [URL] = [] + var totalFileSizeInBytes = 0 + for case let fileURL as URL in directoryEnumerator { + guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys), + let isDirectory = resourceValues.isDirectory else { + continue + } + + guard !isDirectory else { + continue + } + fileURLs.append(fileURL) + totalFileSizeInBytes += resourceValues.fileSize ?? 0 + } + + for fileURL in fileURLs { + try? fileManager.removeItem(at: fileURL) + } + + promise(.success(totalFileSizeInBytes)) + } + } + .eraseToAnyPublisher() + } +// +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: purge temporary directory success", ((#file as NSString).lastPathComponent), #line, #function) +// } +} From deae4bea83d9fafacbf7088888cf7ad60de01844 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 1 Jun 2021 14:08:29 +0800 Subject: [PATCH 09/10] chore: suppression CoreDataStack framework warning --- CoreDataStack/Protocol/Managed.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreDataStack/Protocol/Managed.swift b/CoreDataStack/Protocol/Managed.swift index 4bdff9c3e..9cf9278ff 100644 --- a/CoreDataStack/Protocol/Managed.swift +++ b/CoreDataStack/Protocol/Managed.swift @@ -8,7 +8,7 @@ import Foundation import CoreData -public protocol Managed: AnyObject, NSFetchRequestResult { +public protocol Managed: NSFetchRequestResult { static var entityName: String { get } static var defaultSortDescriptors: [NSSortDescriptor] { get } } From 8b941022891dcb5bf4fd0da6bcf7e1d5172a4b01 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 1 Jun 2021 14:31:31 +0800 Subject: [PATCH 10/10] feat: update status timestamp behavior. resolve #82 --- Localization/app.json | 3 + Mastodon.xcodeproj/project.pbxproj | 204 +++++++++--------- .../Section/ComposeStatusSection.swift | 2 +- .../Section/NotificationSection.swift | 6 +- .../Diffiable/Section/StatusSection.swift | 6 +- Mastodon/Extension/Date.swift | 29 +++ Mastodon/Generated/Strings.swift | 4 + .../Resources/ar.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 1 + 9 files changed, 149 insertions(+), 107 deletions(-) create mode 100644 Mastodon/Extension/Date.swift diff --git a/Localization/app.json b/Localization/app.json index 1833e00e5..3e90f799f 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -173,6 +173,9 @@ "edit_info": "Edit info" }, "timeline": { + "timestamp": { + "now": "Now" + }, "loader": { "load_missing_posts": "Load missing posts", "loading_missing_posts": "Loading missing posts...", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b1aaa14b4..5d765861b 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -67,7 +67,7 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; - 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */; }; + 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; 2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; }; @@ -80,7 +80,7 @@ 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; - 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */; }; + 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; }; 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */; }; 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; @@ -90,7 +90,7 @@ 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; }; 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; - 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */; }; + 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; @@ -116,7 +116,7 @@ 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; - 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */; }; + 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* CropViewController */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */; }; 2D9DB969263A833E007C1D71 /* DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB968263A833E007C1D71 /* DomainBlock.swift */; }; @@ -164,7 +164,7 @@ 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; }; 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; - 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */; }; + 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */; }; 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; @@ -180,11 +180,11 @@ 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */; }; - DB00CA972632DDB600A54956 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */; }; + DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; - DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */; }; + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; @@ -216,7 +216,7 @@ DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; }; DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; }; DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; }; - DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */; }; + DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; @@ -257,7 +257,7 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; - DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */; }; + DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; @@ -293,7 +293,7 @@ DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; }; DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; }; - DB6805102637D0F800430867 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* SwiftPackageProductDependency */; }; + DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; }; DB6805262637D7DD00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; }; @@ -306,7 +306,7 @@ DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; }; DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; }; DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; }; - DB6D9F42263527CE008423CD /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */; }; + DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; }; DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4826353FD6008423CD /* Subscription.swift */; }; DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */; }; DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */; }; @@ -317,8 +317,8 @@ DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; }; DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; }; DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */; }; - DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; }; - DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; }; + DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; }; @@ -374,6 +374,7 @@ DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; }; DB938F25262438D600E5B6C1 /* ThreadViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F24262438D600E5B6C1 /* ThreadViewController+Provider.swift */; }; DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */; }; + DB97131F2666078B00BD1E90 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB97131E2666078B00BD1E90 /* Date.swift */; }; DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; }; DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; }; DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; }; @@ -382,7 +383,7 @@ DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; }; DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */; }; DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; }; - DB9A487E2603456B008B817C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* SwiftPackageProductDependency */; }; + DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; }; DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; }; DB9A489026035963008B817C /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; }; DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */; }; @@ -416,7 +417,7 @@ DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; - DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */; }; + DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; }; @@ -463,7 +464,7 @@ DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; }; DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; }; DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DBF8AE862632992800C9C23C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */; }; + DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; }; DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; }; DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */; }; DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */; }; @@ -552,7 +553,7 @@ files = ( DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */, DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */, - DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */, + DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -946,6 +947,7 @@ DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = ""; }; DB938F24262438D600E5B6C1 /* ThreadViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewController+Provider.swift"; sourceTree = ""; }; DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = ""; }; + DB97131E2666078B00BD1E90 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = ""; }; DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = ""; }; DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = ""; }; @@ -1047,20 +1049,20 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */, - DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */, + DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */, + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, - 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */, - DB9A487E2603456B008B817C /* BuildFile in Frameworks */, - 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */, - DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */, - 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */, + 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, + DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */, + 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, + DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, + 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, - DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */, + DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, - 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */, - DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */, - 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */, + 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, + DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, + 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */, ); @@ -1087,7 +1089,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6805102637D0F800430867 /* BuildFile in Frameworks */, + DB6805102637D0F800430867 /* KeychainAccess in Frameworks */, EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1112,9 +1114,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB00CA972632DDB600A54956 /* BuildFile in Frameworks */, - DB6D9F42263527CE008423CD /* BuildFile in Frameworks */, - DBF8AE862632992800C9C23C /* BuildFile in Frameworks */, + DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */, + DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */, + DBF8AE862632992800C9C23C /* Base85 in Frameworks */, DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */, B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */, ); @@ -2167,6 +2169,7 @@ DBCC3B2F261440A50045B23D /* UITabBarController.swift */, DBCC3B35261440BA0045B23D /* UINavigationController.swift */, DB6D1B23263684C600ACB481 /* UserDefaults.swift */, + DB97131E2666078B00BD1E90 /* Date.swift */, ); path = Extension; sourceTree = ""; @@ -2497,17 +2500,17 @@ ); name = Mastodon; packageProductDependencies = ( - DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */, - 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */, - 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */, - 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */, - DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */, - DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */, - 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */, - 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */, - DB9A487D2603456B008B817C /* SwiftPackageProductDependency */, - DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */, - DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */, + DB3D0FF225BAA61700EAA174 /* AlamofireImage */, + 5D526FE125BE9AC400460CB9 /* MastodonSDK */, + 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, + 2D42FF6025C8177C004A627A /* ActiveLabel */, + DB0140BC25C40D7500F9F3CF /* CommonOSLog */, + DB5086B725CC0D6400C2C187 /* Kingfisher */, + 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, + 2D939AC725EE14620076FA61 /* CropViewController */, + DB9A487D2603456B008B817C /* UITextView+Placeholder */, + DBB525072611EAC0002F1F29 /* Tabman */, + DB6F5E31264E7410009108F4 /* TwitterTextEditor */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -2568,7 +2571,7 @@ ); name = AppShared; packageProductDependencies = ( - DB68050F2637D0F800430867 /* SwiftPackageProductDependency */, + DB68050F2637D0F800430867 /* KeychainAccess */, ); productName = AppShared; productReference = DB68047F2637CD4C00430867 /* AppShared.framework */; @@ -2628,9 +2631,9 @@ ); name = NotificationService; packageProductDependencies = ( - DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */, - DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */, - DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */, + DBF8AE852632992800C9C23C /* Base85 */, + DB00CA962632DDB600A54956 /* CommonOSLog */, + DB6D9F41263527CE008423CD /* AlamofireImage */, ); productName = NotificationService; productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */; @@ -2685,18 +2688,18 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */, - 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */, - 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */, - DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */, - DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */, - 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */, - 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */, - DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */, - DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */, - DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */, - DB6804722637CC1200430867 /* RemoteSwiftPackageReference */, - DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */, + DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, + 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, + 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, + DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, + 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, + 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, + DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, + DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, + DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */, + DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, + DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -3052,6 +3055,7 @@ DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, 2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */, + DB97131F2666078B00BD1E90 /* Date.swift in Sources */, DB98338825C945ED00AD9700 /* Assets.swift in Sources */, DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */, DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */, @@ -4034,7 +4038,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */ = { + 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { @@ -4042,7 +4046,7 @@ version = 5.0.2; }; }; - 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */ = { + 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git"; requirement = { @@ -4050,7 +4054,7 @@ minimumVersion = 1.7.1; }; }; - 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */ = { + 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; requirement = { @@ -4058,7 +4062,7 @@ minimumVersion = 3.1.0; }; }; - 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */ = { + 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TimOliver/TOCropViewController.git"; requirement = { @@ -4066,7 +4070,7 @@ minimumVersion = 2.6.0; }; }; - DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */ = { + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/CommonOSLog"; requirement = { @@ -4074,7 +4078,7 @@ minimumVersion = 0.1.1; }; }; - DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */ = { + DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; requirement = { @@ -4082,7 +4086,7 @@ minimumVersion = 4.1.0; }; }; - DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */ = { + DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { @@ -4090,7 +4094,7 @@ minimumVersion = 6.1.0; }; }; - DB6804722637CC1200430867 /* RemoteSwiftPackageReference */ = { + DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; requirement = { @@ -4098,7 +4102,7 @@ minimumVersion = 4.2.2; }; }; - DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */ = { + DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/TwitterTextEditor.git"; requirement = { @@ -4106,7 +4110,7 @@ kind = branch; }; }; - DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */ = { + DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder"; requirement = { @@ -4114,7 +4118,7 @@ minimumVersion = 1.4.1; }; }; - DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */ = { + DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/uias/Tabman"; requirement = { @@ -4122,7 +4126,7 @@ minimumVersion = 2.11.0; }; }; - DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */ = { + DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/Base85.git"; requirement = { @@ -4133,78 +4137,78 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */ = { + 2D42FF6025C8177C004A627A /* ActiveLabel */ = { isa = XCSwiftPackageProductDependency; - package = 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */; + package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */; productName = ActiveLabel; }; - 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */ = { + 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = { isa = XCSwiftPackageProductDependency; - package = 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */; + package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */; productName = ThirdPartyMailer; }; - 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */ = { + 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */ = { isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */; + package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; productName = AlamofireNetworkActivityIndicator; }; - 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */ = { + 2D939AC725EE14620076FA61 /* CropViewController */ = { isa = XCSwiftPackageProductDependency; - package = 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */; + package = 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */; productName = CropViewController; }; - 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */ = { + 5D526FE125BE9AC400460CB9 /* MastodonSDK */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */ = { + DB00CA962632DDB600A54956 /* CommonOSLog */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; + package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; productName = CommonOSLog; }; - DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */ = { + DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; + package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; productName = CommonOSLog; }; - DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */ = { + DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; + package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; - DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */ = { + DB5086B725CC0D6400C2C187 /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; - package = DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */; + package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; - DB68050F2637D0F800430867 /* SwiftPackageProductDependency */ = { + DB68050F2637D0F800430867 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = DB6804722637CC1200430867 /* RemoteSwiftPackageReference */; + package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; - DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */ = { + DB6D9F41263527CE008423CD /* AlamofireImage */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; + package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; - DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */ = { + DB6F5E31264E7410009108F4 /* TwitterTextEditor */ = { isa = XCSwiftPackageProductDependency; - package = DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */; + package = DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; productName = TwitterTextEditor; }; - DB9A487D2603456B008B817C /* SwiftPackageProductDependency */ = { + DB9A487D2603456B008B817C /* UITextView+Placeholder */ = { isa = XCSwiftPackageProductDependency; - package = DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */; + package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; productName = "UITextView+Placeholder"; }; - DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */ = { + DBB525072611EAC0002F1F29 /* Tabman */ = { isa = XCSwiftPackageProductDependency; - package = DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */; + package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */; productName = Tabman; }; - DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */ = { + DBF8AE852632992800C9C23C /* Base85 */ = { isa = XCSwiftPackageProductDependency; - package = DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */; + package = DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */; productName = Base85; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index a1e6170e8..875a82842 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -76,7 +76,7 @@ extension ComposeStatusSection { //status.emoji cell.statusView.activeTextLabel.configure(content: status.content, emojiDict: [:]) // set date - cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow + cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow cell.framePublisher.assign(to: \.value, on: repliedToCellFrameSubscriber).store(in: &cell.disposeBag) } diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index ead5d48f8..155ec3fc7 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -36,7 +36,7 @@ extension NotificationSection { assertionFailure() return nil } - let timeText = notification.createAt.shortTimeAgoSinceNow + let timeText = notification.createAt.slowedTimeAgoSinceNow let actionText = type.actionText let actionImageName = type.actionImageName @@ -59,7 +59,7 @@ extension NotificationSection { ) timestampUpdatePublisher .sink { _ in - let timeText = notification.createAt.shortTimeAgoSinceNow + let timeText = notification.createAt.slowedTimeAgoSinceNow cell.actionLabel.text = actionText + " · " + timeText } .store(in: &cell.disposeBag) @@ -87,7 +87,7 @@ extension NotificationSection { cell.delegate = delegate timestampUpdatePublisher .sink { _ in - let timeText = notification.createAt.shortTimeAgoSinceNow + let timeText = notification.createAt.slowedTimeAgoSinceNow cell.actionLabel.text = actionText + " · " + timeText } .store(in: &cell.disposeBag) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index ed24db929..f5460b02b 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -468,12 +468,12 @@ extension StatusSection { // set date let createdAt = (status.reblog ?? status).createdAt - cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow + cell.statusView.dateLabel.text = createdAt.slowedTimeAgoSinceNow timestampUpdatePublisher .sink { [weak cell] _ in guard let cell = cell else { return } - cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow - cell.statusView.dateLabel.accessibilityLabel = createdAt.timeAgoSinceNow + cell.statusView.dateLabel.text = createdAt.slowedTimeAgoSinceNow + cell.statusView.dateLabel.accessibilityLabel = createdAt.slowedTimeAgoSinceNow } .store(in: &cell.disposeBag) diff --git a/Mastodon/Extension/Date.swift b/Mastodon/Extension/Date.swift new file mode 100644 index 000000000..c1d73a438 --- /dev/null +++ b/Mastodon/Extension/Date.swift @@ -0,0 +1,29 @@ +// +// Date.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-1. +// + +import Foundation +import DateToolsSwift + +extension Date { + + var slowedTimeAgoSinceNow: String { + return self.slowedTimeAgo(since: Date()) + + } + + func slowedTimeAgo(since date: Date) -> String { + let earlierDate = date < self ? date : self + let latest = earlierDate == date ? self : date + + if earlierDate.timeIntervalSince(latest) >= -60 { + return L10n.Common.Controls.Timeline.Timestamp.now + } else { + return latest.shortTimeAgo(since: earlierDate) + } + } + +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index dfae5e2dd..34556e8a6 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -380,6 +380,10 @@ internal enum L10n { /// Show more replies internal static let showMoreReplies = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.ShowMoreReplies") } + internal enum Timestamp { + /// Now + internal static let now = L10n.tr("Localizable", "Common.Controls.Timeline.Timestamp.Now") + } } } internal enum Countable { diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 39c4c4fec..a95c2cbce 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -132,6 +132,7 @@ Your account looks like this to them."; "Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; +"Common.Controls.Timeline.Timestamp.Now" = "Now"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; "Scene.Compose.Accessibility.AppendAttachment" = "Append attachment"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 39c4c4fec..a95c2cbce 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -132,6 +132,7 @@ Your account looks like this to them."; "Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; +"Common.Controls.Timeline.Timestamp.Now" = "Now"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; "Scene.Compose.Accessibility.AppendAttachment" = "Append attachment";