forked from zelo72/mastodon-ios
Merge branch 'feature/profile-timeline' into develop
This commit is contained in:
commit
d37c7a1200
|
@ -207,6 +207,7 @@
|
|||
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; };
|
||||
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; };
|
||||
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */; };
|
||||
DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */; };
|
||||
DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; };
|
||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
||||
|
@ -414,6 +415,8 @@
|
|||
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
|
||||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; };
|
||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
||||
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; };
|
||||
DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; };
|
||||
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
|
||||
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
||||
|
@ -779,6 +782,7 @@
|
|||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = "<group>"; };
|
||||
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
||||
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = "<group>"; };
|
||||
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderImageCacheService.swift; sourceTree = "<group>"; };
|
||||
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
||||
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -984,6 +988,7 @@
|
|||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
|
||||
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
|
||||
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
||||
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; };
|
||||
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; };
|
||||
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
||||
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1045,6 +1050,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */,
|
||||
DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */,
|
||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
||||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||
|
@ -1350,6 +1356,8 @@
|
|||
DB4924E126312AB200E9DB22 /* NotificationService.swift */,
|
||||
DB6D9F6226357848008423CD /* SettingService.swift */,
|
||||
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
|
||||
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
|
||||
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2497,6 +2505,7 @@
|
|||
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
||||
DBB525072611EAC0002F1F29 /* Tabman */,
|
||||
DB6F5E31264E7410009108F4 /* TwitterTextEditor */,
|
||||
DBAEDE5E267A0B1500D25FF5 /* Nuke */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
|
@ -2686,6 +2695,7 @@
|
|||
DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */,
|
||||
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||
DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -2995,6 +3005,7 @@
|
|||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||
DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */,
|
||||
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
|
||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
||||
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */,
|
||||
|
@ -3100,6 +3111,7 @@
|
|||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */,
|
||||
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */,
|
||||
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
||||
|
@ -4027,7 +4039,7 @@
|
|||
repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 5.0.2;
|
||||
version = 5.0.3;
|
||||
};
|
||||
};
|
||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = {
|
||||
|
@ -4102,6 +4114,14 @@
|
|||
minimumVersion = 1.4.1;
|
||||
};
|
||||
};
|
||||
DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kean/Nuke.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 10.3.0;
|
||||
};
|
||||
};
|
||||
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/uias/Tabman";
|
||||
|
@ -4185,6 +4205,11 @@
|
|||
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||
productName = "UITextView+Placeholder";
|
||||
};
|
||||
DBAEDE5E267A0B1500D25FF5 /* Nuke */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||
productName = Nuke;
|
||||
};
|
||||
DBB525072611EAC0002F1F29 /* Tabman */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */;
|
||||
|
|
|
@ -27,12 +27,12 @@
|
|||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>13</integer>
|
||||
<integer>12</integer>
|
||||
</dict>
|
||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>12</integer>
|
||||
<integer>16</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2132a7bf8da2bea74bb49b0b815950ea4d8f6a7e",
|
||||
"version": "5.0.2"
|
||||
"revision": "d503eb3bfabc54a70139618ab2ba09ebb8c09672",
|
||||
"version": "5.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -73,6 +73,15 @@
|
|||
"version": "6.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Nuke",
|
||||
"repositoryURL": "https://github.com/kean/Nuke.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "69ae6d5b8c4b898450432f94bd35f863d3830cfc",
|
||||
"version": "10.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Pageboy",
|
||||
"repositoryURL": "https://github.com/uias/Pageboy",
|
||||
|
|
|
@ -11,6 +11,7 @@ import CoreDataStack
|
|||
import os.log
|
||||
import UIKit
|
||||
import AVKit
|
||||
import Nuke
|
||||
|
||||
protocol StatusCell: DisposeBagCollectable {
|
||||
var statusView: StatusView { get }
|
||||
|
@ -84,7 +85,7 @@ extension StatusSection {
|
|||
case .root:
|
||||
StatusSection.configureThreadMeta(cell: cell, status: status)
|
||||
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { change in
|
||||
|
@ -160,9 +161,9 @@ extension StatusSection {
|
|||
requestUserID: String,
|
||||
statusItemAttribute: Item.StatusAttribute
|
||||
) {
|
||||
// safely cancel the listenser when deleted
|
||||
// safely cancel the listener when deleted
|
||||
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { [weak cell] change in
|
||||
|
@ -174,11 +175,10 @@ extension StatusSection {
|
|||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
|
||||
// set header
|
||||
StatusSection.configureHeader(cell: cell, status: status)
|
||||
ManagedObjectObserver.observe(object: status)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { [weak cell] change in
|
||||
|
@ -221,7 +221,7 @@ extension StatusSection {
|
|||
cell.statusView.updateVisibility(visibility: visibility)
|
||||
|
||||
cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak cell] isHidden in
|
||||
cell?.statusView.visibilityImageView.isHidden = !isHidden
|
||||
}
|
||||
|
@ -234,7 +234,7 @@ extension StatusSection {
|
|||
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
||||
|
||||
// set image
|
||||
let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments)
|
||||
let mosaicImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments)
|
||||
let imageViewMaxSize: CGSize = {
|
||||
let maxWidth: CGFloat = {
|
||||
// use timelinePostView width as container width
|
||||
|
@ -246,61 +246,78 @@ extension StatusSection {
|
|||
return containerWidth
|
||||
}()
|
||||
let scale: CGFloat = {
|
||||
switch mosiacImageViewModel.metas.count {
|
||||
switch mosaicImageViewModel.metas.count {
|
||||
case 1: return 1.3
|
||||
default: return 0.7
|
||||
}
|
||||
}()
|
||||
return CGSize(width: maxWidth, height: maxWidth * scale)
|
||||
}()
|
||||
let blurhashImageCache = dependency.context.documentStore.blurhashImageCache
|
||||
let mosaics: [MosaicImageViewContainer.ConfigurableMosaic] = {
|
||||
if mosiacImageViewModel.metas.count == 1 {
|
||||
let meta = mosiacImageViewModel.metas[0]
|
||||
if mosaicImageViewModel.metas.count == 1 {
|
||||
let meta = mosaicImageViewModel.metas[0]
|
||||
let mosaic = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize)
|
||||
return [mosaic]
|
||||
} else {
|
||||
let mosaics = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height)
|
||||
let mosaics = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosaicImageViewModel.metas.count, maxSize: imageViewMaxSize)
|
||||
return mosaics
|
||||
}
|
||||
}()
|
||||
for (i, mosiac) in mosaics.enumerated() {
|
||||
let (imageView, blurhashOverlayImageView) = mosiac
|
||||
let meta = mosiacImageViewModel.metas[i]
|
||||
let blurhashImageDataKey = meta.url.absoluteString as NSString
|
||||
if let blurhashImageData = blurhashImageCache.object(forKey: meta.url.absoluteString as NSString),
|
||||
let image = UIImage(data: blurhashImageData as Data) {
|
||||
blurhashOverlayImageView.image = image
|
||||
} else {
|
||||
meta.blurhashImagePublisher()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak blurhashImageCache] image in
|
||||
guard let blurhashImageCache = blurhashImageCache else { return }
|
||||
blurhashOverlayImageView.image = image
|
||||
image?.pngData().flatMap {
|
||||
blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey)
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
imageView.af.setImage(
|
||||
withURL: meta.url,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
) { response in
|
||||
switch response.result {
|
||||
case .success:
|
||||
statusItemAttribute.isImageLoaded.value = true
|
||||
for (i, mosaic) in mosaics.enumerated() {
|
||||
let imageView = mosaic.imageView
|
||||
let blurhashOverlayImageView = mosaic.blurhashOverlayImageView
|
||||
let meta = mosaicImageViewModel.metas[i]
|
||||
|
||||
meta.blurhashImagePublisher()
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { image in
|
||||
blurhashOverlayImageView.image = image
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
let imageSize = CGSize(
|
||||
width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale,
|
||||
height: mosaic.imageViewSize.height * imageView.traitCollection.displayScale
|
||||
)
|
||||
let request = ImageRequest(
|
||||
url: meta.url,
|
||||
processors: [
|
||||
ImageProcessors.Resize(size: imageSize, contentMode: .aspectFill)
|
||||
]
|
||||
)
|
||||
let options = ImageLoadingOptions(
|
||||
transition: .fadeIn(duration: 0.2)
|
||||
)
|
||||
Nuke.loadImage(
|
||||
with: request,
|
||||
options: options,
|
||||
into: imageView
|
||||
) { result in
|
||||
switch result {
|
||||
case .failure:
|
||||
break
|
||||
case .success:
|
||||
statusItemAttribute.isImageLoaded.value = true
|
||||
}
|
||||
}
|
||||
//imageView.af.setImage(
|
||||
// withURL: meta.url,
|
||||
// placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
// imageTransition: .crossDissolve(0.2)
|
||||
//) { response in
|
||||
// switch response.result {
|
||||
// case .success:
|
||||
// statusItemAttribute.isImageLoaded.value = true
|
||||
// case .failure:
|
||||
// break
|
||||
// }
|
||||
//}
|
||||
imageView.accessibilityLabel = meta.altText
|
||||
Publishers.CombineLatest(
|
||||
statusItemAttribute.isImageLoaded,
|
||||
statusItemAttribute.isRevealing
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak cell] isImageLoaded, isMediaRevealing in
|
||||
guard let cell = cell else { return }
|
||||
guard isImageLoaded else {
|
||||
|
@ -322,7 +339,7 @@ extension StatusSection {
|
|||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
|
||||
cell.statusView.statusMosaicImageViewContainer.isHidden = mosaicImageViewModel.metas.isEmpty
|
||||
|
||||
// set audio
|
||||
if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first {
|
||||
|
@ -408,7 +425,7 @@ extension StatusSection {
|
|||
)
|
||||
// observe model change
|
||||
ManagedObjectObserver.observe(object: status)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { [weak dependency, weak cell] change in
|
||||
|
@ -479,7 +496,7 @@ extension StatusSection {
|
|||
|
||||
// observe model change
|
||||
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { [weak dependency, weak cell] change in
|
||||
|
@ -700,7 +717,7 @@ extension StatusSection {
|
|||
ManagedObjectObserver.observe(object: status.authorForUserProvider)
|
||||
.assertNoFailure()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak dependency, weak cell] _, change in
|
||||
guard let cell = cell else { return }
|
||||
guard let dependency = dependency else { return }
|
||||
|
|
|
@ -63,6 +63,7 @@ extension ActiveLabel {
|
|||
extension ActiveLabel {
|
||||
/// status content
|
||||
func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||
attributedText = nil
|
||||
activeEntities.removeAll()
|
||||
|
||||
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
|
||||
|
@ -144,10 +145,10 @@ extension ActiveLabel {
|
|||
element.accessibilityLanguage = accessibilityLanguage
|
||||
elements.append(element)
|
||||
|
||||
for eneity in activeEntities {
|
||||
guard let element = eneity.accessibilityElement(in: self) else { continue }
|
||||
for entity in activeEntities {
|
||||
guard let element = entity.accessibilityElement(in: self) else { continue }
|
||||
var glyphRange = NSRange()
|
||||
layoutManager.characterRange(forGlyphRange: eneity.range, actualGlyphRange: &glyphRange)
|
||||
layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange)
|
||||
let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
element.accessibilityFrame = self.convert(rect, to: nil)
|
||||
element.accessibilityContainer = self
|
||||
|
|
|
@ -22,14 +22,14 @@ extension AvatarConfigurableView {
|
|||
|
||||
public func configure(with configuration: AvatarConfigurableViewConfiguration) {
|
||||
let placeholderImage: UIImage = {
|
||||
let placeholderImage = configuration.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageSize, color: .systemFill)
|
||||
if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 {
|
||||
return placeholderImage
|
||||
.af.imageAspectScaled(toFill: Self.configurableAvatarImageSize)
|
||||
.af.imageRounded(withCornerRadius: Self.configurableAvatarImageCornerRadius, divideRadiusByImageScale: false)
|
||||
} else {
|
||||
return placeholderImage.af.imageRoundedIntoCircle()
|
||||
guard let placeholderImage = configuration.placeholderImage else {
|
||||
return AppContext.shared.placeholderImageCacheService.image(
|
||||
color: .systemFill,
|
||||
size: Self.configurableAvatarImageSize,
|
||||
cornerRadius: Self.configurableAvatarImageCornerRadius
|
||||
)
|
||||
}
|
||||
return placeholderImage
|
||||
}()
|
||||
|
||||
// cancel previous task
|
||||
|
|
|
@ -107,7 +107,11 @@ extension MosaicImageViewContainer {
|
|||
container.spacing = 1
|
||||
}
|
||||
|
||||
typealias ConfigurableMosaic = (imageView: UIImageView, blurhashOverlayImageView: UIImageView)
|
||||
struct ConfigurableMosaic {
|
||||
let imageView: UIImageView
|
||||
let blurhashOverlayImageView: UIImageView
|
||||
let imageViewSize: CGSize
|
||||
}
|
||||
|
||||
func setupImageView(aspectRatio: CGSize, maxSize: CGSize) -> ConfigurableMosaic {
|
||||
reset()
|
||||
|
@ -163,15 +167,21 @@ extension MosaicImageViewContainer {
|
|||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
|
||||
])
|
||||
|
||||
return (imageView, blurhashOverlayImageView)
|
||||
return ConfigurableMosaic(
|
||||
imageView: imageView,
|
||||
blurhashOverlayImageView: blurhashOverlayImageView,
|
||||
imageViewSize: maxSize
|
||||
)
|
||||
}
|
||||
|
||||
func setupImageViews(count: Int, maxHeight: CGFloat) -> [ConfigurableMosaic] {
|
||||
func setupImageViews(count: Int, maxSize: CGSize) -> [ConfigurableMosaic] {
|
||||
reset()
|
||||
guard count > 1 else {
|
||||
return []
|
||||
}
|
||||
|
||||
let maxHeight = maxSize.height
|
||||
|
||||
containerHeightLayoutConstraint.constant = maxHeight
|
||||
containerHeightLayoutConstraint.isActive = true
|
||||
|
||||
|
@ -295,7 +305,35 @@ extension MosaicImageViewContainer {
|
|||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||
])
|
||||
|
||||
return zip(imageViews, blurhashOverlayImageViews).map { ($0, $1) }
|
||||
var mosaics: [ConfigurableMosaic] = []
|
||||
for (i, (imageView, blurhashOverlayImageView)) in zip(imageViews, blurhashOverlayImageViews).enumerated() {
|
||||
let imageViewSize: CGSize = {
|
||||
switch (i, count) {
|
||||
case (_, 4):
|
||||
return CGSize(width: maxSize.width * 0.5, height: maxSize.height * 0.5)
|
||||
case (i, 3):
|
||||
let width = maxSize.width * 0.5
|
||||
if i == 0 {
|
||||
return CGSize(width: width, height: maxSize.height)
|
||||
} else {
|
||||
return CGSize(width: width, height: maxSize.height * 0.5)
|
||||
}
|
||||
case (_, 2):
|
||||
let width = maxSize.width * 0.5
|
||||
return CGSize(width: width, height: maxSize.height)
|
||||
default:
|
||||
assertionFailure()
|
||||
return maxSize
|
||||
}
|
||||
}()
|
||||
let mosaic = ConfigurableMosaic(
|
||||
imageView: imageView,
|
||||
blurhashOverlayImageView: blurhashOverlayImageView,
|
||||
imageViewSize: imageViewSize
|
||||
)
|
||||
mosaics.append(mosaic)
|
||||
}
|
||||
return mosaics
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -366,11 +404,11 @@ struct MosaicImageView_Previews: PreviewProvider {
|
|||
UIViewPreview(width: 375) {
|
||||
let view = MosaicImageViewContainer()
|
||||
let image = images[3]
|
||||
let (imageView, _) = view.setupImageView(
|
||||
let mosaic = view.setupImageView(
|
||||
aspectRatio: image.size,
|
||||
maxSize: CGSize(width: 375, height: 400)
|
||||
)
|
||||
imageView.image = image
|
||||
mosaic.imageView.image = image
|
||||
return view
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 400))
|
||||
|
@ -378,14 +416,14 @@ struct MosaicImageView_Previews: PreviewProvider {
|
|||
UIViewPreview(width: 375) {
|
||||
let view = MosaicImageViewContainer()
|
||||
let image = images[1]
|
||||
let (imageView, _) = view.setupImageView(
|
||||
let mosaic = view.setupImageView(
|
||||
aspectRatio: image.size,
|
||||
maxSize: CGSize(width: 375, height: 400)
|
||||
)
|
||||
imageView.layer.masksToBounds = true
|
||||
imageView.layer.cornerRadius = 8
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.image = image
|
||||
mosaic.imageView.layer.masksToBounds = true
|
||||
mosaic.imageView.layer.cornerRadius = 8
|
||||
mosaic.imageView.contentMode = .scaleAspectFill
|
||||
mosaic.imageView.image = image
|
||||
return view
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 400))
|
||||
|
@ -393,10 +431,9 @@ struct MosaicImageView_Previews: PreviewProvider {
|
|||
UIViewPreview(width: 375) {
|
||||
let view = MosaicImageViewContainer()
|
||||
let images = self.images.prefix(2)
|
||||
let mosaics = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||
for (i, mosiac) in mosaics.enumerated() {
|
||||
let (imageView, _) = mosiac
|
||||
imageView.image = images[i]
|
||||
let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162))
|
||||
for (i, mosaic) in mosaics.enumerated() {
|
||||
mosaic.imageView.image = images[i]
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
@ -405,10 +442,9 @@ struct MosaicImageView_Previews: PreviewProvider {
|
|||
UIViewPreview(width: 375) {
|
||||
let view = MosaicImageViewContainer()
|
||||
let images = self.images.prefix(3)
|
||||
let mosaics = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||
for (i, mosiac) in mosaics.enumerated() {
|
||||
let (imageView, _) = mosiac
|
||||
imageView.image = images[i]
|
||||
let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162))
|
||||
for (i, mosaic) in mosaics.enumerated() {
|
||||
mosaic.imageView.image = images[i]
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
@ -417,10 +453,9 @@ struct MosaicImageView_Previews: PreviewProvider {
|
|||
UIViewPreview(width: 375) {
|
||||
let view = MosaicImageViewContainer()
|
||||
let images = self.images.prefix(4)
|
||||
let mosaics = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||
for (i, mosiac) in mosaics.enumerated() {
|
||||
let (imageView, _) = mosiac
|
||||
imageView.image = images[i]
|
||||
let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162))
|
||||
for (i, mosaic) in mosaics.enumerated() {
|
||||
mosaic.imageView.image = images[i]
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
|
|
@ -619,10 +619,9 @@ struct StatusView_Previews: PreviewProvider {
|
|||
)
|
||||
statusView.headerContainerView.isHidden = false
|
||||
let images = MosaicImageView_Previews.images
|
||||
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162)
|
||||
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162))
|
||||
for (i, mosaic) in mosaics.enumerated() {
|
||||
let (imageView, _) = mosaic
|
||||
imageView.image = images[i]
|
||||
mosaic.imageView.image = images[i]
|
||||
}
|
||||
statusView.statusMosaicImageViewContainer.isHidden = false
|
||||
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = true
|
||||
|
@ -644,10 +643,9 @@ struct StatusView_Previews: PreviewProvider {
|
|||
statusView.updateContentWarningDisplay(isHidden: false, animated: false)
|
||||
statusView.drawContentWarningImageView()
|
||||
let images = MosaicImageView_Previews.images
|
||||
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162)
|
||||
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162))
|
||||
for (i, mosaic) in mosaics.enumerated() {
|
||||
let (imageView, _) = mosaic
|
||||
imageView.image = images[i]
|
||||
mosaic.imageView.image = images[i]
|
||||
}
|
||||
statusView.statusMosaicImageViewContainer.isHidden = false
|
||||
return statusView
|
||||
|
|
|
@ -45,40 +45,12 @@ struct MosaicMeta {
|
|||
let size: CGSize
|
||||
let blurhash: String?
|
||||
let altText: String?
|
||||
|
||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent)
|
||||
|
||||
func blurhashImagePublisher() -> AnyPublisher<UIImage?, Never> {
|
||||
return Future { promise in
|
||||
workingQueue.async {
|
||||
let image = self.blurhashImage()
|
||||
promise(.success(image))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func blurhashImage() -> UIImage? {
|
||||
guard let blurhash = blurhash else {
|
||||
return nil
|
||||
return Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let imageSize: CGSize = {
|
||||
let aspectRadio = size.width / size.height
|
||||
if size.width > size.height {
|
||||
let width: CGFloat = MosaicMeta.edgeMaxLength
|
||||
let height = width / aspectRadio
|
||||
return CGSize(width: width, height: height)
|
||||
} else {
|
||||
let height: CGFloat = MosaicMeta.edgeMaxLength
|
||||
let width = height * aspectRadio
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
}()
|
||||
|
||||
let image = UIImage(blurHash: blurhash, size: imageSize)
|
||||
|
||||
return image
|
||||
return AppContext.shared.blurhashImageCacheService.image(blurhash: blurhash, size: size, url: url)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// BlurhashImageCacheService.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-16.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class BlurhashImageCacheService {
|
||||
|
||||
let cache = NSCache<Key, UIImage>()
|
||||
|
||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent)
|
||||
|
||||
func image(blurhash: String, size: CGSize, url: URL) -> AnyPublisher<UIImage?, Never> {
|
||||
return Future { promise in
|
||||
self.workingQueue.async {
|
||||
let key = Key(blurhash: blurhash, size: size, url: url)
|
||||
guard let image = self.cache.object(forKey: key) else {
|
||||
if let image = BlurhashImageCacheService.blurhashImage(blurhash: blurhash, size: size, url: url) {
|
||||
self.cache.setObject(image, forKey: key)
|
||||
promise(.success(image))
|
||||
} else {
|
||||
promise(.success(nil))
|
||||
}
|
||||
return
|
||||
}
|
||||
promise(.success(image))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
static func blurhashImage(blurhash: String, size: CGSize, url: URL) -> UIImage? {
|
||||
let imageSize: CGSize = {
|
||||
let aspectRadio = size.width / size.height
|
||||
if size.width > size.height {
|
||||
let width: CGFloat = MosaicMeta.edgeMaxLength
|
||||
let height = width / aspectRadio
|
||||
return CGSize(width: width, height: height)
|
||||
} else {
|
||||
let height: CGFloat = MosaicMeta.edgeMaxLength
|
||||
let width = height * aspectRadio
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
}()
|
||||
|
||||
let image = UIImage(blurHash: blurhash, size: imageSize)
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension BlurhashImageCacheService {
|
||||
class Key: Hashable {
|
||||
static func == (lhs: BlurhashImageCacheService.Key, rhs: BlurhashImageCacheService.Key) -> Bool {
|
||||
return lhs.blurhash == rhs.blurhash
|
||||
&& lhs.size == rhs.size
|
||||
&& lhs.url == rhs.url
|
||||
}
|
||||
|
||||
let blurhash: String
|
||||
let size: CGSize
|
||||
let url: URL
|
||||
|
||||
init(blurhash: String, size: CGSize, url: URL) {
|
||||
self.blurhash = blurhash
|
||||
self.size = size
|
||||
self.url = url
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(blurhash)
|
||||
hasher.combine(size.width)
|
||||
hasher.combine(size.height)
|
||||
hasher.combine(url)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// PlaceholderImageCacheService.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-16.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AlamofireImage
|
||||
|
||||
final class PlaceholderImageCacheService {
|
||||
|
||||
let cache = NSCache<Key, UIImage>()
|
||||
|
||||
func image(color: UIColor, size: CGSize, cornerRadius: CGFloat = 0) -> UIImage {
|
||||
let key = Key(color: color, size: size, cornerRadius: cornerRadius)
|
||||
guard let image = cache.object(forKey: key) else {
|
||||
var image = UIImage.placeholder(size: size, color: color)
|
||||
if cornerRadius < size.width * 0.5 {
|
||||
image = image
|
||||
.af.imageAspectScaled(toFill: size)
|
||||
.af.imageRounded(withCornerRadius: cornerRadius, divideRadiusByImageScale: false)
|
||||
} else {
|
||||
image = image.af.imageRoundedIntoCircle()
|
||||
}
|
||||
cache.setObject(image, forKey: key)
|
||||
return image
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PlaceholderImageCacheService {
|
||||
class Key: Hashable {
|
||||
let color: UIColor
|
||||
let size: CGSize
|
||||
let cornerRadius: CGFloat
|
||||
|
||||
init(color: UIColor, size: CGSize, cornerRadius: CGFloat) {
|
||||
self.color = color
|
||||
self.size = size
|
||||
self.cornerRadius = cornerRadius
|
||||
}
|
||||
|
||||
static func == (lhs: PlaceholderImageCacheService.Key, rhs: PlaceholderImageCacheService.Key) -> Bool {
|
||||
return lhs.color == rhs.color
|
||||
&& lhs.size == rhs.size
|
||||
&& lhs.cornerRadius == rhs.cornerRadius
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(color)
|
||||
hasher.combine(size.width)
|
||||
hasher.combine(size.height)
|
||||
hasher.combine(cornerRadius)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,6 +35,9 @@ class AppContext: ObservableObject {
|
|||
|
||||
let blockDomainService: BlockDomainService
|
||||
let photoLibraryService = PhotoLibraryService()
|
||||
|
||||
let placeholderImageCacheService = PlaceholderImageCacheService()
|
||||
let blurhashImageCacheService = BlurhashImageCacheService()
|
||||
|
||||
let documentStore: DocumentStore
|
||||
private var documentStoreSubscription: AnyCancellable!
|
||||
|
|
|
@ -10,7 +10,6 @@ import Combine
|
|||
import MastodonSDK
|
||||
|
||||
class DocumentStore: ObservableObject {
|
||||
let blurhashImageCache = NSCache<NSString, NSData>()
|
||||
let appStartUpTimestamp = Date()
|
||||
var defaultRevealStatusDict: [Mastodon.Entity.Status.ID: Bool] = [:]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue