diff --git a/Localization/app.json b/Localization/app.json index 45c771235..bc810f759 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -194,7 +194,12 @@ "new_reply": "New Reply" }, "content_input_placeholder": "Type or paste what's on your mind", - "compose_action": "Publish" + "compose_action": "Publish", + "attachment": { + "photo": "photo", + "video": "video", + "attachment_broken": "This %s is broken and can't be\nuploaded to Mastodon." + } } } -} +} \ No newline at end of file diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 5afd4f65d..99d1cdf2e 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -212,6 +212,8 @@ DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; }; DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; }; 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 */; }; DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; }; DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; }; DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; }; @@ -496,6 +498,8 @@ DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = ""; }; + DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultLoader.swift; sourceTree = ""; }; + DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = ""; }; DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; @@ -718,6 +722,7 @@ 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */, DB2B3AE825E38850007045F9 /* UIViewPreview.swift */, DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */, + DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */, ); path = Vender; sourceTree = ""; @@ -1061,6 +1066,7 @@ children = ( DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */, DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */, + DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */, ); path = View; sourceTree = ""; @@ -1799,6 +1805,7 @@ DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, + DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */, DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */, DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, @@ -1834,6 +1841,7 @@ DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, + DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index f0d912ebb..a99e1f49a 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -10,6 +10,7 @@ import Combine import CoreData import CoreDataStack import TwitterTextEditor +import AlamofireImage enum ComposeStatusSection: Equatable, Hashable { case repliedTo @@ -30,9 +31,10 @@ extension ComposeStatusSection { dependency: NeedsDependency, managedObjectContext: NSManagedObjectContext, composeKind: ComposeKind, - textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate + textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate, + composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentTableViewCellDelegate ) -> UITableViewDiffableDataSource { - UITableViewDiffableDataSource(tableView: tableView) { [weak textEditorViewTextAttributesDelegate] tableView, indexPath, item -> UITableViewCell? in + UITableViewDiffableDataSource(tableView: tableView) { [weak textEditorViewTextAttributesDelegate, weak composeStatusAttachmentTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in switch item { case .replyTo(let repliedToStatusObjectID): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self), for: indexPath) as! ComposeRepliedToTootContentTableViewCell @@ -62,7 +64,35 @@ extension ComposeStatusSection { return cell case .attachment(let attachmentService): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self), for: indexPath) as! ComposeStatusAttachmentTableViewCell - + cell.delegate = composeStatusAttachmentTableViewCellDelegate + attachmentService.imageData + .receive(on: DispatchQueue.main) + .sink { imageData in + guard let imageData = imageData, + let image = UIImage(data: imageData) else { + let placeholder = UIImage.placeholder( + size: cell.attachmentContainerView.previewImageView.frame.size, + color: Asset.Colors.Background.systemGroupedBackground.color + ) + .af.imageRounded( + withCornerRadius: AttachmentContainerView.containerViewCornerRadius + ) + cell.attachmentContainerView.previewImageView.image = placeholder + return + } + cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.previewImageView.image = image + .af.imageAspectScaled(toFill: cell.attachmentContainerView.previewImageView.frame.size) + .af.imageRounded(withCornerRadius: AttachmentContainerView.containerViewCornerRadius) + } + .store(in: &cell.disposeBag) + attachmentService.error + .receive(on: DispatchQueue.main) + .sink { error in + cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.emptyStateView.isHidden = error == nil + } + .store(in: &cell.disposeBag) return cell } } diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 2133c5aa3..ba58cb3f1 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -30,11 +30,16 @@ internal enum Asset { } internal enum Colors { internal enum Background { + internal enum AudioPlayer { + internal static let highlight = ColorAsset(name: "Colors/Background/AudioPlayer/highlight") + } internal enum Poll { internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled") internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight") } - internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/mediaTypeIndicotor") + internal static let dangerBorder = ColorAsset(name: "Colors/Background/danger.border") + internal static let danger = ColorAsset(name: "Colors/Background/danger") + internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor") internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background") internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background") internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background") @@ -45,7 +50,6 @@ internal enum Asset { internal enum Button { internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar") internal static let disabled = ColorAsset(name: "Colors/Button/disabled") - internal static let highlight = ColorAsset(name: "Colors/Button/highlight") internal static let normal = ColorAsset(name: "Colors/Button/normal") } internal enum Icon { @@ -79,10 +83,12 @@ internal enum Asset { internal static let lightSecondaryText = ColorAsset(name: "Colors/lightSecondaryText") internal static let lightSuccessGreen = ColorAsset(name: "Colors/lightSuccessGreen") internal static let lightWhite = ColorAsset(name: "Colors/lightWhite") - internal static let plusCircleFill = ImageAsset(name: "Colors/plus.circle.fill") internal static let systemGreen = ColorAsset(name: "Colors/system.green") internal static let systemOrange = ColorAsset(name: "Colors/system.orange") } + internal enum Connectivity { + internal static let photoFillSplit = ImageAsset(name: "Connectivity/photo.fill.split") + } internal enum Welcome { internal enum Illustration { internal static let backgroundCyan = ColorAsset(name: "Welcome/illustration/background.cyan") diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 7079b2970..59ee4a49b 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -144,6 +144,16 @@ internal enum L10n { internal static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction") /// Type or paste what's on your mind internal static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder") + internal enum Attachment { + /// This %@ is broken and can't be\nuploaded to Mastodon. + internal static func attachmentBroken(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentBroken", String(describing: p1)) + } + /// photo + internal static let photo = L10n.tr("Localizable", "Scene.Compose.Attachment.Photo") + /// video + internal static let video = L10n.tr("Localizable", "Scene.Compose.Attachment.Video") + } internal enum Title { /// New Post internal static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost") diff --git a/Mastodon/Resources/Assets.xcassets/Circles/plus.circle.fill.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Circles/plus.circle.fill.imageset/Contents.json index 580a3f7a0..40480a161 100644 --- a/Mastodon/Resources/Assets.xcassets/Circles/plus.circle.fill.imageset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Circles/plus.circle.fill.imageset/Contents.json @@ -2,20 +2,14 @@ "images" : [ { "filename" : "plus.circle.fill.pdf", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/AudioPlayer/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/AudioPlayer/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/AudioPlayer/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Button/highlight.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/AudioPlayer/highlight.colorset/Contents.json similarity index 74% rename from Mastodon/Resources/Assets.xcassets/Colors/Button/highlight.colorset/Contents.json rename to Mastodon/Resources/Assets.xcassets/Colors/Background/AudioPlayer/highlight.colorset/Contents.json index 03a422b00..2e1ce5f3a 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Button/highlight.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/AudioPlayer/highlight.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.839", - "green" : "0.573", - "red" : "0.204" + "blue" : "0.851", + "green" : "0.565", + "red" : "0.169" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/danger.border.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/danger.border.colorset/Contents.json new file mode 100644 index 000000000..dabccc33e --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/danger.border.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.353", + "green" : "0.251", + "red" : "0.875" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/danger.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/danger.colorset/Contents.json new file mode 100644 index 000000000..b77cb3c75 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/danger.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "90", + "green" : "64", + "red" : "223" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/mediaTypeIndicotor.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/media.type.indicotor.colorset/Contents.json similarity index 100% rename from Mastodon/Resources/Assets.xcassets/Colors/Background/mediaTypeIndicotor.colorset/Contents.json rename to Mastodon/Resources/Assets.xcassets/Colors/Background/media.type.indicotor.colorset/Contents.json diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/system.grouped.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.grouped.background.colorset/Contents.json index d097fec40..edc0dce9a 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/system.grouped.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.grouped.background.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xE8", - "green" : "0xE1", - "red" : "0xD9" + "blue" : "232", + "green" : "225", + "red" : "217" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/plus.circle.fill.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/plus.circle.fill.imageset/Contents.json deleted file mode 100644 index 580a3f7a0..000000000 --- a/Mastodon/Resources/Assets.xcassets/Colors/plus.circle.fill.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "plus.circle.fill.pdf", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/plus.circle.fill.imageset/plus.circle.fill.pdf b/Mastodon/Resources/Assets.xcassets/Colors/plus.circle.fill.imageset/plus.circle.fill.pdf deleted file mode 100644 index f4a613417..000000000 --- a/Mastodon/Resources/Assets.xcassets/Colors/plus.circle.fill.imageset/plus.circle.fill.pdf +++ /dev/null @@ -1,101 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm -1.000000 1.000000 1.000000 scn -30.000000 15.000000 m -30.000000 6.715729 23.284271 0.000000 15.000000 0.000000 c -6.715729 0.000000 0.000000 6.715729 0.000000 15.000000 c -0.000000 23.284271 6.715729 30.000000 15.000000 30.000000 c -23.284271 30.000000 30.000000 23.284271 30.000000 15.000000 c -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm -0.000000 0.000000 0.000000 scn -15.009004 0.000000 m -23.233341 0.000000 30.000000 6.766640 30.000000 15.009003 c -30.000000 23.233379 23.233341 30.000000 14.991017 30.000000 c -6.766642 30.000000 0.000000 23.233379 0.000000 15.009003 c -0.000000 6.766640 6.766643 0.000000 15.009004 0.000000 c -h -8.098384 15.009003 m -8.098384 16.034798 8.836217 16.790653 9.844025 16.790653 c -13.209368 16.790653 l -13.209368 20.155996 l -13.209368 21.163769 13.965223 21.919624 14.991017 21.919624 c -16.016811 21.919624 16.772667 21.163769 16.772667 20.155996 c -16.772667 16.790653 l -20.137974 16.790653 l -21.163769 16.790653 21.901638 16.034798 21.901638 15.009003 c -21.901638 13.983208 21.163769 13.227352 20.137974 13.227352 c -16.772667 13.227352 l -16.772667 9.862047 l -16.772667 8.854239 16.016811 8.098381 14.991017 8.098381 c -13.965223 8.098381 13.209368 8.854239 13.209368 9.862047 c -13.209368 13.227352 l -9.844025 13.227352 l -8.836217 13.227352 8.098384 13.983208 8.098384 15.009003 c -h -f -n -Q - -endstream -endobj - -3 0 obj - 1426 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Type /Catalog - /Pages 5 0 R - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000001516 00000 n -0000001539 00000 n -0000001712 00000 n -0000001786 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -1845 -%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/Connectivity/Contents.json b/Mastodon/Resources/Assets.xcassets/Connectivity/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Connectivity/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Contents.json new file mode 100644 index 000000000..9c640adf5 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Frame 2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Frame 2.pdf b/Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Frame 2.pdf new file mode 100644 index 000000000..4ce898753 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Frame 2.pdf @@ -0,0 +1,114 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +0.992546 -0.121869 0.121869 0.992546 42.624641 7.462139 cm +0.000000 0.000000 0.000000 scn +29.841717 4.404652 m +10.813622 4.404652 l +5.729721 9.441498 l +29.810324 9.441498 l +32.782593 9.441498 34.628483 11.256016 34.628483 14.259354 c +34.628483 19.077240 l +20.237179 32.404518 l +18.766808 33.781090 16.983574 34.438072 15.262939 34.438072 c +13.481962 34.438072 11.763362 33.813934 10.231857 32.441067 c +0.000000 39.493633 l +11.853184 50.706104 l +1.586006 62.000000 l +29.841717 62.000000 l +36.411587 62.000000 39.665127 58.746330 39.665127 52.301659 c +39.665127 14.102936 l +39.665127 7.658268 36.411587 4.404652 29.841717 4.404652 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 6.000000 11.404663 cm +0.000000 0.000000 0.000000 scn +35.690556 57.595337 m +9.823408 57.595337 l +3.284870 57.595337 0.000000 54.372997 0.000000 47.896996 c +0.000000 9.698273 l +0.000000 3.222317 3.284870 -0.000011 9.823408 -0.000011 c +44.918179 -0.000011 l +39.834278 5.036835 l +9.886006 5.036835 l +6.851334 5.036835 5.036836 6.851357 5.036836 9.917267 c +5.036836 11.825638 l +14.641250 20.209938 l +16.017820 21.430046 17.519461 22.055767 18.896032 22.055767 c +20.428938 22.055767 22.024504 21.430050 23.401012 20.147408 c +29.376427 14.766380 l +44.330532 28.031185 l +44.332489 28.032942 44.334446 28.034697 44.336403 28.036451 c +34.104435 35.089096 l +45.957619 46.301567 l +35.690556 57.595337 l +h +15.736227 35.758499 m +15.736227 31.503782 19.208826 28.031185 23.463608 28.031185 c +27.687059 28.031185 31.159658 31.503782 31.159658 35.758499 c +31.159658 39.981949 27.687059 43.485878 23.463608 43.485878 c +19.208826 43.485878 15.736227 39.981949 15.736227 35.758499 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1681 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 92.000000 76.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001771 00000 n +0000001794 00000 n +0000001967 00000 n +0000002041 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2100 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 92e0161a9..64a3e17e8 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -38,6 +38,10 @@ "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; +"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be +uploaded to Mastodon."; +"Scene.Compose.Attachment.Photo" = "photo"; +"Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.ComposeAction" = "Publish"; "Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind"; "Scene.Compose.Title.NewPost" = "New Post"; diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 0f34a9ffd..7fd5dfd14 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -8,8 +8,9 @@ import os.log import UIKit import Combine -import TwitterTextEditor +import PhotosUI import Kingfisher +import TwitterTextEditor final class ComposeViewController: UIViewController, NeedsDependency { @@ -42,6 +43,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self)) tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none + tableView.showsVerticalScrollIndicator = false return tableView }() @@ -57,6 +59,16 @@ final class ComposeViewController: UIViewController, NeedsDependency { return backgroundView }() + lazy var imagePicker: PHPickerViewController = { + var configuration = PHPickerConfiguration() + configuration.filter = .images + configuration.selectionLimit = 4 + + let imagePicker = PHPickerViewController(configuration: configuration) + imagePicker.delegate = self + return imagePicker + }() + } extension ComposeViewController { @@ -109,7 +121,8 @@ extension ComposeViewController { viewModel.setupDiffableDataSource( for: tableView, dependency: self, - textEditorViewTextAttributesDelegate: self + textEditorViewTextAttributesDelegate: self, + composeStatusAttachmentTableViewCellDelegate: self ) // respond scrollView overlap change @@ -377,15 +390,12 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate { } - - // MARK: - ComposeToolbarViewDelegate extension ComposeViewController: ComposeToolbarViewDelegate { func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let attachmentService = MastodonAttachmentService() - viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService] + present(imagePicker, animated: true, completion: nil) } func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton) { @@ -431,3 +441,29 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { } } + +// MARK: - PHPickerViewControllerDelegate +extension ComposeViewController: PHPickerViewControllerDelegate { + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true, completion: nil) + let attachmentServices = results.map { MastodonAttachmentService(pickerResult: $0) } + viewModel.attachmentServices.value = viewModel.attachmentServices.value + attachmentServices + } +} + +// MARK: - ComposeStatusAttachmentTableViewCellDelegate +extension ComposeViewController: ComposeStatusAttachmentTableViewCellDelegate { + + func composeStatusAttachmentTableViewCell(_ cell: ComposeStatusAttachmentTableViewCell, removeButtonDidPressed button: UIButton) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let indexPath = tableView.indexPath(for: cell) else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + guard case let .attachment(attachmentService) = item else { return } + + var attachmentServices = viewModel.attachmentServices.value + guard let index = attachmentServices.firstIndex(of: attachmentService) else { return } + attachmentServices.remove(at: index) + viewModel.attachmentServices.value = attachmentServices + } + +} diff --git a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift index b58300294..d989d6f46 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift @@ -13,14 +13,16 @@ extension ComposeViewModel { func setupDiffableDataSource( for tableView: UITableView, dependency: NeedsDependency, - textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate + textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate, + composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentTableViewCellDelegate ) { diffableDataSource = ComposeStatusSection.tableViewDiffableDataSource( for: tableView, dependency: dependency, managedObjectContext: context.managedObjectContext, composeKind: composeKind, - textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate + textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate, + composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift index b63a24ebb..88ae255fc 100644 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift @@ -5,13 +5,44 @@ // Created by MainasuK Cirno on 2021-3-17. // +import os.log import UIKit +import Combine + +protocol ComposeStatusAttachmentTableViewCellDelegate: class { + func composeStatusAttachmentTableViewCell(_ cell: ComposeStatusAttachmentTableViewCell, removeButtonDidPressed button: UIButton) +} final class ComposeStatusAttachmentTableViewCell: UITableViewCell { - static let verticalMarginHeight: CGFloat = 8 + var disposeBag = Set() + + static let verticalMarginHeight: CGFloat = ComposeStatusAttachmentTableViewCell.removeButtonSize.height * 0.5 + static let removeButtonSize = CGSize(width: 22, height: 22) + + weak var delegate: ComposeStatusAttachmentTableViewCellDelegate? let attachmentContainerView = AttachmentContainerView() + let removeButton: UIButton = { + let button = HighlightDimmableButton() + button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) + let image = UIImage(systemName: "minus")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)) + button.tintColor = .white + button.setImage(image, for: .normal) + button.setBackgroundImage(.placeholder(color: Asset.Colors.Background.danger.color), for: .normal) + button.layer.masksToBounds = true + button.layer.cornerRadius = ComposeStatusAttachmentTableViewCell.removeButtonSize.width * 0.5 + button.layer.borderColor = Asset.Colors.Background.dangerBorder.color.cgColor + button.layer.borderWidth = 1 + return button + }() + + override func prepareForReuse() { + super.prepareForReuse() + + attachmentContainerView.activityIndicatorView.startAnimating() + delegate = nil + } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -28,6 +59,8 @@ final class ComposeStatusAttachmentTableViewCell: UITableViewCell { extension ComposeStatusAttachmentTableViewCell { private func _init() { + selectionStyle = .none + attachmentContainerView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(attachmentContainerView) NSLayoutConstraint.activate([ @@ -38,8 +71,26 @@ extension ComposeStatusAttachmentTableViewCell { attachmentContainerView.heightAnchor.constraint(equalToConstant: 205).priority(.defaultHigh), ]) - attachmentContainerView.attachmentPreviewImageView.backgroundColor = .systemFill + removeButton.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(removeButton) + NSLayoutConstraint.activate([ + removeButton.centerXAnchor.constraint(equalTo: attachmentContainerView.trailingAnchor), + removeButton.centerYAnchor.constraint(equalTo: attachmentContainerView.topAnchor), + removeButton.widthAnchor.constraint(equalToConstant: ComposeStatusAttachmentTableViewCell.removeButtonSize.width).priority(.defaultHigh), + removeButton.heightAnchor.constraint(equalToConstant: ComposeStatusAttachmentTableViewCell.removeButtonSize.height).priority(.defaultHigh), + ]) + + removeButton.addTarget(self, action: #selector(ComposeStatusAttachmentTableViewCell.removeButtonDidPressed(_:)), for: .touchUpInside) } } + +extension ComposeStatusAttachmentTableViewCell { + + @objc private func removeButtonDidPressed(_ sender: UIButton) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.composeStatusAttachmentTableViewCell(self, removeButtonDidPressed: sender) + } + +} diff --git a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift new file mode 100644 index 000000000..8a0efa800 --- /dev/null +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift @@ -0,0 +1,131 @@ +// +// AttachmentContainerView+EmptyStateView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-18. +// + +import UIKit + +extension AttachmentContainerView { + final class EmptyStateView: UIView { + + static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate) + static let videoSplashImage: UIImage = { + let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64)) + return image + }() + + let imageView: UIImageView = { + let imageView = UIImageView() + imageView.tintColor = Asset.Colors.Label.secondary.color + imageView.image = AttachmentContainerView.EmptyStateView.photoFillSplitImage + return imageView + }() + let label: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) + label.textColor = Asset.Colors.Label.secondary.color + label.textAlignment = .center + label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) + label.numberOfLines = 2 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + + } +} + +extension AttachmentContainerView.EmptyStateView { + private func _init() { + layer.masksToBounds = true + layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius + layer.cornerCurve = .continuous + + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .center + stackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(stackView) + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + let topPaddingView = UIView() + let middlePaddingView = UIView() + let bottomPaddingView = UIView() + + topPaddingView.translatesAutoresizingMaskIntoConstraints = false + stackView.addArrangedSubview(topPaddingView) + imageView.translatesAutoresizingMaskIntoConstraints = false + stackView.addArrangedSubview(imageView) + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh), + imageView.heightAnchor.constraint(equalToConstant: 76).priority(.defaultHigh), + ]) + imageView.setContentHuggingPriority(.required - 1, for: .vertical) + middlePaddingView.translatesAutoresizingMaskIntoConstraints = false + stackView.addArrangedSubview(middlePaddingView) + stackView.addArrangedSubview(label) + bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false + stackView.addArrangedSubview(bottomPaddingView) + NSLayoutConstraint.activate([ + topPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5), + bottomPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5), + ]) + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct AttachmentContainerView_EmptyStateView_Previews: PreviewProvider { + + static var previews: some View { + Group { + UIViewPreview(width: 375) { + let emptyStateView = AttachmentContainerView.EmptyStateView() + NSLayoutConstraint.activate([ + emptyStateView.heightAnchor.constraint(equalToConstant: 205) + ]) + return emptyStateView + } + .previewLayout(.fixed(width: 375, height: 205)) + UIViewPreview(width: 375) { + let emptyStateView = AttachmentContainerView.EmptyStateView() + NSLayoutConstraint.activate([ + emptyStateView.heightAnchor.constraint(equalToConstant: 205) + ]) + return emptyStateView + } + .preferredColorScheme(.dark) + .previewLayout(.fixed(width: 375, height: 205)) + UIViewPreview(width: 375) { + let emptyStateView = AttachmentContainerView.EmptyStateView() + emptyStateView.imageView.image = AttachmentContainerView.EmptyStateView.videoSplashImage + emptyStateView.label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) + + NSLayoutConstraint.activate([ + emptyStateView.heightAnchor.constraint(equalToConstant: 205) + ]) + return emptyStateView + } + .previewLayout(.fixed(width: 375, height: 205)) + } + } + +} + +#endif + diff --git a/Mastodon/Scene/Compose/View/AttachmentContainerView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView.swift index f098d983c..d61f2e672 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView.swift @@ -8,16 +8,20 @@ import UIKit final class AttachmentContainerView: UIView { + + static let containerViewCornerRadius: CGFloat = 4 - let attachmentPreviewImageView: UIImageView = { + let activityIndicatorView = UIActivityIndicatorView(style: .medium) + + let previewImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = 4 - imageView.layer.cornerCurve = .continuous return imageView }() + let emptyStateView = AttachmentContainerView.EmptyStateView() + override init(frame: CGRect) { super.init(frame: frame) _init() @@ -33,16 +37,34 @@ final class AttachmentContainerView: UIView { extension AttachmentContainerView { private func _init() { - - attachmentPreviewImageView.translatesAutoresizingMaskIntoConstraints = false - addSubview(attachmentPreviewImageView) + previewImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(previewImageView) NSLayoutConstraint.activate([ - attachmentPreviewImageView.topAnchor.constraint(equalTo: topAnchor), - attachmentPreviewImageView.leadingAnchor.constraint(equalTo: leadingAnchor), - attachmentPreviewImageView.trailingAnchor.constraint(equalTo: trailingAnchor), - attachmentPreviewImageView.bottomAnchor.constraint(equalTo: bottomAnchor), + previewImageView.topAnchor.constraint(equalTo: topAnchor), + previewImageView.leadingAnchor.constraint(equalTo: leadingAnchor), + previewImageView.trailingAnchor.constraint(equalTo: trailingAnchor), + previewImageView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) + emptyStateView.translatesAutoresizingMaskIntoConstraints = false + addSubview(emptyStateView) + NSLayoutConstraint.activate([ + emptyStateView.topAnchor.constraint(equalTo: topAnchor), + emptyStateView.leadingAnchor.constraint(equalTo: leadingAnchor), + emptyStateView.trailingAnchor.constraint(equalTo: trailingAnchor), + emptyStateView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false + addSubview(activityIndicatorView) + NSLayoutConstraint.activate([ + activityIndicatorView.centerXAnchor.constraint(equalTo: previewImageView.centerXAnchor), + activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor), + ]) + + emptyStateView.isHidden = true + activityIndicatorView.hidesWhenStopped = true + activityIndicatorView.startAnimating() } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift index c19d45e47..dc7b8a47b 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineNavigationBarView.swift @@ -22,7 +22,7 @@ final class HomeTimelineNavigationBarView { }() static let newPostsView: UIView = { - let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.Button.highlight.color) + let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.Button.normal.color) let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.newPosts) HomeTimelineNavigationBarView.addLabelToView(label: label, view: view) return view diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index 63c1e4217..e415c5737 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -37,10 +37,10 @@ final class WelcomeViewController: UIViewController, NeedsDependency { private(set) lazy var signUpButton: PrimaryActionButton = { let button = PrimaryActionButton() button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal) - let backgroundImageColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? .white : Asset.Colors.Button.highlight.color + let backgroundImageColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? .white : Asset.Colors.Button.normal.color button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal) button.setBackgroundImage(.placeholder(color: backgroundImageColor.withAlphaComponent(0.9)), for: .highlighted) - let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? Asset.Colors.Button.highlight.color : UIColor.white + let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? Asset.Colors.Button.normal.color : UIColor.white button.setTitleColor(titleColor, for: .normal) button.translatesAutoresizingMaskIntoConstraints = false return button @@ -50,7 +50,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency { let button = UIButton(type: .system) button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold)) button.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal) - let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? UIColor.white.withAlphaComponent(0.8) : Asset.Colors.Button.highlight.color + let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? UIColor.white.withAlphaComponent(0.8) : Asset.Colors.Button.normal.color button.setTitleColor(titleColor, for: .normal) button.translatesAutoresizingMaskIntoConstraints = false return button diff --git a/Mastodon/Scene/Share/View/Button/HighlightDimmableButton.swift b/Mastodon/Scene/Share/View/Button/HighlightDimmableButton.swift index 3eb916f26..5202d376a 100644 --- a/Mastodon/Scene/Share/View/Button/HighlightDimmableButton.swift +++ b/Mastodon/Scene/Share/View/Button/HighlightDimmableButton.swift @@ -9,6 +9,8 @@ import UIKit final class HighlightDimmableButton: UIButton { + var expandEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + override init(frame: CGRect) { super.init(frame: frame) _init() @@ -19,6 +21,9 @@ final class HighlightDimmableButton: UIButton { _init() } + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return bounds.inset(by: expandEdgeInsets).contains(point) + } override var isHighlighted: Bool { didSet { diff --git a/Mastodon/Scene/Share/View/Button/PrimaryActionButton.swift b/Mastodon/Scene/Share/View/Button/PrimaryActionButton.swift index 0d68cd74d..aa36fd237 100644 --- a/Mastodon/Scene/Share/View/Button/PrimaryActionButton.swift +++ b/Mastodon/Scene/Share/View/Button/PrimaryActionButton.swift @@ -38,8 +38,8 @@ extension PrimaryActionButton { private func _init() { titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)) setTitleColor(.white, for: .normal) - setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.highlight.color), for: .normal) - setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.highlight.color.withAlphaComponent(0.5)), for: .highlighted) + setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.normal.color), for: .normal) + setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.normal.color.withAlphaComponent(0.5)), for: .highlighted) setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) applyCornerRadius(radius: 10) } diff --git a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift index 980e5ae87..336fade8f 100644 --- a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift @@ -22,7 +22,7 @@ final class AudioContainerView: UIView { stackView.isLayoutMarginsRelativeArrangement = true stackView.layer.cornerRadius = AudioContainerView.cornerRadius stackView.clipsToBounds = true - stackView.backgroundColor = Asset.Colors.Button.highlight.color + stackView.backgroundColor = Asset.Colors.Background.AudioPlayer.highlight.color stackView.translatesAutoresizingMaskIntoConstraints = false return stackView }() @@ -31,7 +31,7 @@ final class AudioContainerView: UIView { let view = UIView() view.layer.cornerRadius = 16 view.clipsToBounds = true - view.backgroundColor = Asset.Colors.Button.highlight.color + view.backgroundColor = Asset.Colors.Background.AudioPlayer.highlight.color view.translatesAutoresizingMaskIntoConstraints = false return view }() @@ -109,3 +109,20 @@ extension AudioContainerView { ]) } } + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct AudioContainerView_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 375) { + AudioContainerView() + } + .previewLayout(.fixed(width: 375, height: 100)) + } + +} + +#endif + diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 3987aa5fc..0ceb248a6 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -151,8 +151,8 @@ final class StatusView: UIView { let button = HitTestExpandedButton() button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold)) button.setTitle(L10n.Common.Controls.Status.Poll.vote, for: .normal) - button.setTitleColor(Asset.Colors.Button.highlight.color, for: .normal) - button.setTitleColor(Asset.Colors.Button.highlight.color.withAlphaComponent(0.8), for: .highlighted) + button.setTitleColor(Asset.Colors.Button.normal.color, for: .normal) + button.setTitleColor(Asset.Colors.Button.normal.color.withAlphaComponent(0.8), for: .highlighted) button.setTitleColor(Asset.Colors.Button.disabled.color, for: .disabled) button.isEnabled = false return button diff --git a/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift index 7aa7ef41d..2fd3a023d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift @@ -35,7 +35,7 @@ final class PollOptionTableViewCell: UITableViewCell { let imageView = UIImageView() let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))! imageView.image = image.withRenderingMode(.alwaysTemplate) - imageView.tintColor = Asset.Colors.Button.highlight.color + imageView.tintColor = Asset.Colors.Button.normal.color return imageView }() diff --git a/Mastodon/Service/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService.swift index bdf6da657..e845d2d7e 100644 --- a/Mastodon/Service/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService.swift @@ -7,11 +7,41 @@ import UIKit import Combine +import PhotosUI final class MastodonAttachmentService { + var disposeBag = Set() + let identifier = UUID() + // input + let pickerResult: PHPickerResult + + // output + let imageData = CurrentValueSubject(nil) + let error = CurrentValueSubject(nil) + + init(pickerResult: PHPickerResult) { + self.pickerResult = pickerResult + // end init + + PHPickerResultLoader.loadImageData(from: pickerResult) + .sink { [weak self] completion in + guard let self = self else { return } + switch completion { + case .failure(let error): + self.error.value = error + case .finished: + break + } + } receiveValue: { [weak self] imageData in + guard let self = self else { return } + self.imageData.value = imageData + } + .store(in: &disposeBag) + } + } extension MastodonAttachmentService: Equatable, Hashable { diff --git a/Mastodon/Vender/PHPickerResultLoader.swift b/Mastodon/Vender/PHPickerResultLoader.swift new file mode 100644 index 000000000..7e083001c --- /dev/null +++ b/Mastodon/Vender/PHPickerResultLoader.swift @@ -0,0 +1,72 @@ +// +// PHPickerResultLoader.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-18. +// + +import os.log +import Foundation +import Combine +import MobileCoreServices +import PhotosUI + +// load image with low memory usage +// Refs: https://christianselig.com/2020/09/phpickerviewcontroller-efficiently/ +enum PHPickerResultLoader { + + static func loadImageData(from result: PHPickerResult) -> Future { + Future { promise in + result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, error in + if let error = error { + promise(.failure(error)) + return + } + + guard let url = url else { + promise(.success(nil)) + return + } + + let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary + guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else { + return + } + + let downsampleOptions = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: 4096, + ] as CFDictionary + + guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { + return + } + + let data = NSMutableData() + guard let imageDestination = CGImageDestinationCreateWithData(data, kUTTypeJPEG, 1, nil) else { + promise(.success(nil)) + return + } + + let isPNG: Bool = { + guard let utType = cgImage.utType else { return false } + return (utType as String) == UTType.png.identifier + }() + + let destinationProperties = [ + kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75 + ] as CFDictionary + + CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties) + CGImageDestinationFinalize(imageDestination) + + 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)) + } + } + } + +}