fix: toggle content overlay not works for reblog issue. Update content overlay UI
This commit is contained in:
parent
da9af9a076
commit
51c29fa564
|
@ -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": {
|
||||||
|
|
|
@ -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" */;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue