diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 0c42dfc6b..3fe5fe16e 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -7,6 +7,23 @@ + + + + + + + + + + + + + + + + + @@ -110,6 +127,7 @@ + @@ -127,6 +145,7 @@ - + + \ No newline at end of file diff --git a/CoreDataStack/Entity/Attachment.swift b/CoreDataStack/Entity/Attachment.swift new file mode 100644 index 000000000..f3071872f --- /dev/null +++ b/CoreDataStack/Entity/Attachment.swift @@ -0,0 +1,126 @@ +// +// Attachment.swift +// CoreDataStack +// +// Created by MainasuK Cirno on 2021-2-23. +// + +import CoreData +import Foundation + +public final class Attachment: NSManagedObject { + public typealias ID = String + + @NSManaged public private(set) var id: ID + @NSManaged public private(set) var domain: String + @NSManaged public private(set) var typeRaw: String + @NSManaged public private(set) var url: String + @NSManaged public private(set) var previewURL: String + + @NSManaged public private(set) var remoteURL: String? + @NSManaged public private(set) var metaData: Data? + @NSManaged public private(set) var textURL: String? + @NSManaged public private(set) var descriptionString: String? + @NSManaged public private(set) var blurhash: String? + + @NSManaged public private(set) var createdAt: Date + @NSManaged public private(set) var updatedAt: Date + @NSManaged public private(set) var index: NSNumber + + // many-to-one relastionship + @NSManaged public private(set) var toot: Toot? + +} + +public extension Attachment { + + override func awakeFromInsert() { + super.awakeFromInsert() + createdAt = Date() + } + + @discardableResult + static func insert( + into context: NSManagedObjectContext, + property: Property + ) -> Attachment { + let attachment: Attachment = context.insertObject() + + attachment.domain = property.domain + attachment.index = property.index + + attachment.id = property.id + attachment.typeRaw = property.typeRaw + attachment.url = property.url + attachment.previewURL = property.previewURL + + attachment.remoteURL = property.remoteURL + attachment.metaData = property.metaData + attachment.textURL = property.textURL + attachment.descriptionString = property.descriptionString + attachment.blurhash = property.blurhash + + attachment.updatedAt = property.networkDate + + return attachment + } + + func didUpdate(at networkDate: Date) { + self.updatedAt = networkDate + } + +} + +public extension Attachment { + struct Property { + public let domain: String + public let index: NSNumber + + public let id: ID + public let typeRaw: String + public let url: String + + public let previewURL: String + public let remoteURL: String? + public let metaData: Data? + public let textURL: String? + public let descriptionString: String? + public let blurhash: String? + + public let networkDate: Date + + public init( + domain: String, + index: Int, + id: Attachment.ID, + typeRaw: String, + url: String, + previewURL: String, + remoteURL: String?, + metaData: Data?, + textURL: String?, + descriptionString: String?, + blurhash: String?, + networkDate: Date + ) { + self.domain = domain + self.index = NSNumber(value: index) + self.id = id + self.typeRaw = typeRaw + self.url = url + self.previewURL = previewURL + self.remoteURL = remoteURL + self.metaData = metaData + self.textURL = textURL + self.descriptionString = descriptionString + self.blurhash = blurhash + self.networkDate = networkDate + } + } +} + +extension Attachment: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \Attachment.createdAt, ascending: false)] + } +} diff --git a/CoreDataStack/Entity/Toot.swift b/CoreDataStack/Entity/Toot.swift index 7bc3f2261..b37609a21 100644 --- a/CoreDataStack/Entity/Toot.swift +++ b/CoreDataStack/Entity/Toot.swift @@ -39,11 +39,13 @@ public final class Toot: NSManagedObject { // many-to-one relastionship @NSManaged public private(set) var author: MastodonUser @NSManaged public private(set) var reblog: Toot? + + // many-to-many relastionship @NSManaged public private(set) var favouritedBy: Set? @NSManaged public private(set) var rebloggedBy: Set? @NSManaged public private(set) var mutedBy: Set? @NSManaged public private(set) var bookmarkedBy: Set? - + // one-to-one relastionship @NSManaged public private(set) var pinnedBy: MastodonUser? @@ -53,6 +55,7 @@ public final class Toot: NSManagedObject { @NSManaged public private(set) var emojis: Set? @NSManaged public private(set) var tags: Set? @NSManaged public private(set) var homeTimelineIndexes: Set? + @NSManaged public private(set) var mediaAttachments: Set? @NSManaged public private(set) var updatedAt: Date @NSManaged public private(set) var deletedAt: Date? @@ -69,6 +72,7 @@ public extension Toot { mentions: [Mention]?, emojis: [Emoji]?, tags: [Tag]?, + mediaAttachments: [Attachment]?, favouritedBy: MastodonUser?, rebloggedBy: MastodonUser?, mutedBy: MastodonUser?, @@ -115,6 +119,9 @@ public extension Toot { if let tags = tags { toot.mutableSetValue(forKey: #keyPath(Toot.tags)).addObjects(from: tags) } + if let mediaAttachments = mediaAttachments { + toot.mutableSetValue(forKey: #keyPath(Toot.mediaAttachments)).addObjects(from: mediaAttachments) + } if let favouritedBy = favouritedBy { toot.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(favouritedBy) } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0d1c575b5..6b60486f2 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -137,6 +137,10 @@ 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 */; }; + DB9D6C0E25E4F9780051B173 /* MosaicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */; }; + DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; }; + DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; }; + DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; }; DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; }; DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; }; DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; }; @@ -334,6 +338,10 @@ 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 = ""; }; + DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageView.swift; sourceTree = ""; }; + DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = ""; }; + DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; + DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = ""; }; DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = ""; }; DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = ""; }; @@ -547,6 +555,7 @@ 2D7631A425C1532200929FB9 /* Share */ = { isa = PBXGroup; children = ( + DB9D6C2025E502C60051B173 /* ViewModel */, 2D7631A525C1532D00929FB9 /* View */, ); path = Share; @@ -557,6 +566,7 @@ children = ( 2D42FF8325C82245004A627A /* Button */, 2D42FF7C25C82207004A627A /* ToolBar */, + DB9D6C1325E4F97A0051B173 /* Container */, 2D152A8A25C295B8009AA50C /* Content */, 2D7631A625C1533800929FB9 /* TableviewCell */, ); @@ -628,6 +638,7 @@ children = ( DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */, DB084B5625CBC56C00F898ED /* Toot.swift */, + DB9D6C3725E508BE0051B173 /* Attachment.swift */, ); path = CoreDataStack; sourceTree = ""; @@ -816,6 +827,7 @@ 2D927F1325C7EDD9004F19B8 /* Emoji.swift */, DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */, 2DA7D05625CA693F00804E11 /* Application.swift */, + DB9D6C2D25E504AC0051B173 /* Attachment.swift */, ); path = Entity; sourceTree = ""; @@ -935,6 +947,22 @@ path = Profile; sourceTree = ""; }; + DB9D6C1325E4F97A0051B173 /* Container */ = { + isa = PBXGroup; + children = ( + DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */, + ); + path = Container; + sourceTree = ""; + }; + DB9D6C2025E502C60051B173 /* ViewModel */ = { + isa = PBXGroup; + children = ( + DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; DBE0821A25CD382900FD6BBD /* Register */ = { isa = PBXGroup; children = ( @@ -1319,6 +1347,7 @@ DB98338825C945ED00AD9700 /* Assets.swift in Sources */, 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */, DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */, + DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */, 2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */, DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, @@ -1355,6 +1384,7 @@ DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, + DB9D6C0E25E4F9780051B173 /* MosaicImageView.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, @@ -1369,6 +1399,7 @@ DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */, 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */, 2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */, + DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */, DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */, @@ -1412,6 +1443,7 @@ 2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */, DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */, DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */, + DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */, 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */, DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */, DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/TimelineSection.swift b/Mastodon/Diffiable/Section/TimelineSection.swift index 9f75960ba..18fb05086 100644 --- a/Mastodon/Diffiable/Section/TimelineSection.swift +++ b/Mastodon/Diffiable/Section/TimelineSection.swift @@ -34,7 +34,7 @@ extension TimelineSection { // configure cell managedObjectContext.performAndWait { let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex - TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID) + TimelineSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID) } cell.delegate = timelinePostTableViewCellDelegate return cell @@ -45,7 +45,7 @@ extension TimelineSection { // configure cell managedObjectContext.performAndWait { let toot = managedObjectContext.object(with: objectID) as! Toot - TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID) + TimelineSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID) } cell.delegate = timelinePostTableViewCellDelegate return cell @@ -69,21 +69,73 @@ extension TimelineSection { static func configure( cell: StatusTableViewCell, + readableLayoutFrame: CGRect?, timestampUpdatePublisher: AnyPublisher, toot: Toot, requestUserID: String ) { // set header cell.statusView.headerContainerStackView.isHidden = toot.reblog == nil - cell.statusView.headerInfoLabel.text = L10n.Common.Controls.Status.userboosted(toot.author.displayName) + cell.statusView.headerInfoLabel.text = { + let author = toot.author + let name = author.displayName.isEmpty ? author.username : author.displayName + return L10n.Common.Controls.Status.userboosted(name) + }() // set name username avatar - cell.statusView.nameLabel.text = toot.author.displayName - cell.statusView.usernameLabel.text = "@" + toot.author.acct - cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL())) + cell.statusView.nameLabel.text = { + let author = (toot.reblog ?? toot).author + return author.displayName.isEmpty ? author.username : author.displayName + }() + cell.statusView.usernameLabel.text = "@" + (toot.reblog ?? toot).author.acct + cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: (toot.reblog ?? toot).author.avatarImageURL())) // set text cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content) + + // prepare media attachments + let mediaAttachments = Array((toot.reblog ?? toot).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } + + // set image + let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments) + let imageViewMaxSize: CGSize = { + let maxWidth: CGFloat = { + // use timelinePostView width as container width + // that width follows readable width and keep constant width after rotate + let containerFrame = readableLayoutFrame ?? cell.statusView.frame + var containerWidth = containerFrame.width + containerWidth -= 10 + containerWidth -= StatusView.avatarImageSize.width + return containerWidth + }() + let scale: CGFloat = { + switch mosiacImageViewModel.metas.count { + case 1: return 1.3 + default: return 0.7 + } + }() + return CGSize(width: maxWidth, height: maxWidth * scale) + }() + if mosiacImageViewModel.metas.count == 1 { + let meta = mosiacImageViewModel.metas[0] + let imageView = cell.statusView.mosaicImageView.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) + imageView.af.setImage( + withURL: meta.url, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + } else { + let imageViews = cell.statusView.mosaicImageView.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) + for (i, imageView) in imageViews.enumerated() { + let meta = mosiacImageViewModel.metas[i] + imageView.af.setImage( + withURL: meta.url, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + } + } + cell.statusView.mosaicImageView.isHidden = mosiacImageViewModel.metas.isEmpty // toolbar let replyCountTitle: String = { diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index 539be0189..9219701f5 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -41,10 +41,12 @@ extension ActiveLabel { extension ActiveLabel { func config(content: String) { + activeEntities.removeAll() if let parseResult = try? TootContent.parse(toot: content) { - activeEntities.removeAll() text = parseResult.trimmed activeEntities = parseResult.activeEntities + } else { + text = "" } } } diff --git a/Mastodon/Extension/CoreDataStack/Attachment.swift b/Mastodon/Extension/CoreDataStack/Attachment.swift new file mode 100644 index 000000000..e17f9bfef --- /dev/null +++ b/Mastodon/Extension/CoreDataStack/Attachment.swift @@ -0,0 +1,23 @@ +// +// Attachment.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-2-23. +// + +import Foundation +import CoreDataStack +import MastodonSDK + +extension Attachment { + + var type: Mastodon.Entity.Attachment.AttachmentType { + return Mastodon.Entity.Attachment.AttachmentType(rawValue: typeRaw) ?? ._other(typeRaw) + } + + var meta: Mastodon.Entity.Attachment.Meta? { + let decoder = JSONDecoder() + return metaData.flatMap { try? decoder.decode(Mastodon.Entity.Attachment.Meta.self, from: $0) } + } + +} diff --git a/Mastodon/Resources/Preview Assets.xcassets/bradley-dunn.imageset/Contents.json b/Mastodon/Resources/Preview Assets.xcassets/bradley-dunn.imageset/Contents.json new file mode 100644 index 000000000..9e42e6457 --- /dev/null +++ b/Mastodon/Resources/Preview Assets.xcassets/bradley-dunn.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bradley-dunn-miqbDWtOG-o-unsplash.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Preview Assets.xcassets/bradley-dunn.imageset/bradley-dunn-miqbDWtOG-o-unsplash.jpg b/Mastodon/Resources/Preview Assets.xcassets/bradley-dunn.imageset/bradley-dunn-miqbDWtOG-o-unsplash.jpg new file mode 100644 index 000000000..f0068b561 Binary files /dev/null and b/Mastodon/Resources/Preview Assets.xcassets/bradley-dunn.imageset/bradley-dunn-miqbDWtOG-o-unsplash.jpg differ diff --git a/Mastodon/Resources/Preview Assets.xcassets/lucas-ludwig.imageset/Contents.json b/Mastodon/Resources/Preview Assets.xcassets/lucas-ludwig.imageset/Contents.json new file mode 100644 index 000000000..343ab1207 --- /dev/null +++ b/Mastodon/Resources/Preview Assets.xcassets/lucas-ludwig.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "lucas-ludwig-8ARg12PU8nE-unsplash.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Preview Assets.xcassets/lucas-ludwig.imageset/lucas-ludwig-8ARg12PU8nE-unsplash.jpg b/Mastodon/Resources/Preview Assets.xcassets/lucas-ludwig.imageset/lucas-ludwig-8ARg12PU8nE-unsplash.jpg new file mode 100644 index 000000000..05da9354c Binary files /dev/null and b/Mastodon/Resources/Preview Assets.xcassets/lucas-ludwig.imageset/lucas-ludwig-8ARg12PU8nE-unsplash.jpg differ diff --git a/Mastodon/Resources/Preview Assets.xcassets/markus-spiske.imageset/Contents.json b/Mastodon/Resources/Preview Assets.xcassets/markus-spiske.imageset/Contents.json new file mode 100644 index 000000000..45d5d122c --- /dev/null +++ b/Mastodon/Resources/Preview Assets.xcassets/markus-spiske.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "markus-spiske-45R3oFOJt2k-unsplash.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Preview Assets.xcassets/markus-spiske.imageset/markus-spiske-45R3oFOJt2k-unsplash.jpg b/Mastodon/Resources/Preview Assets.xcassets/markus-spiske.imageset/markus-spiske-45R3oFOJt2k-unsplash.jpg new file mode 100644 index 000000000..9e0612e68 Binary files /dev/null and b/Mastodon/Resources/Preview Assets.xcassets/markus-spiske.imageset/markus-spiske-45R3oFOJt2k-unsplash.jpg differ diff --git a/Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/Contents.json b/Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/Contents.json new file mode 100644 index 000000000..75530a923 --- /dev/null +++ b/Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "mrdongok-Z53ognhPjek-unsplash.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/mrdongok-Z53ognhPjek-unsplash.jpg b/Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/mrdongok-Z53ognhPjek-unsplash.jpg new file mode 100644 index 000000000..4e507c8a0 Binary files /dev/null and b/Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/mrdongok-Z53ognhPjek-unsplash.jpg differ diff --git a/Mastodon/Resources/Preview Assets.xcassets/tiraya-adam-QfHEWqPelsc-unsplash.imageset/Contents.json b/Mastodon/Resources/Preview Assets.xcassets/tiraya-adam.imageset/Contents.json similarity index 100% rename from Mastodon/Resources/Preview Assets.xcassets/tiraya-adam-QfHEWqPelsc-unsplash.imageset/Contents.json rename to Mastodon/Resources/Preview Assets.xcassets/tiraya-adam.imageset/Contents.json diff --git a/Mastodon/Resources/Preview Assets.xcassets/tiraya-adam-QfHEWqPelsc-unsplash.imageset/tiraya-adam-QfHEWqPelsc-unsplash.jpg b/Mastodon/Resources/Preview Assets.xcassets/tiraya-adam.imageset/tiraya-adam-QfHEWqPelsc-unsplash.jpg similarity index 100% rename from Mastodon/Resources/Preview Assets.xcassets/tiraya-adam-QfHEWqPelsc-unsplash.imageset/tiraya-adam-QfHEWqPelsc-unsplash.jpg rename to Mastodon/Resources/Preview Assets.xcassets/tiraya-adam.imageset/tiraya-adam-QfHEWqPelsc-unsplash.jpg diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageView.swift b/Mastodon/Scene/Share/View/Container/MosaicImageView.swift new file mode 100644 index 000000000..5f8c877db --- /dev/null +++ b/Mastodon/Scene/Share/View/Container/MosaicImageView.swift @@ -0,0 +1,284 @@ +// +// MosaicImageView.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-2-23. +// + +import os.log +import func AVFoundation.AVMakeRect +import UIKit + +protocol MosaicImageViewPresentable: class { + var mosaicImageView: MosaicImageView { get } +} + +protocol MosaicImageViewDelegate: class { + func mosaicImageView(_ mosaicImageView: MosaicImageView, didTapImageView imageView: UIImageView, atIndex index: Int) +} + +final class MosaicImageView: UIView { + + static let cornerRadius: CGFloat = 4 + + weak var delegate: MosaicImageViewDelegate? + + let container = UIStackView() + var imageViews = [UIImageView]() { + didSet { + imageViews.forEach { imageView in + imageView.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer + tapGesture.addTarget(self, action: #selector(MosaicImageView.photoTapGestureRecognizerHandler(_:))) + imageView.addGestureRecognizer(tapGesture) + } + } + } + + private var containerHeightLayoutConstraint: NSLayoutConstraint! + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension MosaicImageView { + + private func _init() { + container.translatesAutoresizingMaskIntoConstraints = false + addSubview(container) + containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1) + NSLayoutConstraint.activate([ + container.topAnchor.constraint(equalTo: topAnchor), + container.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingAnchor.constraint(equalTo: container.trailingAnchor), + bottomAnchor.constraint(equalTo: container.bottomAnchor), + containerHeightLayoutConstraint + ]) + + container.axis = .horizontal + container.distribution = .fillEqually + } + +} + +extension MosaicImageView { + + func reset() { + container.arrangedSubviews.forEach { subview in + container.removeArrangedSubview(subview) + subview.removeFromSuperview() + } + container.subviews.forEach { subview in + subview.removeFromSuperview() + } + imageViews = [] + + container.spacing = 1 + } + + func setupImageView(aspectRatio: CGSize, maxSize: CGSize) -> UIImageView { + reset() + + let contentView = UIView() + contentView.translatesAutoresizingMaskIntoConstraints = false + container.addArrangedSubview(contentView) + + let rect = AVMakeRect( + aspectRatio: aspectRatio, + insideRect: CGRect(origin: .zero, size: maxSize) + ) + + let imageView = UIImageView() + imageViews.append(imageView) + imageView.layer.masksToBounds = true + imageView.layer.cornerRadius = MosaicImageView.cornerRadius + imageView.contentMode = .scaleAspectFill + + imageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(imageView) + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: contentView.topAnchor), + imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + imageView.widthAnchor.constraint(equalToConstant: floor(rect.width)).priority(.required - 1), + ]) + containerHeightLayoutConstraint.constant = floor(rect.height) + containerHeightLayoutConstraint.isActive = true + + return imageView + } + + func setupImageViews(count: Int, maxHeight: CGFloat) -> [UIImageView] { + reset() + guard count > 1 else { + return [] + } + + containerHeightLayoutConstraint.constant = maxHeight + containerHeightLayoutConstraint.isActive = true + + let contentLeftStackView = UIStackView() + let contentRightStackView = UIStackView() + [contentLeftStackView, contentRightStackView].forEach { stackView in + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.spacing = 1 + } + container.addArrangedSubview(contentLeftStackView) + container.addArrangedSubview(contentRightStackView) + + var imageViews: [UIImageView] = [] + for _ in 0.. Tag in - let histories = tag.history?.compactMap({ (history) -> History in + let histories = tag.history?.compactMap { history -> History in History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts)) - }) + } return Tag.insert(into: managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories)) } + let mediaAttachments: [Attachment]? = { + let encoder = JSONEncoder() + var attachments: [Attachment] = [] + for (index, attachment) in (entity.mediaAttachments ?? []).enumerated() { + let metaData = attachment.meta.flatMap { meta in + try? encoder.encode(meta) + } + let property = Attachment.Property(domain: domain, index: index, id: attachment.id, typeRaw: attachment.type.rawValue, url: attachment.url, previewURL: attachment.previewURL, remoteURL: attachment.remoteURL, metaData: metaData, textURL: attachment.textURL, descriptionString: attachment.description, blurhash: attachment.blurhash, networkDate: networkDate) + attachments.append(Attachment.insert(into: managedObjectContext, property: property)) + } + guard !attachments.isEmpty else { return nil } + return attachments + }() let tootProperty = Toot.Property(entity: entity, domain: domain, networkDate: networkDate) let toot = Toot.insert( into: managedObjectContext, @@ -73,6 +86,7 @@ extension APIService.CoreData { mentions: metions, emojis: emojis, tags: tags, + mediaAttachments: mediaAttachments, favouritedBy: (entity.favourited ?? false) ? requestMastodonUser : nil, rebloggedBy: (entity.reblogged ?? false) ? requestMastodonUser : nil, mutedBy: (entity.muted ?? false) ? requestMastodonUser : nil, diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Attachment.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Attachment.swift index 9c1a34106..2a09ccfc8 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Attachment.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Attachment.swift @@ -47,6 +47,7 @@ extension Mastodon.Entity { } extension Mastodon.Entity.Attachment { + public typealias AttachmentType = Type public enum `Type`: RawRepresentable, Codable { case unknown case image diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift index 4b820b235..31a8806a7 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift @@ -14,7 +14,7 @@ extension Mastodon.Entity { /// - Since: 0.1.0 /// - Version: 3.3.0 /// # Last Update - /// 2021/1/28 + /// 2021/2/23 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/status/) public class Status: Codable { @@ -31,7 +31,7 @@ extension Mastodon.Entity { public let visibility: Visibility? public let sensitive: Bool? public let spoilerText: String? - public let mediaAttachments: [Attachment] + public let mediaAttachments: [Attachment]? public let application: Application? // Rendering