fix: toggle content overlay not works for reblog issue. Update content overlay UI

This commit is contained in:
CMK 2021-06-23 20:47:49 +08:00
parent da9af9a076
commit 51c29fa564
28 changed files with 671 additions and 712 deletions

View File

@ -121,9 +121,8 @@
"user_replied_to": "Replied to %s", "user_replied_to": "Replied to %s",
"show_post": "Show Post", "show_post": "Show Post",
"show_user_profile": "Show user profile", "show_user_profile": "Show user profile",
"content_warning": "content warning", "content_warning": "Content Warning",
"content_warning_text": "cw: %s", "media_content_warning": "Tap anywhere to reveal",
"media_content_warning": "Tap to reveal that may be sensitive",
"poll": { "poll": {
"vote": "Vote", "vote": "Vote",
"vote_count": { "vote_count": {

View File

@ -192,6 +192,7 @@
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; }; DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; };
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; }; DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; }; DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */ = {isa = PBXBuildFile; productRef = DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */; };
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; }; DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; };
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; }; DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; }; DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
@ -1127,6 +1128,7 @@
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */, DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
@ -2647,6 +2649,7 @@
DBAC649D267DFE43007FE9FD /* DiffableDataSources */, DBAC649D267DFE43007FE9FD /* DiffableDataSources */,
DBAC64A0267E6D02007FE9FD /* Fuzi */, DBAC64A0267E6D02007FE9FD /* Fuzi */,
DBF7A0FB26830C33004176A2 /* FPSIndicator */, DBF7A0FB26830C33004176A2 /* FPSIndicator */,
DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */,
); );
productName = Mastodon; productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@ -2839,6 +2842,7 @@
DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */, DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */,
DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */, DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */,
DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */, DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */,
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */,
); );
productRefGroup = DB427DD325BAA00100D1B89D /* Products */; productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -4722,6 +4726,14 @@
minimumVersion = 0.1.1; minimumVersion = 0.1.1;
}; };
}; };
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 8.0.0;
};
};
DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git";
@ -4847,6 +4859,11 @@
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
productName = CommonOSLog; productName = CommonOSLog;
}; };
DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */ = {
isa = XCSwiftPackageProductDependency;
package = DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */;
productName = NukeFLAnimatedImagePlugin;
};
DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;

View File

@ -12,12 +12,12 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>30</integer> <integer>21</integer>
</dict> </dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key> <key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>4</integer>
</dict> </dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key> <key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -37,7 +37,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>27</integer> <integer>22</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -64,6 +64,15 @@
"version": "1.2.0" "version": "1.2.0"
} }
}, },
{
"package": "FLAnimatedImage",
"repositoryURL": "https://github.com/Flipboard/FLAnimatedImage",
"state": {
"branch": null,
"revision": "e7f9fd4681ae41bf6f3056db08af4f401d61da52",
"version": "1.0.16"
}
},
{ {
"package": "FPSIndicator", "package": "FPSIndicator",
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git", "repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",
@ -109,6 +118,15 @@
"version": "10.3.0" "version": "10.3.0"
} }
}, },
{
"package": "NukeFLAnimatedImagePlugin",
"repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
"state": {
"branch": null,
"revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16",
"version": "8.0.0"
}
},
{ {
"package": "Pageboy", "package": "Pageboy",
"repositoryURL": "https://github.com/uias/Pageboy", "repositoryURL": "https://github.com/uias/Pageboy",

View File

@ -41,8 +41,14 @@ enum Item {
extension Item { extension Item {
class StatusAttribute { class StatusAttribute {
var isSeparatorLineHidden: Bool var isSeparatorLineHidden: Bool
/// is media loaded or not
let isImageLoaded = CurrentValueSubject<Bool, Never>(false) let isImageLoaded = CurrentValueSubject<Bool, Never>(false)
/// flag for current sensitive content reveal state
///
/// - true: displaying sensitive content
/// - false: displaying content warning overlay
let isRevealing = CurrentValueSubject<Bool, Never>(false) let isRevealing = CurrentValueSubject<Bool, Never>(false)
init(isSeparatorLineHidden: Bool = false) { init(isSeparatorLineHidden: Bool = false) {

View File

@ -54,7 +54,6 @@ extension NotificationSection {
cell: cell, cell: cell,
dependency: dependency, dependency: dependency,
readableLayoutFrame: frame, readableLayoutFrame: frame,
timestampUpdatePublisher: timestampUpdatePublisher,
status: status, status: status,
requestUserID: requestUserID, requestUserID: requestUserID,
statusItemAttribute: attribute statusItemAttribute: attribute

View File

@ -43,7 +43,6 @@ extension ReportSection {
cell: cell, cell: cell,
dependency: dependency, dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame, readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
status: status, status: status,
requestUserID: requestUserID, requestUserID: requestUserID,
statusItemAttribute: attribute statusItemAttribute: attribute

View File

@ -19,7 +19,6 @@ import AsyncDisplayKit
protocol StatusCell: DisposeBagCollectable { protocol StatusCell: DisposeBagCollectable {
var statusView: StatusView { get } var statusView: StatusView { get }
var pollCountdownSubscription: AnyCancellable? { get set }
} }
enum StatusSection: Equatable, Hashable { enum StatusSection: Equatable, Hashable {
@ -76,24 +75,24 @@ extension StatusSection {
switch item { switch item {
case .homeTimelineIndex(objectID: let objectID, let attribute): case .homeTimelineIndex(objectID: let objectID, let attribute):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex
// note: force check optional for status
// status maybe <uninitialized> here when delete in thread scene
guard let status = timelineIndex?.status,
let userID = timelineIndex?.userID else {
return cell
}
// configure cell // configure cell
managedObjectContext.performAndWait { configureStatusTableViewCell(
let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex cell: cell,
// note: force check optional for status dependency: dependency,
// status maybe <uninitialized> here when delete in thread scene readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
guard let status = timelineIndex?.status, status: status,
let userID = timelineIndex?.userID else { return } requestUserID: userID,
StatusSection.configure( statusItemAttribute: attribute
cell: cell, )
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
status: status,
requestUserID: userID,
statusItemAttribute: attribute
)
}
cell.delegate = statusTableViewCellDelegate cell.delegate = statusTableViewCellDelegate
cell.isAccessibilityElement = true cell.isAccessibilityElement = true
return cell return cell
@ -111,7 +110,6 @@ extension StatusSection {
cell: cell, cell: cell,
dependency: dependency, dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame, readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
status: status, status: status,
requestUserID: requestUserID, requestUserID: requestUserID,
statusItemAttribute: attribute statusItemAttribute: attribute
@ -187,12 +185,29 @@ extension StatusSection {
} }
extension StatusSection { extension StatusSection {
static func configureStatusTableViewCell(
cell: StatusTableViewCell,
dependency: NeedsDependency,
readableLayoutFrame: CGRect?,
status: Status,
requestUserID: String,
statusItemAttribute: Item.StatusAttribute
) {
configure(
cell: cell,
dependency: dependency,
readableLayoutFrame: readableLayoutFrame,
status: status,
requestUserID: requestUserID,
statusItemAttribute: statusItemAttribute
)
}
static func configure( static func configure(
cell: StatusCell, cell: StatusCell,
dependency: NeedsDependency, dependency: NeedsDependency,
readableLayoutFrame: CGRect?, readableLayoutFrame: CGRect?,
timestampUpdatePublisher: AnyPublisher<Date, Never>,
status: Status, status: Status,
requestUserID: String, requestUserID: String,
statusItemAttribute: Item.StatusAttribute statusItemAttribute: Item.StatusAttribute
@ -212,273 +227,28 @@ extension StatusSection {
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
// set header // set header
StatusSection.configureHeader(cell: cell, status: status) StatusSection.configureStatusViewHeader(cell: cell, status: status)
ManagedObjectObserver.observe(object: status) // set author: name + username + avatar
.receive(on: RunLoop.main) StatusSection.configureStatusViewAuthor(cell: cell, status: status)
.sink { _ in // set timestamp
// do nothing let createdAt = (status.reblog ?? status).createdAt
} receiveValue: { [weak cell] change in cell.statusView.dateLabel.text = createdAt.slowedTimeAgoSinceNow
AppContext.shared.timestampUpdatePublisher
.receive(on: RunLoop.main) // will be paused when scrolling (on purpose)
.sink { [weak cell] _ in
guard let cell = cell else { return } guard let cell = cell else { return }
guard case .update(let object) = change.changeType, cell.statusView.dateLabel.text = createdAt.slowedTimeAgoSinceNow
let newStatus = object as? Status else { return } cell.statusView.dateLabel.accessibilityLabel = createdAt.slowedTimeAgoSinceNow
StatusSection.configureHeader(cell: cell, status: newStatus)
} }
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
// set content
// set name username StatusSection.configureStatusContent(
let nameText: String = { cell: cell,
let author = (status.reblog ?? status).author status: status,
return author.displayName.isEmpty ? author.username : author.displayName readableLayoutFrame: readableLayoutFrame,
}() statusItemAttribute: statusItemAttribute
MastodonStatusContent.parseResult(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict)
.receive(on: DispatchQueue.main)
.sink { [weak cell] parseResult in
guard let cell = cell else { return }
cell.statusView.nameLabel.configure(contentParseResult: parseResult)
}
.store(in: &cell.disposeBag)
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
// set avatar
if let reblog = status.reblog {
cell.statusView.avatarButton.isHidden = true
cell.statusView.avatarStackedContainerButton.isHidden = false
cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL()))
cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
} else {
cell.statusView.avatarButton.isHidden = false
cell.statusView.avatarStackedContainerButton.isHidden = true
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
}
// set text
// func configureStatusContent() {
// let content = (status.reblog ?? status).content
// let emojiDict = (status.reblog ?? status).emojiDict
// if let cachedParseResult = AppContext.shared.statusContentCacheService.parseResult(content: content, emojiDict: emojiDict) {
// cell.statusView.activeTextLabel.configure(contentParseResult: cachedParseResult)
// } else {
// cell.statusView.activeTextLabel.configure(
// content: (status.reblog ?? status).content,
// emojiDict: (status.reblog ?? status).emojiDict
// )
// }
// }
// configureStatusContent()
cell.statusView.activeTextLabel.configure(
content: (status.reblog ?? status).content,
emojiDict: (status.reblog ?? status).emojiDict
) )
cell.statusView.activeTextLabel.accessibilityLanguage = (status.reblog ?? status).language // set content warning
// set visibility
if let visibility = (status.reblog ?? status).visibility {
cell.statusView.updateVisibility(visibility: visibility)
cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
.receive(on: DispatchQueue.main)
.sink { [weak cell] isHidden in
cell?.statusView.visibilityImageView.isHidden = !isHidden
}
.store(in: &cell.disposeBag)
} else {
cell.statusView.visibilityImageView.isHidden = true
}
// prepare media attachments
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
// set image
let mosaicImageViewModel = 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 mosaicImageViewModel.metas.count {
case 1: return 1.3
default: return 0.7
}
}()
return CGSize(width: maxWidth, height: floor(maxWidth * scale))
}()
let mosaics: [MosaicImageViewContainer.ConfigurableMosaic] = {
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: mosaicImageViewModel.metas.count, maxSize: imageViewMaxSize)
return mosaics
}
}()
for (i, mosaic) in mosaics.enumerated() {
let imageView = mosaic.imageView
let blurhashOverlayImageView = mosaic.blurhashOverlayImageView
let meta = mosaicImageViewModel.metas[i]
// set blurhash image
meta.blurhashImagePublisher()
.sink { image in
blurhashOverlayImageView.image = image
}
.store(in: &cell.disposeBag)
let isSingleMosaicLayout = mosaics.count == 1
// set image
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,
unit: .pixels,
contentMode: isSingleMosaicLayout ? .aspectFill : .aspectFit,
crop: isSingleMosaicLayout
)
]
)
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.accessibilityLabel = meta.altText
Publishers.CombineLatest(
statusItemAttribute.isImageLoaded,
statusItemAttribute.isRevealing
)
.receive(on: DispatchQueue.main) // needs call immediately
.sink { [weak cell] isImageLoaded, isMediaRevealing in
guard let cell = cell else { return }
guard isImageLoaded else {
blurhashOverlayImageView.alpha = 1
blurhashOverlayImageView.isHidden = false
return
}
blurhashOverlayImageView.alpha = isMediaRevealing ? 0 : 1
if isMediaRevealing {
let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
animator.addAnimations {
blurhashOverlayImageView.alpha = isMediaRevealing ? 0 : 1
}
animator.startAnimation()
} else {
cell.statusView.drawContentWarningImageView()
}
}
.store(in: &cell.disposeBag)
}
cell.statusView.statusMosaicImageViewContainer.isHidden = mosaicImageViewModel.metas.isEmpty
// set audio
if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first {
cell.statusView.audioView.isHidden = false
AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment, audioService: dependency.context.audioPlaybackService)
} else {
cell.statusView.audioView.isHidden = true
}
// set GIF & video
let playerViewMaxSize: CGSize = {
let maxWidth: CGFloat = {
// use statusView width as container width
// that width follows readable width and keep constant width after rotate
let containerFrame = readableLayoutFrame ?? cell.statusView.frame
return containerFrame.width
}()
let scale: CGFloat = 1.3
return CGSize(width: maxWidth, height: floor(maxWidth * scale))
}()
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) {
var parent: UIViewController?
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? = nil
switch cell {
case is StatusTableViewCell:
let statusTableViewCell = cell as! StatusTableViewCell
parent = statusTableViewCell.delegate?.parent()
playerViewControllerDelegate = statusTableViewCell.delegate?.playerViewControllerDelegate
case is NotificationStatusTableViewCell:
let notificationTableViewCell = cell as! NotificationStatusTableViewCell
parent = notificationTableViewCell.delegate?.parent()
case is ReportedStatusTableViewCell:
let reportTableViewCell = cell as! ReportedStatusTableViewCell
parent = reportTableViewCell.dependency
default:
parent = nil
assertionFailure("unknown cell")
}
let playerContainerView = cell.statusView.playerContainerView
let playerViewController = playerContainerView.setupPlayer(
aspectRatio: videoPlayerViewModel.videoSize,
maxSize: playerViewMaxSize,
parent: parent
)
playerViewController.delegate = playerViewControllerDelegate
playerViewController.player = videoPlayerViewModel.player
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
switch videoPlayerViewModel.videoKind {
case .gif:
playerContainerView.setMediaIndicator(isHidden: false)
case .video:
playerContainerView.setMediaIndicator(isHidden: true)
}
playerContainerView.isHidden = false
// set blurhash overlay
playerContainerView.isReadyForDisplay
.receive(on: DispatchQueue.main)
.sink { [weak playerContainerView] isReadyForDisplay in
guard let playerContainerView = playerContainerView else { return }
playerContainerView.blurhashOverlayImageView.alpha = isReadyForDisplay ? 0 : 1
}
.store(in: &cell.disposeBag)
if let blurhash = videoAttachment.blurhash,
let url = URL(string: videoAttachment.url) {
AppContext.shared.blurhashImageCacheService.image(
blurhash: blurhash,
size: playerContainerView.playerViewController.view.frame.size,
url: url
)
.sink { image in
playerContainerView.blurhashOverlayImageView.image = image
}
.store(in: &cell.disposeBag)
}
} else {
cell.statusView.playerContainerView.playerViewController.player?.pause()
cell.statusView.playerContainerView.playerViewController.player = nil
}
// set text content warning
StatusSection.configureContentWarningOverlay( StatusSection.configureContentWarningOverlay(
statusView: cell.statusView, statusView: cell.statusView,
status: status, status: status,
@ -486,36 +256,14 @@ extension StatusSection {
documentStore: dependency.context.documentStore, documentStore: dependency.context.documentStore,
animated: false animated: false
) )
// observe model change
ManagedObjectObserver.observe(object: status)
.receive(on: RunLoop.main)
.sink { _ in
// do nothing
} receiveValue: { [weak dependency, weak cell] change in
guard let cell = cell else { return }
guard let dependency = dependency else { return }
guard case .update(let object) = change.changeType,
let status = object as? Status else { return }
StatusSection.configureContentWarningOverlay(
statusView: cell.statusView,
status: status,
attribute: statusItemAttribute,
documentStore: dependency.context.documentStore,
animated: true
)
}
.store(in: &cell.disposeBag)
// set poll // set poll
let poll = (status.reblog ?? status).poll
StatusSection.configurePoll( StatusSection.configurePoll(
cell: cell, cell: cell,
poll: poll, poll: (status.reblog ?? status).poll,
requestUserID: requestUserID, requestUserID: requestUserID,
updateProgressAnimated: false, updateProgressAnimated: false
timestampUpdatePublisher: timestampUpdatePublisher
) )
if let poll = poll { if let poll = (status.reblog ?? status).poll {
ManagedObjectObserver.observe(object: poll) ManagedObjectObserver.observe(object: poll)
.sink { _ in .sink { _ in
// do nothing // do nothing
@ -527,56 +275,66 @@ extension StatusSection {
cell: cell, cell: cell,
poll: newPoll, poll: newPoll,
requestUserID: requestUserID, requestUserID: requestUserID,
updateProgressAnimated: true, updateProgressAnimated: true
timestampUpdatePublisher: timestampUpdatePublisher
) )
} }
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
} }
// set action toolbar
if let statusTableViewCell = cell as? StatusTableViewCell { if let cell = cell as? StatusTableViewCell {
// toolbar
StatusSection.configureActionToolBar( StatusSection.configureActionToolBar(
cell: statusTableViewCell, cell: cell,
dependency: dependency, dependency: dependency,
status: status, status: status,
requestUserID: requestUserID requestUserID: requestUserID
) )
// separator line // separator line
statusTableViewCell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden cell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden
} }
// set date // listen model changed
let createdAt = (status.reblog ?? status).createdAt ManagedObjectObserver.observe(object: status)
cell.statusView.dateLabel.text = createdAt.slowedTimeAgoSinceNow .receive(on: RunLoop.main)
timestampUpdatePublisher .sink { _ in
.sink { [weak cell] _ in // do nothing
} receiveValue: { [weak cell] change in
guard let cell = cell else { return } guard let cell = cell else { return }
cell.statusView.dateLabel.text = createdAt.slowedTimeAgoSinceNow guard case .update(let object) = change.changeType,
cell.statusView.dateLabel.accessibilityLabel = createdAt.slowedTimeAgoSinceNow let status = object as? Status, !status.isDeleted else {
return
}
// update header
StatusSection.configureStatusViewHeader(cell: cell, status: status)
} }
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
// observe model change
ManagedObjectObserver.observe(object: status.reblog ?? status) ManagedObjectObserver.observe(object: status.reblog ?? status)
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.sink { _ in .sink { _ in
// do nothing // do nothing
} receiveValue: { [weak dependency, weak cell] change in } receiveValue: { [weak cell] change in
guard let dependency = dependency else { return } guard let cell = cell else { return }
guard case .update(let object) = change.changeType, guard case .update(let object) = change.changeType,
let status = object as? Status, let status = object as? Status, !status.isDeleted else {
!status.isDeleted else { return } return
guard let statusTableViewCell = cell as? StatusTableViewCell else { return } }
StatusSection.configureActionToolBar( // update content warning overlay
cell: statusTableViewCell, StatusSection.configureContentWarningOverlay(
dependency: dependency, statusView: cell.statusView,
status: status, status: status,
requestUserID: requestUserID attribute: statusItemAttribute,
documentStore: dependency.context.documentStore,
animated: true
) )
// update action toolbar
os_log("%{public}s[%{public}ld], %{public}s: reblog count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.reblogsCount.intValue) if let cell = cell as? StatusTableViewCell {
os_log("%{public}s[%{public}ld], %{public}s: like count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.favouritesCount.intValue) StatusSection.configureActionToolBar(
cell: cell,
dependency: dependency,
status: status,
requestUserID: requestUserID
)
}
} }
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
} }
@ -593,7 +351,7 @@ extension StatusSection {
if spoilerText.isEmpty { if spoilerText.isEmpty {
return L10n.Common.Controls.Status.contentWarning return L10n.Common.Controls.Status.contentWarning
} else { } else {
return L10n.Common.Controls.Status.contentWarningText(spoilerText) return spoilerText
} }
}() }()
let appStartUpTimestamp = documentStore.appStartUpTimestamp let appStartUpTimestamp = documentStore.appStartUpTimestamp
@ -643,12 +401,12 @@ extension StatusSection {
attribute.isRevealing.value = needsReveal attribute.isRevealing.value = needsReveal
if needsReveal { if needsReveal {
statusView.updateRevealContentWarningButton(isRevealing: true) statusView.updateRevealContentWarningButton(isRevealing: true)
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.update(isRevealing: true, style: .visualEffectView) statusView.statusMosaicImageViewContainer.contentWarningOverlayView.update(isRevealing: true, style: .media)
statusView.playerContainerView.contentWarningOverlayView.update(isRevealing: true, style: .visualEffectView) statusView.playerContainerView.contentWarningOverlayView.update(isRevealing: true, style: .media)
} else { } else {
statusView.updateRevealContentWarningButton(isRevealing: false) statusView.updateRevealContentWarningButton(isRevealing: false)
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.update(isRevealing: false, style: .visualEffectView) statusView.statusMosaicImageViewContainer.contentWarningOverlayView.update(isRevealing: false, style: .media)
statusView.playerContainerView.contentWarningOverlayView.update(isRevealing: false, style: .visualEffectView) statusView.playerContainerView.contentWarningOverlayView.update(isRevealing: false, style: .media)
} }
} }
if animated { if animated {
@ -697,9 +455,8 @@ extension StatusSection {
cell.threadMetaView.isHidden = false cell.threadMetaView.isHidden = false
} }
static func configureHeader( static func configureStatusViewHeader(
cell: StatusCell, cell: StatusCell,
status: Status status: Status
) { ) {
@ -743,80 +500,255 @@ extension StatusSection {
cell.statusView.headerInfoLabel.isAccessibilityElement = false cell.statusView.headerInfoLabel.isAccessibilityElement = false
} }
} }
static func configureActionToolBar( static func configureStatusViewAuthor(
cell: StatusTableViewCell, cell: StatusCell,
dependency: NeedsDependency, status: Status
status: Status,
requestUserID: String
) { ) {
let status = status.reblog ?? status // name
let author = (status.reblog ?? status).author
// set reply let nameContent = author.displayNameWithFallback
let replyCountTitle: String = { cell.statusView.nameLabel.configure(content: nameContent, emojiDict: author.emojiDict)
let count = status.repliesCount?.intValue ?? 0 // username
return StatusSection.formattedNumberTitleForActionButton(count) cell.statusView.usernameLabel.text = "@" + author.acct
}() // avatar
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal) if let reblog = status.reblog {
cell.statusView.actionToolbarContainer.replyButton.accessibilityValue = status.repliesCount.flatMap { cell.statusView.avatarImageView.isHidden = true
L10n.Common.Controls.Timeline.Accessibility.countReplies($0.intValue) cell.statusView.avatarStackedContainerButton.isHidden = false
} ?? nil cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL()))
// set reblog cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false } else {
let reblogCountTitle: String = { cell.statusView.avatarImageView.isHidden = false
let count = status.reblogsCount.intValue cell.statusView.avatarStackedContainerButton.isHidden = true
return StatusSection.formattedNumberTitleForActionButton(count) cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
}() }
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal) }
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged
cell.statusView.actionToolbarContainer.reblogButton.accessibilityLabel = isReblogged ? L10n.Common.Controls.Status.Actions.unreblog : L10n.Common.Controls.Status.Actions.reblog static func configureStatusContent(
cell.statusView.actionToolbarContainer.reblogButton.accessibilityValue = { cell: StatusCell,
guard status.reblogsCount.intValue > 0 else { return nil } status: Status,
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.reblogsCount.intValue) readableLayoutFrame: CGRect?,
}() statusItemAttribute: Item.StatusAttribute
// set like ) {
let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false // set content
let favoriteCountTitle: String = { cell.statusView.activeTextLabel.configure(
let count = status.favouritesCount.intValue content: (status.reblog ?? status).content,
return StatusSection.formattedNumberTitleForActionButton(count) emojiDict: (status.reblog ?? status).emojiDict
}()
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike
cell.statusView.actionToolbarContainer.favoriteButton.accessibilityLabel = isLike ? L10n.Common.Controls.Status.Actions.unfavorite : L10n.Common.Controls.Status.Actions.favorite
cell.statusView.actionToolbarContainer.favoriteButton.accessibilityValue = {
guard status.favouritesCount.intValue > 0 else { return nil }
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue)
}()
Publishers.CombineLatest(
dependency.context.blockDomainService.blockedDomains.setFailureType(to: ManagedObjectObserver.Error.self),
ManagedObjectObserver.observe(object: status.authorForUserProvider)
) )
.receive(on: RunLoop.main) cell.statusView.activeTextLabel.accessibilityLanguage = (status.reblog ?? status).language
.sink(receiveCompletion: { _ in
// do nothing // set visibility
}, receiveValue: { [weak dependency, weak cell] _, change in if let visibility = (status.reblog ?? status).visibility {
guard let cell = cell else { return } cell.statusView.updateVisibility(visibility: visibility)
guard let dependency = dependency else { return }
switch change.changeType { cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
case .delete: .receive(on: DispatchQueue.main)
return .sink { [weak cell] isHidden in
case .update(_): cell?.statusView.visibilityImageView.isHidden = !isHidden
break }
case .none: .store(in: &cell.disposeBag)
break } else {
cell.statusView.visibilityImageView.isHidden = true
}
// prepare media attachments
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
// set image
let mosaicImageViewModel = 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 mosaicImageViewModel.metas.count {
case 1: return 1.3
default: return 0.7
}
}()
return CGSize(width: maxWidth, height: floor(maxWidth * scale))
}()
let mosaics: [MosaicImageViewContainer.ConfigurableMosaic] = {
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: mosaicImageViewModel.metas.count, maxSize: imageViewMaxSize)
return mosaics
} }
StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status) }()
}) for (i, mosaic) in mosaics.enumerated() {
.store(in: &cell.disposeBag) let imageView = mosaic.imageView
self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status) let blurhashOverlayImageView = mosaic.blurhashOverlayImageView
let meta = mosaicImageViewModel.metas[i]
// set blurhash image
meta.blurhashImagePublisher()
.sink { image in
blurhashOverlayImageView.image = image
}
.store(in: &cell.disposeBag)
let isSingleMosaicLayout = mosaics.count == 1
// set image
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,
unit: .pixels,
contentMode: isSingleMosaicLayout ? .aspectFill : .aspectFit,
crop: isSingleMosaicLayout
)
]
)
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.accessibilityLabel = meta.altText
// setup media content overlay trigger
Publishers.CombineLatest(
statusItemAttribute.isImageLoaded,
statusItemAttribute.isRevealing
)
.receive(on: DispatchQueue.main) // needs call immediately
.sink { [weak cell] isImageLoaded, isMediaRevealing in
guard let _ = cell else { return }
guard isImageLoaded else {
// always display blurhash image when before image loaded
blurhashOverlayImageView.alpha = 1
blurhashOverlayImageView.isHidden = false
return
}
// display blurhash image depends on revealing state
let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
animator.addAnimations {
blurhashOverlayImageView.alpha = isMediaRevealing ? 0 : 1
}
animator.startAnimation()
}
.store(in: &cell.disposeBag)
}
cell.statusView.statusMosaicImageViewContainer.isHidden = mosaicImageViewModel.metas.isEmpty
// set audio
if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first {
cell.statusView.audioView.isHidden = false
AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment, audioService: AppContext.shared.audioPlaybackService)
} else {
cell.statusView.audioView.isHidden = true
}
// set GIF & video
let playerViewMaxSize: CGSize = {
let maxWidth: CGFloat = {
// use statusView width as container width
// that width follows readable width and keep constant width after rotate
let containerFrame = readableLayoutFrame ?? cell.statusView.frame
return containerFrame.width
}()
let scale: CGFloat = 1.3
return CGSize(width: maxWidth, height: floor(maxWidth * scale))
}()
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
let videoPlayerViewModel = AppContext.shared.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) {
var parent: UIViewController?
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? = nil
switch cell {
case is StatusTableViewCell:
let statusTableViewCell = cell as! StatusTableViewCell
parent = statusTableViewCell.delegate?.parent()
playerViewControllerDelegate = statusTableViewCell.delegate?.playerViewControllerDelegate
case is NotificationStatusTableViewCell:
let notificationTableViewCell = cell as! NotificationStatusTableViewCell
parent = notificationTableViewCell.delegate?.parent()
case is ReportedStatusTableViewCell:
let reportTableViewCell = cell as! ReportedStatusTableViewCell
parent = reportTableViewCell.dependency
default:
parent = nil
assertionFailure("unknown cell")
}
let playerContainerView = cell.statusView.playerContainerView
let playerViewController = playerContainerView.setupPlayer(
aspectRatio: videoPlayerViewModel.videoSize,
maxSize: playerViewMaxSize,
parent: parent
)
playerViewController.delegate = playerViewControllerDelegate
playerViewController.player = videoPlayerViewModel.player
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
switch videoPlayerViewModel.videoKind {
case .gif:
playerContainerView.setMediaIndicator(isHidden: false)
case .video:
playerContainerView.setMediaIndicator(isHidden: true)
}
playerContainerView.isHidden = false
// set blurhash overlay
playerContainerView.isReadyForDisplay
.receive(on: DispatchQueue.main)
.sink { [weak playerContainerView] isReadyForDisplay in
guard let playerContainerView = playerContainerView else { return }
playerContainerView.blurhashOverlayImageView.alpha = isReadyForDisplay ? 0 : 1
}
.store(in: &cell.disposeBag)
if let blurhash = videoAttachment.blurhash,
let url = URL(string: videoAttachment.url) {
AppContext.shared.blurhashImageCacheService.image(
blurhash: blurhash,
size: playerContainerView.playerViewController.view.frame.size,
url: url
)
.sink { image in
playerContainerView.blurhashOverlayImageView.image = image
}
.store(in: &cell.disposeBag)
}
} else {
cell.statusView.playerContainerView.playerViewController.player?.pause()
cell.statusView.playerContainerView.playerViewController.player = nil
}
} }
static func configurePoll( static func configurePoll(
cell: StatusCell, cell: StatusCell,
poll: Poll?, poll: Poll?,
requestUserID: String, requestUserID: String,
updateProgressAnimated: Bool, updateProgressAnimated: Bool
timestampUpdatePublisher: AnyPublisher<Date, Never>
) { ) {
guard let poll = poll, guard let poll = poll,
let managedObjectContext = poll.managedObjectContext let managedObjectContext = poll.managedObjectContext
@ -847,17 +779,16 @@ extension StatusSection {
} }
}() }()
if poll.expired { if poll.expired {
cell.pollCountdownSubscription = nil cell.statusView.pollCountdownSubscription = nil
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed
} else if let expiresAt = poll.expiresAt { } else if let expiresAt = poll.expiresAt {
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow) cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
cell.pollCountdownSubscription = timestampUpdatePublisher cell.statusView.pollCountdownSubscription = AppContext.shared.timestampUpdatePublisher
.sink { _ in .sink { _ in
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow) cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
} }
} else { } else {
// assertionFailure() cell.statusView.pollCountdownSubscription = nil
cell.pollCountdownSubscription = nil
cell.statusView.pollCountdownLabel.text = "-" cell.statusView.pollCountdownLabel.text = "-"
} }
@ -920,7 +851,78 @@ extension StatusSection {
snapshot.appendItems(pollItems, toSection: .main) snapshot.appendItems(pollItems, toSection: .main)
cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
} }
static func configureActionToolBar(
cell: StatusTableViewCell,
dependency: NeedsDependency,
status: Status,
requestUserID: String
) {
let status = status.reblog ?? status
// set reply
let replyCountTitle: String = {
let count = status.repliesCount?.intValue ?? 0
return StatusSection.formattedNumberTitleForActionButton(count)
}()
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.replyButton.accessibilityValue = status.repliesCount.flatMap {
L10n.Common.Controls.Timeline.Accessibility.countReplies($0.intValue)
} ?? nil
// set reblog
let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
let reblogCountTitle: String = {
let count = status.reblogsCount.intValue
return StatusSection.formattedNumberTitleForActionButton(count)
}()
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged
cell.statusView.actionToolbarContainer.reblogButton.accessibilityLabel = isReblogged ? L10n.Common.Controls.Status.Actions.unreblog : L10n.Common.Controls.Status.Actions.reblog
cell.statusView.actionToolbarContainer.reblogButton.accessibilityValue = {
guard status.reblogsCount.intValue > 0 else { return nil }
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.reblogsCount.intValue)
}()
// set like
let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
let favoriteCountTitle: String = {
let count = status.favouritesCount.intValue
return StatusSection.formattedNumberTitleForActionButton(count)
}()
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike
cell.statusView.actionToolbarContainer.favoriteButton.accessibilityLabel = isLike ? L10n.Common.Controls.Status.Actions.unfavorite : L10n.Common.Controls.Status.Actions.favorite
cell.statusView.actionToolbarContainer.favoriteButton.accessibilityValue = {
guard status.favouritesCount.intValue > 0 else { return nil }
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue)
}()
Publishers.CombineLatest(
dependency.context.blockDomainService.blockedDomains.setFailureType(to: ManagedObjectObserver.Error.self),
ManagedObjectObserver.observe(object: status.authorForUserProvider)
)
.receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in
// do nothing
}, receiveValue: { [weak dependency, weak cell] _, change in
guard let cell = cell else { return }
guard let dependency = dependency else { return }
switch change.changeType {
case .delete:
return
case .update(_):
break
case .none:
break
}
StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
})
.store(in: &cell.disposeBag)
self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
}
}
extension StatusSection {
static func configureEmptyStateHeader( static func configureEmptyStateHeader(
cell: TimelineHeaderTableViewCell, cell: TimelineHeaderTableViewCell,
attribute: Item.EmptyStateHeaderAttribute attribute: Item.EmptyStateHeaderAttribute

View File

@ -29,9 +29,6 @@ extension ActiveLabel {
hashtagColor = Asset.Colors.brandBlue.color hashtagColor = Asset.Colors.brandBlue.color
URLColor = Asset.Colors.brandBlue.color URLColor = Asset.Colors.brandBlue.color
emojiPlaceholderColor = .systemFill emojiPlaceholderColor = .systemFill
#if DEBUG
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
#endif
accessibilityContainerType = .semanticGroup accessibilityContainerType = .semanticGroup

View File

@ -22,7 +22,7 @@ extension UIView {
} }
// MARK: - Convinience view appearance modification method // MARK: - Convenience view appearance modification method
extension UIView { extension UIView {
@discardableResult @discardableResult
func applyCornerRadius(radius: CGFloat) -> Self { func applyCornerRadius(radius: CGFloat) -> Self {

View File

@ -58,6 +58,9 @@ internal enum Asset {
internal static let disabled = ColorAsset(name: "Colors/Button/disabled") internal static let disabled = ColorAsset(name: "Colors/Button/disabled")
internal static let inactive = ColorAsset(name: "Colors/Button/inactive") internal static let inactive = ColorAsset(name: "Colors/Button/inactive")
} }
internal enum ContentWarningOverlay {
internal static let background = ColorAsset(name: "Colors/ContentWarningOverlay/background")
}
internal enum Icon { internal enum Icon {
internal static let plus = ColorAsset(name: "Colors/Icon/plus") internal static let plus = ColorAsset(name: "Colors/Icon/plus")
} }

View File

@ -254,13 +254,9 @@ internal enum L10n {
} }
} }
internal enum Status { internal enum Status {
/// content warning /// Content Warning
internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning") internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning")
/// cw: %@ /// Tap anywhere to reveal
internal static func contentWarningText(_ p1: Any) -> String {
return L10n.tr("Localizable", "Common.Controls.Status.ContentWarningText", String(describing: p1))
}
/// Tap to reveal that may be sensitive
internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning") internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning")
/// Show Post /// Show Post
internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost") internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost")

View File

@ -7,7 +7,8 @@
import UIKit import UIKit
import AlamofireImage import AlamofireImage
import Kingfisher import FLAnimatedImage
import Nuke
protocol AvatarConfigurableView { protocol AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { get } static var configurableAvatarImageSize: CGSize { get }
@ -31,13 +32,7 @@ extension AvatarConfigurableView {
} }
return placeholderImage return placeholderImage
}() }()
// cancel previous task
configurableAvatarImageView?.af.cancelImageRequest()
configurableAvatarImageView?.kf.cancelDownloadTask()
configurableAvatarButton?.af.cancelImageRequest(for: .normal)
configurableAvatarButton?.kf.cancelImageDownloadTask()
// reset layer attributes // reset layer attributes
configurableAvatarImageView?.layer.masksToBounds = false configurableAvatarImageView?.layer.masksToBounds = false
configurableAvatarImageView?.layer.cornerRadius = 0 configurableAvatarImageView?.layer.cornerRadius = 0
@ -55,85 +50,50 @@ extension AvatarConfigurableView {
avatarConfigurableView(self, didFinishConfiguration: configuration) avatarConfigurableView(self, didFinishConfiguration: configuration)
} }
let filter = ScaledToSizeWithRoundedCornersFilter( guard let imageDisplayingView: ImageDisplayingView = configurableAvatarImageView ?? configurableAvatarButton?.imageView else {
size: Self.configurableAvatarImageSize,
radius: configuration.keepImageCorner ? 0 : Self.configurableAvatarImageCornerRadius
)
// set placeholder if no asset
guard let avatarImageURL = configuration.avatarImageURL else {
configurableAvatarImageView?.image = placeholderImage
configurableAvatarImageView?.layer.masksToBounds = true
configurableAvatarImageView?.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
configurableAvatarImageView?.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular
configurableAvatarButton?.setImage(placeholderImage, for: .normal)
configurableAvatarButton?.layer.masksToBounds = true
configurableAvatarButton?.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
configurableAvatarButton?.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular
return return
} }
if let avatarImageView = configurableAvatarImageView { // set corner radius (due to GIF won't crop)
// set avatar (GIF using Kingfisher) imageDisplayingView.layer.masksToBounds = true
switch avatarImageURL.pathExtension { imageDisplayingView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
case "gif": imageDisplayingView.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular
avatarImageView.kf.setImage(
with: avatarImageURL, // set border
placeholder: placeholderImage, configureLayerBorder(view: imageDisplayingView, configuration: configuration)
options: [
.transition(.fade(0.2))
] // set image
) let url = configuration.avatarImageURL
avatarImageView.layer.masksToBounds = true let processors: [ImageProcessing] = [
avatarImageView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius ImageProcessors.Resize(
avatarImageView.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular size: Self.configurableAvatarImageSize,
unit: .points,
default: contentMode: .aspectFill,
avatarImageView.af.setImage( crop: false
withURL: avatarImageURL, ),
placeholderImage: placeholderImage, ImageProcessors.RoundedCorners(
filter: filter, radius: Self.configurableAvatarImageCornerRadius
imageTransition: .crossDissolve(0.3), )
runImageTransitionIfCached: false, ]
completion: nil
) let request = ImageRequest(url: url, processors: processors)
let options = ImageLoadingOptions(
if Self.configurableAvatarImageCornerRadius > 0, configuration.keepImageCorner { placeholder: placeholderImage,
configurableAvatarImageView?.layer.masksToBounds = true transition: .fadeIn(duration: 0.2)
configurableAvatarImageView?.layer.cornerRadius = Self.configurableAvatarImageCornerRadius )
configurableAvatarImageView?.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular
} Nuke.loadImage(
with: request,
options: options,
into: imageDisplayingView
) { result in
switch result {
case .failure:
break
case .success:
break
} }
configureLayerBorder(view: avatarImageView, configuration: configuration)
}
if let avatarButton = configurableAvatarButton {
switch avatarImageURL.pathExtension {
case "gif":
avatarButton.kf.setImage(
with: avatarImageURL,
for: .normal,
placeholder: placeholderImage,
options: [
.transition(.fade(0.2))
]
)
avatarButton.layer.masksToBounds = true
avatarButton.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
avatarButton.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous : .circular
default:
avatarButton.af.setImage(
for: .normal,
url: avatarImageURL,
placeholderImage: placeholderImage,
filter: filter,
completion: nil
)
}
configureLayerBorder(view: avatarButton, configuration: configuration)
} }
} }

View File

@ -20,7 +20,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .secondary, provider: self, cell: cell) StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .secondary, provider: self, cell: cell)
} }
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarButtonDidPressed button: UIButton) { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView) {
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, cell: cell) StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, cell: cell)
} }

View File

@ -66,6 +66,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
.store(in: &disposeBag) .store(in: &disposeBag)
status(for: cell, indexPath: indexPath) status(for: cell, indexPath: indexPath)
.receive(on: RunLoop.main)
.sink { [weak self] status in .sink { [weak self] status in
guard let self = self else { return } guard let self = self else { return }
let status = status?.reblog ?? status let status = status?.reblog ?? status

View File

@ -541,8 +541,9 @@ extension StatusProviderFacade {
.compactMap { [weak dependency] status -> AnyPublisher<Status?, Never>? in .compactMap { [weak dependency] status -> AnyPublisher<Status?, Never>? in
guard let dependency = dependency else { return nil } guard let dependency = dependency else { return nil }
guard let _status = status else { return nil } guard let _status = status else { return nil }
return dependency.context.managedObjectContext.performChanges { let managedObjectContext = dependency.context.backgroundManagedObjectContext
guard let status = dependency.context.managedObjectContext.object(with: _status.objectID) as? Status else { return } return managedObjectContext.performChanges {
guard let status = managedObjectContext.object(with: _status.objectID) as? Status else { return }
let appStartUpTimestamp = dependency.context.documentStore.appStartUpTimestamp let appStartUpTimestamp = dependency.context.documentStore.appStartUpTimestamp
let isRevealing: Bool = { let isRevealing: Bool = {
if dependency.context.documentStore.defaultRevealStatusDict[status.id] == true { if dependency.context.documentStore.defaultRevealStatusDict[status.id] == true {
@ -560,7 +561,11 @@ extension StatusProviderFacade {
// toggle reveal // toggle reveal
dependency.context.documentStore.defaultRevealStatusDict[status.id] = false dependency.context.documentStore.defaultRevealStatusDict[status.id] = false
status.update(isReveal: !isRevealing) status.update(isReveal: !isRevealing)
status.reblog?.update(isReveal: !isRevealing)
if let reblog = status.reblog {
dependency.context.documentStore.defaultRevealStatusDict[reblog.id] = false
reblog.update(isReveal: !isRevealing)
}
// pause video playback if isRevealing before toggle // pause video playback if isRevealing before toggle
if isRevealing, let attachment = (status.reblog ?? status).mediaAttachments?.first, if isRevealing, let attachment = (status.reblog ?? status).mediaAttachments?.first,

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "235",
"green" : "229",
"red" : "221"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.922",
"green" : "0.898",
"red" : "0.867"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -94,9 +94,8 @@ Please check your internet connection.";
"Common.Controls.Status.Actions.Reply" = "Reply"; "Common.Controls.Status.Actions.Reply" = "Reply";
"Common.Controls.Status.Actions.Unfavorite" = "Unfavorite"; "Common.Controls.Status.Actions.Unfavorite" = "Unfavorite";
"Common.Controls.Status.Actions.Unreblog" = "Unreblog"; "Common.Controls.Status.Actions.Unreblog" = "Unreblog";
"Common.Controls.Status.ContentWarning" = "content warning"; "Common.Controls.Status.ContentWarning" = "Content Warning";
"Common.Controls.Status.ContentWarningText" = "cw: %@"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal";
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
"Common.Controls.Status.Poll.Closed" = "Closed"; "Common.Controls.Status.Poll.Closed" = "Closed";
"Common.Controls.Status.Poll.TimeLeft" = "%@ left"; "Common.Controls.Status.Poll.TimeLeft" = "%@ left";
"Common.Controls.Status.Poll.Vote" = "Vote"; "Common.Controls.Status.Poll.Vote" = "Vote";

View File

@ -94,9 +94,8 @@ Please check your internet connection.";
"Common.Controls.Status.Actions.Reply" = "Reply"; "Common.Controls.Status.Actions.Reply" = "Reply";
"Common.Controls.Status.Actions.Unfavorite" = "Unfavorite"; "Common.Controls.Status.Actions.Unfavorite" = "Unfavorite";
"Common.Controls.Status.Actions.Unreblog" = "Unreblog"; "Common.Controls.Status.Actions.Unreblog" = "Unreblog";
"Common.Controls.Status.ContentWarning" = "content warning"; "Common.Controls.Status.ContentWarning" = "Content Warning";
"Common.Controls.Status.ContentWarningText" = "cw: %@"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal";
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
"Common.Controls.Status.Poll.Closed" = "Closed"; "Common.Controls.Status.Poll.Closed" = "Closed";
"Common.Controls.Status.Poll.TimeLeft" = "%@ left"; "Common.Controls.Status.Poll.TimeLeft" = "%@ left";
"Common.Controls.Status.Poll.Vote" = "Vote"; "Common.Controls.Status.Poll.Vote" = "Vote";

View File

@ -104,16 +104,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
super.init(coder: coder) super.init(coder: coder)
configure() configure()
} }
override func layoutSubviews() {
super.layoutSubviews()
// precondition: app is active
guard UIApplication.shared.applicationState == .active else { return }
DispatchQueue.main.async {
self.statusView.drawContentWarningImageView()
}
}
} }
extension NotificationStatusTableViewCell { extension NotificationStatusTableViewCell {
@ -231,32 +222,17 @@ extension NotificationStatusTableViewCell {
statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor
actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor
} }
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
resetContentOverlayBlurImageBackgroundColor(selected: highlighted)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
resetContentOverlayBlurImageBackgroundColor(selected: selected)
}
private func resetContentOverlayBlurImageBackgroundColor(selected: Bool) {
let imageViewBackgroundColor: UIColor? = selected ? selectedBackgroundView?.backgroundColor : backgroundColor
statusView.contentWarningOverlayView.blurContentImageView.backgroundColor = imageViewBackgroundColor
}
} }
// MARK: - StatusViewDelegate // MARK: - StatusViewDelegate
extension NotificationStatusTableViewCell: StatusViewDelegate { extension NotificationStatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) { func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) {
// do nothing // do nothing
} }
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) { func statusView(_ statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView) {
// do nothing // do nothing
} }

View File

@ -9,6 +9,7 @@ import os.log
import UIKit import UIKit
import ActiveLabel import ActiveLabel
import TwitterTextEditor import TwitterTextEditor
import FLAnimatedImage
protocol ProfileHeaderViewDelegate: AnyObject { protocol ProfileHeaderViewDelegate: AnyObject {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView)
@ -66,7 +67,7 @@ final class ProfileHeaderView: UIView {
}() }()
let avatarImageView: UIImageView = { let avatarImageView: UIImageView = {
let imageView = UIImageView() let imageView = FLAnimatedImageView()
let placeholderImage = UIImage let placeholderImage = UIImage
.placeholder(size: ProfileHeaderView.avatarImageViewSize, color: Asset.Colors.Background.systemGroupedBackground.color) .placeholder(size: ProfileHeaderView.avatarImageViewSize, color: Asset.Colors.Background.systemGroupedBackground.color)
.af.imageRounded(withCornerRadius: ProfileHeaderView.avatarImageViewCornerRadius, divideRadiusByImageScale: false) .af.imageRounded(withCornerRadius: ProfileHeaderView.avatarImageViewCornerRadius, divideRadiusByImageScale: false)

View File

@ -19,7 +19,6 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell {
weak var dependency: ReportViewController? weak var dependency: ReportViewController?
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var pollCountdownSubscription: AnyCancellable?
var observations = Set<NSKeyValueObservation>() var observations = Set<NSKeyValueObservation>()
let statusView = StatusView() let statusView = StatusView()
@ -62,16 +61,6 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell {
_init() _init()
} }
override func layoutSubviews() {
super.layoutSubviews()
// precondition: app is active
guard UIApplication.shared.applicationState == .active else { return }
DispatchQueue.main.async {
self.statusView.drawContentWarningImageView()
}
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) { override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated) super.setHighlighted(highlighted, animated: animated)
if highlighted { if highlighted {
@ -134,7 +123,6 @@ extension ReportedStatusTableViewCell {
statusView.delegate = self statusView.delegate = self
statusView.statusMosaicImageViewContainer.delegate = self statusView.statusMosaicImageViewContainer.delegate = self
statusView.actionToolbarContainer.isHidden = true statusView.actionToolbarContainer.isHidden = true
statusView.contentWarningOverlayView.blurContentImageView.backgroundColor = backgroundColor
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@ -188,12 +176,13 @@ extension ReportedStatusTableViewCell: MosaicImageViewContainerDelegate {
} }
extension ReportedStatusTableViewCell: StatusViewDelegate { extension ReportedStatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) { func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) {
} }
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) { func statusView(_ statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView) {
} }
func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) { func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) {
guard let dependency = self.dependency else { return } guard let dependency = self.dependency else { return }
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self) StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self)

View File

@ -42,7 +42,7 @@ final class MosaicImageViewContainer: UIView {
let contentWarningOverlayView: ContentWarningOverlayView = { let contentWarningOverlayView: ContentWarningOverlayView = {
let contentWarningOverlayView = ContentWarningOverlayView() let contentWarningOverlayView = ContentWarningOverlayView()
contentWarningOverlayView.configure(style: .visualEffectView) contentWarningOverlayView.configure(style: .media)
return contentWarningOverlayView return contentWarningOverlayView
}() }()

View File

@ -29,19 +29,22 @@ class ContentWarningOverlayView: UIView {
label.isAccessibilityElement = false label.isAccessibilityElement = false
return label return label
}() }()
let blurContentImageView: UIImageView = { // for status style overlay
let imageView = UIImageView() let contentOverlayView: UIView = {
imageView.layer.masksToBounds = false let view = UIView()
return imageView view.backgroundColor = Asset.Colors.ContentWarningOverlay.background.color
view.applyCornerRadius(radius: ContentWarningOverlayView.cornerRadius)
return view
}() }()
let blurContentWarningTitleLabel: UILabel = { let blurContentWarningTitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17), maximumPointSize: 23) label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 23)
label.text = L10n.Common.Controls.Status.mediaContentWarning label.text = L10n.Common.Controls.Status.mediaContentWarning
label.textColor = Asset.Colors.Label.primary.color label.textColor = Asset.Colors.Label.primary.color
label.textAlignment = .center label.textAlignment = .center
label.isAccessibilityElement = false label.isAccessibilityElement = false
label.numberOfLines = 2
return label return label
}() }()
let blurContentWarningLabel: UILabel = { let blurContentWarningLabel: UILabel = {
@ -50,8 +53,8 @@ class ContentWarningOverlayView: UIView {
label.text = L10n.Common.Controls.Status.mediaContentWarning label.text = L10n.Common.Controls.Status.mediaContentWarning
label.textColor = Asset.Colors.Label.secondary.color label.textColor = Asset.Colors.Label.secondary.color
label.textAlignment = .center label.textAlignment = .center
label.layer.setupShadow()
label.isAccessibilityElement = false label.isAccessibilityElement = false
label.numberOfLines = 2
return label return label
}() }()
@ -108,13 +111,13 @@ extension ContentWarningOverlayView {
]) ])
// blur image style // blur image style
blurContentImageView.translatesAutoresizingMaskIntoConstraints = false contentOverlayView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurContentImageView) addSubview(contentOverlayView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
blurContentImageView.topAnchor.constraint(equalTo: topAnchor), contentOverlayView.topAnchor.constraint(equalTo: topAnchor),
blurContentImageView.leadingAnchor.constraint(equalTo: leadingAnchor), contentOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
blurContentImageView.trailingAnchor.constraint(equalTo: trailingAnchor), contentOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
blurContentImageView.bottomAnchor.constraint(equalTo: bottomAnchor), contentOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
]) ])
let blurContentWarningLabelContainer = UIStackView() let blurContentWarningLabelContainer = UIStackView()
@ -123,7 +126,7 @@ extension ContentWarningOverlayView {
blurContentWarningLabelContainer.alignment = .center blurContentWarningLabelContainer.alignment = .center
blurContentWarningLabelContainer.translatesAutoresizingMaskIntoConstraints = false blurContentWarningLabelContainer.translatesAutoresizingMaskIntoConstraints = false
blurContentImageView.addSubview(blurContentWarningLabelContainer) contentOverlayView.addSubview(blurContentWarningLabelContainer)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
blurContentWarningLabelContainer.topAnchor.constraint(equalTo: topAnchor), blurContentWarningLabelContainer.topAnchor.constraint(equalTo: topAnchor),
blurContentWarningLabelContainer.leadingAnchor.constraint(equalTo: leadingAnchor), blurContentWarningLabelContainer.leadingAnchor.constraint(equalTo: leadingAnchor),
@ -143,42 +146,43 @@ extension ContentWarningOverlayView {
topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 1.0).priority(.defaultHigh), topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 1.0).priority(.defaultHigh),
]) ])
blurContentWarningTitleLabel.setContentHuggingPriority(.defaultHigh + 2, for: .vertical) blurContentWarningTitleLabel.setContentHuggingPriority(.defaultHigh + 2, for: .vertical)
blurContentWarningTitleLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .vertical)
blurContentWarningLabel.setContentHuggingPriority(.defaultHigh + 1, for: .vertical) blurContentWarningLabel.setContentHuggingPriority(.defaultHigh + 1, for: .vertical)
tapGestureRecognizer.addTarget(self, action: #selector(ContentWarningOverlayView.tapGestureRecognizerHandler(_:))) tapGestureRecognizer.addTarget(self, action: #selector(ContentWarningOverlayView.tapGestureRecognizerHandler(_:)))
addGestureRecognizer(tapGestureRecognizer) addGestureRecognizer(tapGestureRecognizer)
configure(style: .visualEffectView) configure(style: .media)
} }
} }
extension ContentWarningOverlayView { extension ContentWarningOverlayView {
enum Style { enum Style {
case visualEffectView case media // visualEffectView for media
case blurContentImageView case contentWarning // overlay for post
} }
func configure(style: Style) { func configure(style: Style) {
switch style { switch style {
case .visualEffectView: case .media:
blurVisualEffectView.isHidden = false blurVisualEffectView.isHidden = false
vibrancyVisualEffectView.isHidden = false vibrancyVisualEffectView.isHidden = false
blurContentImageView.isHidden = true contentOverlayView.isHidden = true
case .blurContentImageView: case .contentWarning:
blurVisualEffectView.isHidden = true blurVisualEffectView.isHidden = true
vibrancyVisualEffectView.isHidden = true vibrancyVisualEffectView.isHidden = true
blurContentImageView.isHidden = false contentOverlayView.isHidden = false
} }
} }
func update(isRevealing: Bool, style: Style) { func update(isRevealing: Bool, style: Style) {
switch style { switch style {
case .visualEffectView: case .media:
blurVisualEffectView.effect = isRevealing ? nil : ContentWarningOverlayView.blurVisualEffect blurVisualEffectView.effect = isRevealing ? nil : ContentWarningOverlayView.blurVisualEffect
vibrancyVisualEffectView.alpha = isRevealing ? 0 : 1 vibrancyVisualEffectView.alpha = isRevealing ? 0 : 1
isUserInteractionEnabled = !isRevealing isUserInteractionEnabled = !isRevealing
case .blurContentImageView: case .contentWarning:
assertionFailure("not handle here") assertionFailure("not handle here")
break break
} }

View File

@ -7,13 +7,15 @@
import os.log import os.log
import UIKit import UIKit
import Combine
import AVKit import AVKit
import ActiveLabel import ActiveLabel
import AlamofireImage import AlamofireImage
import FLAnimatedImage
protocol StatusViewDelegate: AnyObject { protocol StatusViewDelegate: AnyObject {
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView)
func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
@ -24,6 +26,7 @@ protocol StatusViewDelegate: AnyObject {
final class StatusView: UIView { final class StatusView: UIView {
var statusPollTableViewHeightObservation: NSKeyValueObservation? var statusPollTableViewHeightObservation: NSKeyValueObservation?
var pollCountdownSubscription: AnyCancellable?
static let avatarImageSize = CGSize(width: 42, height: 42) static let avatarImageSize = CGSize(width: 42, height: 42)
static let avatarImageCornerRadius: CGFloat = 4 static let avatarImageCornerRadius: CGFloat = 4
@ -32,9 +35,9 @@ final class StatusView: UIView {
static let containerStackViewSpacing: CGFloat = 10 static let containerStackViewSpacing: CGFloat = 10
weak var delegate: StatusViewDelegate? weak var delegate: StatusViewDelegate?
private var needsDrawContentOverlay = false
var pollTableViewDataSource: UITableViewDiffableDataSource<PollSection, PollItem>? var pollTableViewDataSource: UITableViewDiffableDataSource<PollSection, PollItem>?
var pollTableViewHeightLaoutConstraint: NSLayoutConstraint! var pollTableViewHeightLayoutConstraint: NSLayoutConstraint!
let containerStackView = UIStackView() let containerStackView = UIStackView()
let headerContainerView = UIView() let headerContainerView = UIView()
@ -82,18 +85,11 @@ final class StatusView: UIView {
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
return view return view
}() }()
let avatarButton: UIButton = { let avatarImageView: UIImageView = FLAnimatedImageView()
let button = HighlightDimmableButton(type: .custom)
let placeholderImage = UIImage.placeholder(size: avatarImageSize, color: .systemFill)
.af.imageRounded(withCornerRadius: StatusView.avatarImageCornerRadius, divideRadiusByImageScale: true)
button.setImage(placeholderImage, for: .normal)
return button
}()
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton() let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
let nameLabel: ActiveLabel = { let nameLabel: ActiveLabel = {
let label = ActiveLabel(style: .statusName) let label = ActiveLabel(style: .statusName)
label.text = "Alice"
return label return label
}() }()
@ -185,8 +181,8 @@ final class StatusView: UIView {
// do not use visual effect view due to we blur text only without background // do not use visual effect view due to we blur text only without background
let contentWarningOverlayView: ContentWarningOverlayView = { let contentWarningOverlayView: ContentWarningOverlayView = {
let contentWarningOverlayView = ContentWarningOverlayView() let contentWarningOverlayView = ContentWarningOverlayView()
contentWarningOverlayView.layer.masksToBounds = false contentWarningOverlayView.configure(style: .contentWarning)
contentWarningOverlayView.configure(style: .blurContentImageView) contentWarningOverlayView.layer.masksToBounds = true
return contentWarningOverlayView return contentWarningOverlayView
}() }()
@ -218,15 +214,6 @@ final class StatusView: UIView {
_init() _init()
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// update blur image when interface style changed
if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle {
drawContentWarningImageView()
}
}
deinit { deinit {
statusPollTableViewHeightObservation = nil statusPollTableViewHeightObservation = nil
} }
@ -249,6 +236,7 @@ extension StatusView {
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor), bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
]) ])
containerStackView.setContentHuggingPriority(.required - 1, for: .vertical) containerStackView.setContentHuggingPriority(.required - 1, for: .vertical)
containerStackView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
// header container: [icon | info] // header container: [icon | info]
let headerContainerStackView = UIStackView() let headerContainerStackView = UIStackView()
@ -281,13 +269,13 @@ extension StatusView {
avatarView.widthAnchor.constraint(equalToConstant: StatusView.avatarImageSize.width).priority(.required - 1), avatarView.widthAnchor.constraint(equalToConstant: StatusView.avatarImageSize.width).priority(.required - 1),
avatarView.heightAnchor.constraint(equalToConstant: StatusView.avatarImageSize.height).priority(.required - 1), avatarView.heightAnchor.constraint(equalToConstant: StatusView.avatarImageSize.height).priority(.required - 1),
]) ])
avatarButton.translatesAutoresizingMaskIntoConstraints = false avatarImageView.translatesAutoresizingMaskIntoConstraints = false
avatarView.addSubview(avatarButton) avatarView.addSubview(avatarImageView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
avatarButton.topAnchor.constraint(equalTo: avatarView.topAnchor), avatarImageView.topAnchor.constraint(equalTo: avatarView.topAnchor),
avatarButton.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor), avatarImageView.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
avatarButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor), avatarImageView.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
avatarButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor), avatarImageView.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
]) ])
avatarStackedContainerButton.translatesAutoresizingMaskIntoConstraints = false avatarStackedContainerButton.translatesAutoresizingMaskIntoConstraints = false
avatarView.addSubview(avatarStackedContainerButton) avatarView.addSubview(avatarStackedContainerButton)
@ -355,18 +343,20 @@ extension StatusView {
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addSubview(contentWarningOverlayView) containerStackView.addSubview(contentWarningOverlayView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
statusContainerStackView.topAnchor.constraint(equalTo: contentWarningOverlayView.topAnchor, constant: StatusView.contentWarningBlurRadius).priority(.defaultHigh), statusContainerStackView.topAnchor.constraint(equalTo: contentWarningOverlayView.topAnchor).priority(.defaultHigh),
statusContainerStackView.leftAnchor.constraint(equalTo: contentWarningOverlayView.leftAnchor, constant: StatusView.contentWarningBlurRadius).priority(.defaultHigh), statusContainerStackView.leftAnchor.constraint(equalTo: contentWarningOverlayView.leftAnchor).priority(.defaultHigh),
contentWarningOverlayView.rightAnchor.constraint(equalTo: statusContainerStackView.rightAnchor, constant: StatusView.contentWarningBlurRadius).priority(.defaultHigh), contentWarningOverlayView.rightAnchor.constraint(equalTo: statusContainerStackView.rightAnchor).priority(.defaultHigh),
// only layout to top and left & right then draw image to fit size contentWarningOverlayView.bottomAnchor.constraint(equalTo: statusContainerStackView.bottomAnchor).priority(.defaultHigh),
]) ])
// avoid overlay clip author view // avoid overlay behind other views
containerStackView.bringSubviewToFront(authorContainerView) defer {
containerStackView.bringSubviewToFront(authorContainerView)
}
// status // status
statusContainerStackView.addArrangedSubview(activeTextLabel) statusContainerStackView.addArrangedSubview(activeTextLabel)
activeTextLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) activeTextLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
// image // image
statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer) statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer)
@ -382,18 +372,18 @@ extension StatusView {
pollTableView.translatesAutoresizingMaskIntoConstraints = false pollTableView.translatesAutoresizingMaskIntoConstraints = false
statusContainerStackView.addArrangedSubview(pollTableView) statusContainerStackView.addArrangedSubview(pollTableView)
pollTableViewHeightLaoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1) pollTableViewHeightLayoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
pollTableViewHeightLaoutConstraint, pollTableViewHeightLayoutConstraint,
]) ])
statusPollTableViewHeightObservation = pollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in statusPollTableViewHeightObservation = pollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in
guard let self = self else { return } guard let self = self else { return }
guard self.pollTableView.contentSize.height != .zero else { guard self.pollTableView.contentSize.height != .zero else {
self.pollTableViewHeightLaoutConstraint.constant = 44 self.pollTableViewHeightLayoutConstraint.constant = 44
return return
} }
self.pollTableViewHeightLaoutConstraint.constant = self.pollTableView.contentSize.height self.pollTableViewHeightLayoutConstraint.constant = self.pollTableView.contentSize.height
}) })
statusContainerStackView.addArrangedSubview(pollStatusStackView) statusContainerStackView.addArrangedSubview(pollStatusStackView)
@ -409,6 +399,7 @@ extension StatusView {
// action toolbar container // action toolbar container
containerStackView.addArrangedSubview(actionToolbarContainer) containerStackView.addArrangedSubview(actionToolbarContainer)
containerStackView.sendSubviewToBack(actionToolbarContainer)
actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical) actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
headerContainerView.isHidden = true headerContainerView.isHidden = true
@ -428,8 +419,12 @@ extension StatusView {
headerInfoLabelTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerInfoLabelTapGestureRecognizerHandler(_:))) headerInfoLabelTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerInfoLabelTapGestureRecognizerHandler(_:)))
headerInfoLabel.isUserInteractionEnabled = true headerInfoLabel.isUserInteractionEnabled = true
headerInfoLabel.addGestureRecognizer(headerInfoLabelTapGestureRecognizer) headerInfoLabel.addGestureRecognizer(headerInfoLabelTapGestureRecognizer)
avatarButton.addTarget(self, action: #selector(StatusView.avatarButtonDidPressed(_:)), for: .touchUpInside) let avatarImageViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
avatarImageViewTapGestureRecognizer.addTarget(self, action: #selector(StatusView.avatarImageViewDidPressed(_:)))
avatarImageView.addGestureRecognizer(avatarImageViewTapGestureRecognizer)
avatarImageView.isUserInteractionEnabled = true
avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside) avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside)
revealContentWarningButton.addTarget(self, action: #selector(StatusView.revealContentWarningButtonDidPressed(_:)), for: .touchUpInside) revealContentWarningButton.addTarget(self, action: #selector(StatusView.revealContentWarningButtonDidPressed(_:)), for: .touchUpInside)
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside) pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
@ -438,49 +433,21 @@ extension StatusView {
} }
extension StatusView { extension StatusView {
private func cleanUpContentWarning() { func updateContentWarningDisplay(isHidden: Bool, animated: Bool) {
contentWarningOverlayView.blurContentImageView.image = nil func updateOverlayView() {
} contentWarningOverlayView.contentOverlayView.alpha = isHidden ? 0 : 1
func drawContentWarningImageView() {
guard window != nil else {
return
}
guard needsDrawContentOverlay, statusContainerStackView.frame != .zero else {
cleanUpContentWarning()
return
} }
let format = UIGraphicsImageRendererFormat()
format.opaque = false
let image = UIGraphicsImageRenderer(size: statusContainerStackView.frame.size, format: format).image { context in
statusContainerStackView.drawHierarchy(in: statusContainerStackView.bounds, afterScreenUpdates: true)
}
.blur(radius: StatusView.contentWarningBlurRadius)
contentWarningOverlayView.blurContentImageView.contentScaleFactor = traitCollection.displayScale
contentWarningOverlayView.blurContentImageView.image = image
}
func updateContentWarningDisplay(isHidden: Bool, animated: Bool) {
needsDrawContentOverlay = !isHidden
if !isHidden {
drawContentWarningImageView()
}
if animated { if animated {
UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) { [weak self] in let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut) {
guard let self = self else { return } updateOverlayView()
self.contentWarningOverlayView.alpha = isHidden ? 0 : 1
} completion: { _ in
// do nothing
} }
animator.startAnimation()
} else { } else {
contentWarningOverlayView.alpha = isHidden ? 0 : 1 updateOverlayView()
} }
contentWarningOverlayView.blurContentWarningTitleLabel.isHidden = isHidden contentWarningOverlayView.blurContentWarningTitleLabel.isHidden = isHidden
contentWarningOverlayView.blurContentWarningLabel.isHidden = isHidden contentWarningOverlayView.blurContentWarningLabel.isHidden = isHidden
} }
@ -512,14 +479,14 @@ extension StatusView {
delegate?.statusView(self, headerInfoLabelDidPressed: headerInfoLabel) delegate?.statusView(self, headerInfoLabelDidPressed: headerInfoLabel)
} }
@objc private func avatarButtonDidPressed(_ sender: UIButton) { @objc private func avatarImageViewDidPressed(_ sender: UITapGestureRecognizer) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.statusView(self, avatarButtonDidPressed: sender) delegate?.statusView(self, avatarImageViewDidPressed: avatarImageView)
} }
@objc private func avatarStackedContainerButtonDidPressed(_ sender: UIButton) { @objc private func avatarStackedContainerButtonDidPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.statusView(self, avatarButtonDidPressed: sender) delegate?.statusView(self, avatarImageViewDidPressed: avatarStackedContainerButton.topLeadingAvatarStackedImageView)
} }
@objc private func revealContentWarningButtonDidPressed(_ sender: UIButton) { @objc private func revealContentWarningButtonDidPressed(_ sender: UIButton) {
@ -562,9 +529,8 @@ extension StatusView: PlayerContainerViewDelegate {
extension StatusView: AvatarConfigurableView { extension StatusView: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize } static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
static var configurableAvatarImageCornerRadius: CGFloat { return 4 } static var configurableAvatarImageCornerRadius: CGFloat { return 4 }
var configurableAvatarImageView: UIImageView? { return nil } var configurableAvatarImageView: UIImageView? { avatarImageView }
var configurableAvatarButton: UIButton? { return avatarButton } var configurableAvatarButton: UIButton? { nil }
var configurableVerifiedBadgeImageView: UIImageView? { nil }
} }
#if canImport(SwiftUI) && DEBUG #if canImport(SwiftUI) && DEBUG
@ -592,7 +558,7 @@ struct StatusView_Previews: PreviewProvider {
UIViewPreview(width: 375) { UIViewPreview(width: 375) {
let statusView = StatusView() let statusView = StatusView()
statusView.headerContainerView.isHidden = false statusView.headerContainerView.isHidden = false
statusView.avatarButton.isHidden = true statusView.avatarImageView.isHidden = true
statusView.avatarStackedContainerButton.isHidden = false statusView.avatarStackedContainerButton.isHidden = false
statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure( statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(
with: AvatarConfigurableViewConfiguration( with: AvatarConfigurableViewConfiguration(
@ -642,7 +608,6 @@ struct StatusView_Previews: PreviewProvider {
statusView.setNeedsLayout() statusView.setNeedsLayout()
statusView.layoutIfNeeded() statusView.layoutIfNeeded()
statusView.updateContentWarningDisplay(isHidden: false, animated: false) statusView.updateContentWarningDisplay(isHidden: false, animated: false)
statusView.drawContentWarningImageView()
let images = MosaicImageView_Previews.images let images = MosaicImageView_Previews.images
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162)) let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162))
for (i, mosaic) in mosaics.enumerated() { for (i, mosaic) in mosaics.enumerated() {

View File

@ -7,7 +7,9 @@
import os.log import os.log
import UIKit import UIKit
final class AvatarStackedImageView: UIImageView { } import FLAnimatedImage
final class AvatarStackedImageView: FLAnimatedImageView { }
// MARK: - AvatarConfigurableView // MARK: - AvatarConfigurableView
extension AvatarStackedImageView: AvatarConfigurableView { extension AvatarStackedImageView: AvatarConfigurableView {

View File

@ -21,7 +21,7 @@ protocol StatusTableViewCellDelegate: AnyObject {
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get } var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get }
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, headerInfoLabelDidPressed label: UILabel) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarButtonDidPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
@ -93,16 +93,6 @@ final class StatusTableViewCell: UITableViewCell, StatusCell {
_init() _init()
} }
override func layoutSubviews() {
super.layoutSubviews()
// precondition: app is active
guard UIApplication.shared.applicationState == .active else { return }
DispatchQueue.main.async {
self.statusView.drawContentWarningImageView()
}
}
} }
extension StatusTableViewCell { extension StatusTableViewCell {
@ -154,18 +144,7 @@ extension StatusTableViewCell {
resetSeparatorLineLayout() resetSeparatorLineLayout()
} }
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
resetContentOverlayBlurImageBackgroundColor(selected: highlighted)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
resetContentOverlayBlurImageBackgroundColor(selected: selected)
}
} }
@ -199,11 +178,7 @@ extension StatusTableViewCell {
} }
} }
} }
private func resetContentOverlayBlurImageBackgroundColor(selected: Bool) {
let imageViewBackgroundColor: UIColor? = selected ? selectedBackgroundView?.backgroundColor : backgroundColor
statusView.contentWarningOverlayView.blurContentImageView.backgroundColor = imageViewBackgroundColor
}
} }
// MARK: - MosaicImageViewContainerPresentable // MARK: - MosaicImageViewContainerPresentable
@ -301,9 +276,9 @@ extension StatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) { func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) {
delegate?.statusTableViewCell(self, statusView: statusView, headerInfoLabelDidPressed: label) delegate?.statusTableViewCell(self, statusView: statusView, headerInfoLabelDidPressed: label)
} }
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) { func statusView(_ statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView) {
delegate?.statusTableViewCell(self, statusView: statusView, avatarButtonDidPressed: button) delegate?.statusTableViewCell(self, statusView: statusView, avatarImageViewDidPressed: imageView)
} }
func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) { func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) {