Merge branch 'fix/home-timeline' into develop
This commit is contained in:
commit
ee0479682a
@ -27,7 +27,7 @@ public final class Attachment: NSManagedObject {
|
|||||||
@NSManaged public private(set) var updatedAt: Date
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
@NSManaged public private(set) var index: NSNumber
|
@NSManaged public private(set) var index: NSNumber
|
||||||
|
|
||||||
// many-to-one relastionship
|
// many-to-one relationship
|
||||||
@NSManaged public private(set) var status: Status?
|
@NSManaged public private(set) var status: Status?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,8 @@
|
|||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"timestamp": {
|
"timestamp": {
|
||||||
"now": "Now"
|
"now": "Now",
|
||||||
|
"time_ago": "%s ago"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"load_missing_posts": "Load missing posts",
|
"load_missing_posts": "Load missing posts",
|
||||||
|
@ -417,6 +417,7 @@
|
|||||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
||||||
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; };
|
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; };
|
||||||
DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; };
|
DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; };
|
||||||
|
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */; };
|
||||||
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
|
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
|
||||||
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
||||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
||||||
@ -989,6 +990,7 @@
|
|||||||
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
|
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
|
||||||
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
||||||
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; };
|
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; };
|
||||||
|
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentCacheService.swift; sourceTree = "<group>"; };
|
||||||
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; };
|
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; };
|
||||||
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
||||||
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
||||||
@ -1358,6 +1360,7 @@
|
|||||||
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
|
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
|
||||||
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
|
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
|
||||||
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
|
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
|
||||||
|
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */,
|
||||||
);
|
);
|
||||||
path = Service;
|
path = Service;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -2985,6 +2988,7 @@
|
|||||||
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */,
|
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */,
|
||||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
|
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
|
||||||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
||||||
|
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */,
|
||||||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
|
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
|
||||||
|
@ -12,12 +12,12 @@
|
|||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>15</integer>
|
<integer>17</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - Release.xcscheme_^#shared#^_</key>
|
<key>Mastodon - Release.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
@ -27,7 +27,7 @@
|
|||||||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>12</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -194,9 +194,15 @@ extension StatusSection {
|
|||||||
let author = (status.reblog ?? status).author
|
let author = (status.reblog ?? status).author
|
||||||
return author.displayName.isEmpty ? author.username : author.displayName
|
return author.displayName.isEmpty ? author.username : author.displayName
|
||||||
}()
|
}()
|
||||||
cell.statusView.nameLabel.configure(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict)
|
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
|
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||||
|
|
||||||
// set avatar
|
// set avatar
|
||||||
if let reblog = status.reblog {
|
if let reblog = status.reblog {
|
||||||
cell.statusView.avatarButton.isHidden = true
|
cell.statusView.avatarButton.isHidden = true
|
||||||
@ -210,6 +216,19 @@ extension StatusSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set text
|
// 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(
|
cell.statusView.activeTextLabel.configure(
|
||||||
content: (status.reblog ?? status).content,
|
content: (status.reblog ?? status).content,
|
||||||
emojiDict: (status.reblog ?? status).emojiDict
|
emojiDict: (status.reblog ?? status).emojiDict
|
||||||
@ -221,7 +240,7 @@ extension StatusSection {
|
|||||||
cell.statusView.updateVisibility(visibility: visibility)
|
cell.statusView.updateVisibility(visibility: visibility)
|
||||||
|
|
||||||
cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
|
cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak cell] isHidden in
|
.sink { [weak cell] isHidden in
|
||||||
cell?.statusView.visibilityImageView.isHidden = !isHidden
|
cell?.statusView.visibilityImageView.isHidden = !isHidden
|
||||||
}
|
}
|
||||||
@ -305,7 +324,7 @@ extension StatusSection {
|
|||||||
switch result {
|
switch result {
|
||||||
case .failure:
|
case .failure:
|
||||||
break
|
break
|
||||||
case .success(let response)
|
case .success:
|
||||||
statusItemAttribute.isImageLoaded.value = true
|
statusItemAttribute.isImageLoaded.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -356,12 +375,11 @@ extension StatusSection {
|
|||||||
return containerFrame.width
|
return containerFrame.width
|
||||||
}()
|
}()
|
||||||
let scale: CGFloat = 1.3
|
let scale: CGFloat = 1.3
|
||||||
return CGSize(width: maxWidth, height: maxWidth * scale)
|
return CGSize(width: maxWidth, height: floor(maxWidth * scale))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
|
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
|
||||||
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
|
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) {
|
||||||
{
|
|
||||||
var parent: UIViewController?
|
var parent: UIViewController?
|
||||||
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? = nil
|
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? = nil
|
||||||
switch cell {
|
switch cell {
|
||||||
@ -389,24 +407,35 @@ extension StatusSection {
|
|||||||
playerViewController.player = videoPlayerViewModel.player
|
playerViewController.player = videoPlayerViewModel.player
|
||||||
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
|
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
|
||||||
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
|
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
|
||||||
if videoPlayerViewModel.videoKind == .gif {
|
switch videoPlayerViewModel.videoKind {
|
||||||
|
case .gif:
|
||||||
playerContainerView.setMediaIndicator(isHidden: false)
|
playerContainerView.setMediaIndicator(isHidden: false)
|
||||||
} else {
|
case .video:
|
||||||
videoPlayerViewModel.timeControlStatus.sink { timeControlStatus in
|
playerContainerView.setMediaIndicator(isHidden: true)
|
||||||
UIView.animate(withDuration: 0.33) {
|
}
|
||||||
switch timeControlStatus {
|
playerContainerView.isHidden = false
|
||||||
case .playing:
|
|
||||||
playerContainerView.setMediaIndicator(isHidden: true)
|
// set blurhash overlay
|
||||||
case .paused, .waitingToPlayAtSpecifiedRate:
|
playerContainerView.isReadyForDisplay
|
||||||
playerContainerView.setMediaIndicator(isHidden: false)
|
.receive(on: DispatchQueue.main)
|
||||||
@unknown default:
|
.sink { [weak playerContainerView] isReadyForDisplay in
|
||||||
assertionFailure()
|
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)
|
.store(in: &cell.disposeBag)
|
||||||
}
|
}
|
||||||
playerContainerView.isHidden = false
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
cell.statusView.playerContainerView.playerViewController.player?.pause()
|
cell.statusView.playerContainerView.playerViewController.player?.pause()
|
||||||
@ -646,7 +675,13 @@ extension StatusSection {
|
|||||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||||
return L10n.Common.Controls.Status.userReblogged(name)
|
return L10n.Common.Controls.Status.userReblogged(name)
|
||||||
}()
|
}()
|
||||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict)
|
MastodonStatusContent.parseResult(content: headerText, emojiDict: status.author.emojiDict)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak cell] parseResult in
|
||||||
|
guard let cell = cell else { return }
|
||||||
|
cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
||||||
} else if status.inReplyToID != nil {
|
} else if status.inReplyToID != nil {
|
||||||
cell.statusView.headerContainerView.isHidden = false
|
cell.statusView.headerContainerView.isHidden = false
|
||||||
@ -659,7 +694,13 @@ extension StatusSection {
|
|||||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||||
return L10n.Common.Controls.Status.userRepliedTo(name)
|
return L10n.Common.Controls.Status.userRepliedTo(name)
|
||||||
}()
|
}()
|
||||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
|
MastodonStatusContent.parseResult(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak cell] parseResult in
|
||||||
|
guard let cell = cell else { return }
|
||||||
|
cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
||||||
} else {
|
} else {
|
||||||
cell.statusView.headerContainerView.isHidden = true
|
cell.statusView.headerContainerView.isHidden = true
|
||||||
|
@ -61,6 +61,7 @@ extension ActiveLabel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension ActiveLabel {
|
extension ActiveLabel {
|
||||||
|
|
||||||
/// status content
|
/// status content
|
||||||
func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||||
attributedText = nil
|
attributedText = nil
|
||||||
@ -76,6 +77,14 @@ extension ActiveLabel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configure(contentParseResult parseResult: MastodonStatusContent.ParseResult?) {
|
||||||
|
attributedText = nil
|
||||||
|
activeEntities.removeAll()
|
||||||
|
text = parseResult?.trimmed ?? ""
|
||||||
|
activeEntities = parseResult?.activeEntities ?? []
|
||||||
|
accessibilityLabel = parseResult?.original ?? nil
|
||||||
|
}
|
||||||
|
|
||||||
/// account note
|
/// account note
|
||||||
func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||||
configure(content: note, emojiDict: emojiDict)
|
configure(content: note, emojiDict: emojiDict)
|
||||||
|
@ -22,7 +22,8 @@ extension Date {
|
|||||||
if earlierDate.timeIntervalSince(latest) >= -60 {
|
if earlierDate.timeIntervalSince(latest) >= -60 {
|
||||||
return L10n.Common.Controls.Timeline.Timestamp.now
|
return L10n.Common.Controls.Timeline.Timestamp.now
|
||||||
} else {
|
} else {
|
||||||
return latest.shortTimeAgo(since: earlierDate)
|
let interval = latest.shortTimeAgo(since: earlierDate) // 1s
|
||||||
|
return L10n.Common.Controls.Timeline.Timestamp.timeAgo(interval) // 1s ago
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ internal enum Asset {
|
|||||||
internal static let searchCard = ColorAsset(name: "Colors/Shadow/SearchCard")
|
internal static let searchCard = ColorAsset(name: "Colors/Shadow/SearchCard")
|
||||||
}
|
}
|
||||||
internal enum Slider {
|
internal enum Slider {
|
||||||
internal static let bar = ColorAsset(name: "Colors/Slider/bar")
|
internal static let track = ColorAsset(name: "Colors/Slider/track")
|
||||||
}
|
}
|
||||||
internal enum TextField {
|
internal enum TextField {
|
||||||
internal static let background = ColorAsset(name: "Colors/TextField/background")
|
internal static let background = ColorAsset(name: "Colors/TextField/background")
|
||||||
|
@ -383,6 +383,10 @@ internal enum L10n {
|
|||||||
internal enum Timestamp {
|
internal enum Timestamp {
|
||||||
/// Now
|
/// Now
|
||||||
internal static let now = L10n.tr("Localizable", "Common.Controls.Timeline.Timestamp.Now")
|
internal static let now = L10n.tr("Localizable", "Common.Controls.Timeline.Timestamp.Now")
|
||||||
|
/// %@ ago
|
||||||
|
internal static func timeAgo(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Common.Controls.Timeline.Timestamp.TimeAgo", String(describing: p1))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
import Kanna
|
import Kanna
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
|
||||||
@ -14,6 +15,18 @@ enum MastodonStatusContent {
|
|||||||
typealias EmojiShortcode = String
|
typealias EmojiShortcode = String
|
||||||
typealias EmojiDict = [EmojiShortcode: URL]
|
typealias EmojiDict = [EmojiShortcode: URL]
|
||||||
|
|
||||||
|
static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive, attributes: .concurrent)
|
||||||
|
|
||||||
|
static func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> AnyPublisher<MastodonStatusContent.ParseResult?, Never> {
|
||||||
|
return Future { promise in
|
||||||
|
self.workingQueue.async {
|
||||||
|
let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict)
|
||||||
|
promise(.success(parseResult))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult {
|
static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult {
|
||||||
let document: String = {
|
let document: String = {
|
||||||
var content = content
|
var content = content
|
||||||
@ -113,11 +126,25 @@ extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonStatusContent {
|
extension MastodonStatusContent {
|
||||||
struct ParseResult {
|
struct ParseResult: Hashable {
|
||||||
let document: String
|
let document: String
|
||||||
let original: String
|
let original: String
|
||||||
let trimmed: String
|
let trimmed: String
|
||||||
let activeEntities: [ActiveEntity]
|
let activeEntities: [ActiveEntity]
|
||||||
|
|
||||||
|
static func == (lhs: MastodonStatusContent.ParseResult, rhs: MastodonStatusContent.ParseResult) -> Bool {
|
||||||
|
return lhs.document == rhs.document
|
||||||
|
&& lhs.original == rhs.original
|
||||||
|
&& lhs.trimmed == rhs.trimmed
|
||||||
|
&& lhs.activeEntities.count == rhs.activeEntities.count // FIXME:
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(document)
|
||||||
|
hasher.combine(original)
|
||||||
|
hasher.combine(trimmed)
|
||||||
|
hasher.combine(activeEntities.count) // FIXME:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,17 +33,22 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
for objectID in statusObjectIDs {
|
for objectID in statusObjectIDs {
|
||||||
let status = backgroundManagedObjectContext.object(with: objectID) as! Status
|
let status = backgroundManagedObjectContext.object(with: objectID) as! Status
|
||||||
guard let replyToID = status.inReplyToID, status.replyTo == nil else {
|
|
||||||
// skip
|
// fetch in-reply info if needs
|
||||||
continue
|
if let replyToID = status.inReplyToID, status.replyTo == nil {
|
||||||
|
self.context.statusPrefetchingService.prefetchReplyTo(
|
||||||
|
domain: domain,
|
||||||
|
statusObjectID: status.objectID,
|
||||||
|
statusID: status.id,
|
||||||
|
replyToStatusID: replyToID,
|
||||||
|
authorizationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
}
|
}
|
||||||
self.context.statusPrefetchingService.prefetchReplyTo(
|
|
||||||
domain: domain,
|
// self.context.statusContentCacheService.prefetch(
|
||||||
statusObjectID: status.objectID,
|
// content: (status.reblog ?? status).content,
|
||||||
statusID: status.id,
|
// emojiDict: (status.reblog ?? status).emojiDict
|
||||||
replyToStatusID: replyToID,
|
// )
|
||||||
authorizationBox: activeMastodonAuthenticationBox
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,7 +247,7 @@ extension StatusProviderFacade {
|
|||||||
}
|
}
|
||||||
.map { statusObjectID, favoriteKind -> AnyPublisher<(Status.ID, Mastodon.API.Favorites.FavoriteKind), Error> in
|
.map { statusObjectID, favoriteKind -> AnyPublisher<(Status.ID, Mastodon.API.Favorites.FavoriteKind), Error> in
|
||||||
return context.apiService.favorite(
|
return context.apiService.favorite(
|
||||||
statusObjectID: statusObjectID,
|
statusObjectID: statusObjectID,
|
||||||
mastodonUserObjectID: mastodonUserObjectID,
|
mastodonUserObjectID: mastodonUserObjectID,
|
||||||
favoriteKind: favoriteKind
|
favoriteKind: favoriteKind
|
||||||
)
|
)
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "0.600",
|
"alpha" : "0.600",
|
||||||
"blue" : "0",
|
"blue" : "67",
|
||||||
"green" : "0",
|
"green" : "60",
|
||||||
"red" : "0"
|
"red" : "60"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
@ -23,9 +23,9 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "0.600",
|
"alpha" : "0.600",
|
||||||
"blue" : "1.000",
|
"blue" : "245",
|
||||||
"green" : "1.000",
|
"green" : "235",
|
||||||
"red" : "1.000"
|
"red" : "235"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "147",
|
|
||||||
"green" : "106",
|
|
||||||
"red" : "51"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.600",
|
||||||
|
"blue" : "213",
|
||||||
|
"green" : "213",
|
||||||
|
"red" : "212"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.300",
|
||||||
|
"blue" : "1.000",
|
||||||
|
"green" : "1.000",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -133,6 +133,7 @@ Your account looks like this to them.";
|
|||||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||||
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
|
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
|
||||||
"Common.Controls.Timeline.Timestamp.Now" = "Now";
|
"Common.Controls.Timeline.Timestamp.Now" = "Now";
|
||||||
|
"Common.Controls.Timeline.Timestamp.TimeAgo" = "%@ ago";
|
||||||
"Common.Countable.Photo.Multiple" = "photos";
|
"Common.Countable.Photo.Multiple" = "photos";
|
||||||
"Common.Countable.Photo.Single" = "photo";
|
"Common.Countable.Photo.Single" = "photo";
|
||||||
"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment";
|
"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment";
|
||||||
|
@ -133,6 +133,7 @@ Your account looks like this to them.";
|
|||||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||||
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
|
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
|
||||||
"Common.Controls.Timeline.Timestamp.Now" = "Now";
|
"Common.Controls.Timeline.Timestamp.Now" = "Now";
|
||||||
|
"Common.Controls.Timeline.Timestamp.TimeAgo" = "%@ ago";
|
||||||
"Common.Countable.Photo.Multiple" = "photos";
|
"Common.Countable.Photo.Multiple" = "photos";
|
||||||
"Common.Countable.Photo.Single" = "photo";
|
"Common.Countable.Photo.Single" = "photo";
|
||||||
"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment";
|
"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment";
|
||||||
|
@ -24,7 +24,7 @@ class HashtagTimelineViewController: UIViewController, NeedsDependency, MediaPre
|
|||||||
|
|
||||||
let composeBarButtonItem: UIBarButtonItem = {
|
let composeBarButtonItem: UIBarButtonItem = {
|
||||||
let barButtonItem = UIBarButtonItem()
|
let barButtonItem = UIBarButtonItem()
|
||||||
barButtonItem.tintColor = Asset.Colors.Label.highlight.color
|
barButtonItem.tintColor = Asset.Colors.brandBlue.color
|
||||||
barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate)
|
barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate)
|
||||||
return barButtonItem
|
return barButtonItem
|
||||||
}()
|
}()
|
||||||
|
@ -15,6 +15,10 @@ import GameplayKit
|
|||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import GDPerformanceView_Swift
|
||||||
|
#endif
|
||||||
|
|
||||||
final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
@ -38,14 +42,14 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media
|
|||||||
|
|
||||||
let settingBarButtonItem: UIBarButtonItem = {
|
let settingBarButtonItem: UIBarButtonItem = {
|
||||||
let barButtonItem = UIBarButtonItem()
|
let barButtonItem = UIBarButtonItem()
|
||||||
barButtonItem.tintColor = Asset.Colors.Label.highlight.color
|
barButtonItem.tintColor = Asset.Colors.brandBlue.color
|
||||||
barButtonItem.image = UIImage(systemName: "gear")?.withRenderingMode(.alwaysTemplate)
|
barButtonItem.image = UIImage(systemName: "gear")?.withRenderingMode(.alwaysTemplate)
|
||||||
return barButtonItem
|
return barButtonItem
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let composeBarButtonItem: UIBarButtonItem = {
|
let composeBarButtonItem: UIBarButtonItem = {
|
||||||
let barButtonItem = UIBarButtonItem()
|
let barButtonItem = UIBarButtonItem()
|
||||||
barButtonItem.tintColor = Asset.Colors.Label.highlight.color
|
barButtonItem.tintColor = Asset.Colors.brandBlue.color
|
||||||
barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate)
|
barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate)
|
||||||
return barButtonItem
|
return barButtonItem
|
||||||
}()
|
}()
|
||||||
@ -87,6 +91,7 @@ extension HomeTimelineViewController {
|
|||||||
titleView.delegate = self
|
titleView.delegate = self
|
||||||
|
|
||||||
viewModel.homeTimelineNavigationBarTitleViewModel.state
|
viewModel.homeTimelineNavigationBarTitleViewModel.state
|
||||||
|
.removeDuplicates()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] state in
|
.sink { [weak self] state in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@ -94,10 +99,11 @@ extension HomeTimelineViewController {
|
|||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// long press to trigger debug menu
|
// long press to trigger debug menu
|
||||||
settingBarButtonItem.menu = debugMenu
|
settingBarButtonItem.menu = debugMenu
|
||||||
|
PerformanceMonitor.shared().delegate = self
|
||||||
|
|
||||||
#else
|
#else
|
||||||
settingBarButtonItem.target = self
|
settingBarButtonItem.target = self
|
||||||
settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
|
settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
|
||||||
@ -554,3 +560,11 @@ extension HomeTimelineViewController: StatusTableViewControllerNavigateable {
|
|||||||
statusKeyCommandHandler(sender)
|
statusKeyCommandHandler(sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
extension HomeTimelineViewController: PerformanceMonitorDelegate {
|
||||||
|
func performanceMonitor(didReport performanceReport: PerformanceReport) {
|
||||||
|
// print(performanceReport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
@ -36,6 +36,7 @@ final class HomeTimelineViewModel: NSObject {
|
|||||||
|
|
||||||
let timelineIsEmpty = CurrentValueSubject<Bool, Never>(false)
|
let timelineIsEmpty = CurrentValueSubject<Bool, Never>(false)
|
||||||
let homeTimelineNeedRefresh = PassthroughSubject<Void, Never>()
|
let homeTimelineNeedRefresh = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
// output
|
// output
|
||||||
// top loader
|
// top loader
|
||||||
private(set) lazy var loadLatestStateMachine: GKStateMachine = {
|
private(set) lazy var loadLatestStateMachine: GKStateMachine = {
|
||||||
@ -130,6 +131,12 @@ final class HomeTimelineViewModel: NSObject {
|
|||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
homeTimelineNavigationBarTitleViewModel.isPublished
|
||||||
|
.sink { [weak self] isPublished in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.homeTimelineNeedRefresh.send()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
@ -106,9 +106,11 @@ extension MainTabBarController {
|
|||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
|
|
||||||
// TODO: custom accent color
|
// TODO: custom accent color
|
||||||
// let tabBarAppearance = UITabBarAppearance()
|
let tabBarAppearance = UITabBarAppearance()
|
||||||
// tabBarAppearance.configureWithDefaultBackground()
|
tabBarAppearance.configureWithDefaultBackground()
|
||||||
// tabBar.standardAppearance = tabBarAppearance
|
tabBarAppearance.selectionIndicatorTintColor = Asset.Colors.brandBlue.color
|
||||||
|
tabBar.standardAppearance = tabBarAppearance
|
||||||
|
|
||||||
|
|
||||||
context.apiService.error
|
context.apiService.error
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -52,9 +52,10 @@ final class AudioContainerView: UIView {
|
|||||||
|
|
||||||
let slider: UISlider = {
|
let slider: UISlider = {
|
||||||
let slider = UISlider()
|
let slider = UISlider()
|
||||||
|
slider.isContinuous = true
|
||||||
slider.translatesAutoresizingMaskIntoConstraints = false
|
slider.translatesAutoresizingMaskIntoConstraints = false
|
||||||
slider.minimumTrackTintColor = Asset.Colors.Slider.bar.color
|
slider.minimumTrackTintColor = Asset.Colors.Slider.track.color
|
||||||
slider.maximumTrackTintColor = Asset.Colors.Slider.bar.color
|
slider.maximumTrackTintColor = Asset.Colors.Slider.track.color
|
||||||
if let image = UIImage.placeholder(size: CGSize(width: 22, height: 22), color: .white).withRoundedCorners(radius: 11) {
|
if let image = UIImage.placeholder(size: CGSize(width: 22, height: 22), color: .white).withRoundedCorners(radius: 11) {
|
||||||
slider.setThumbImage(image, for: .normal)
|
slider.setThumbImage(image, for: .normal)
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import UIKit
|
|||||||
|
|
||||||
extension PlayerContainerView {
|
extension PlayerContainerView {
|
||||||
|
|
||||||
final class MediaTypeIndicotorView: UIView {
|
final class MediaTypeIndicatorView: UIView {
|
||||||
|
|
||||||
static let indicatorViewSize = CGSize(width: 47, height: 25)
|
static let indicatorViewSize = CGSize(width: 47, height: 25)
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ extension PlayerContainerView {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PlayerContainerView.MediaTypeIndicotorView {
|
extension PlayerContainerView.MediaTypeIndicatorView {
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
backgroundColor = Asset.Colors.Background.mediaTypeIndicotor.color
|
backgroundColor = Asset.Colors.Background.mediaTypeIndicotor.color
|
||||||
@ -87,14 +87,10 @@ extension PlayerContainerView.MediaTypeIndicotorView {
|
|||||||
|
|
||||||
switch kind {
|
switch kind {
|
||||||
case .gif:
|
case .gif:
|
||||||
label.font = PlayerContainerView.MediaTypeIndicotorView.roundedFont(weight: .heavy, fontSize: fontSize)
|
label.font = PlayerContainerView.MediaTypeIndicatorView.roundedFont(weight: .heavy, fontSize: fontSize)
|
||||||
label.text = "GIF"
|
label.text = "GIF"
|
||||||
case .video:
|
case .video:
|
||||||
let configuration = UIImage.SymbolConfiguration(font: PlayerContainerView.MediaTypeIndicotorView.roundedFont(weight: .regular, fontSize: fontSize))
|
label.text = " "
|
||||||
let image = UIImage(systemName: "video.fill", withConfiguration: configuration)!
|
|
||||||
let attachment = NSTextAttachment()
|
|
||||||
attachment.image = image.withTintColor(.white)
|
|
||||||
label.attributedText = NSAttributedString(attachment: attachment)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,12 +99,12 @@ extension PlayerContainerView.MediaTypeIndicotorView {
|
|||||||
#if canImport(SwiftUI) && DEBUG
|
#if canImport(SwiftUI) && DEBUG
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PlayerContainerViewMediaTypeIndicotorView_Previews: PreviewProvider {
|
struct PlayerContainerViewMediaTypeIndicatorView_Previews: PreviewProvider {
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
UIViewPreview(width: 47) {
|
UIViewPreview(width: 47) {
|
||||||
let view = PlayerContainerView.MediaTypeIndicotorView()
|
let view = PlayerContainerView.MediaTypeIndicatorView()
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
view.heightAnchor.constraint(equalToConstant: 25),
|
view.heightAnchor.constraint(equalToConstant: 25),
|
||||||
@ -118,17 +114,6 @@ struct PlayerContainerViewMediaTypeIndicotorView_Previews: PreviewProvider {
|
|||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 47, height: 25))
|
.previewLayout(.fixed(width: 47, height: 25))
|
||||||
UIViewPreview(width: 47) {
|
|
||||||
let view = PlayerContainerView.MediaTypeIndicotorView()
|
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
view.heightAnchor.constraint(equalToConstant: 25),
|
|
||||||
view.widthAnchor.constraint(equalToConstant: 47),
|
|
||||||
])
|
|
||||||
view.setMediaKind(kind: .video)
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
.previewLayout(.fixed(width: 47, height: 25))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
import os.log
|
import os.log
|
||||||
import AVKit
|
import AVKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
protocol PlayerContainerViewDelegate: AnyObject {
|
protocol PlayerContainerViewDelegate: AnyObject {
|
||||||
func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
@ -28,11 +29,14 @@ final class PlayerContainerView: UIView {
|
|||||||
|
|
||||||
let playerViewController = AVPlayerViewController()
|
let playerViewController = AVPlayerViewController()
|
||||||
|
|
||||||
let mediaTypeIndicotorView = MediaTypeIndicotorView()
|
let blurhashOverlayImageView = UIImageView()
|
||||||
let mediaTypeIndicotorViewInContentWarningOverlay = MediaTypeIndicotorView()
|
let mediaTypeIndicatorView = MediaTypeIndicatorView()
|
||||||
|
|
||||||
weak var delegate: PlayerContainerViewDelegate?
|
weak var delegate: PlayerContainerViewDelegate?
|
||||||
|
|
||||||
|
private var isReadyForDisplayObservation: NSKeyValueObservation?
|
||||||
|
let isReadyForDisplay = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
@ -65,25 +69,31 @@ extension PlayerContainerView {
|
|||||||
playerViewController.view.layer.cornerRadius = PlayerContainerView.cornerRadius
|
playerViewController.view.layer.cornerRadius = PlayerContainerView.cornerRadius
|
||||||
playerViewController.view.layer.cornerCurve = .continuous
|
playerViewController.view.layer.cornerCurve = .continuous
|
||||||
|
|
||||||
// mediaType
|
blurhashOverlayImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
mediaTypeIndicotorView.translatesAutoresizingMaskIntoConstraints = false
|
playerViewController.contentOverlayView!.addSubview(blurhashOverlayImageView)
|
||||||
playerViewController.contentOverlayView!.addSubview(mediaTypeIndicotorView)
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor),
|
blurhashOverlayImageView.topAnchor.constraint(equalTo: playerViewController.contentOverlayView!.topAnchor),
|
||||||
mediaTypeIndicotorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor),
|
blurhashOverlayImageView.leadingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.leadingAnchor),
|
||||||
mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1),
|
blurhashOverlayImageView.trailingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.trailingAnchor),
|
||||||
mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1),
|
blurhashOverlayImageView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.translatesAutoresizingMaskIntoConstraints = false
|
// mediaType
|
||||||
contentWarningOverlayView.addSubview(mediaTypeIndicotorViewInContentWarningOverlay)
|
mediaTypeIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
playerViewController.contentOverlayView!.addSubview(mediaTypeIndicatorView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.bottomAnchor.constraint(equalTo: contentWarningOverlayView.bottomAnchor),
|
mediaTypeIndicatorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor),
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.rightAnchor.constraint(equalTo: contentWarningOverlayView.rightAnchor),
|
mediaTypeIndicatorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor),
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1),
|
mediaTypeIndicatorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicatorView.indicatorViewSize.height).priority(.required - 1),
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1),
|
mediaTypeIndicatorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicatorView.indicatorViewSize.width).priority(.required - 1),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
isReadyForDisplayObservation = playerViewController.observe(\.isReadyForDisplay, options: [.initial, .new]) { [weak self] playerViewController, _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: isReadyForDisplay: %s", (#file as NSString).lastPathComponent, #line, #function, playerViewController.isReadyForDisplay.description)
|
||||||
|
self.isReadyForDisplay.value = playerViewController.isReadyForDisplay
|
||||||
|
}
|
||||||
|
|
||||||
contentWarningOverlayView.delegate = self
|
contentWarningOverlayView.delegate = self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,6 +114,8 @@ extension PlayerContainerView {
|
|||||||
playerViewController.view.removeFromSuperview()
|
playerViewController.view.removeFromSuperview()
|
||||||
playerViewController.removeFromParent()
|
playerViewController.removeFromParent()
|
||||||
|
|
||||||
|
blurhashOverlayImageView.image = nil
|
||||||
|
|
||||||
container.subviews.forEach { subview in
|
container.subviews.forEach { subview in
|
||||||
subview.removeFromSuperview()
|
subview.removeFromSuperview()
|
||||||
}
|
}
|
||||||
@ -123,7 +135,7 @@ extension PlayerContainerView {
|
|||||||
let rect = AVMakeRect(
|
let rect = AVMakeRect(
|
||||||
aspectRatio: aspectRatio,
|
aspectRatio: aspectRatio,
|
||||||
insideRect: CGRect(origin: .zero, size: maxSize)
|
insideRect: CGRect(origin: .zero, size: maxSize)
|
||||||
)
|
).integral
|
||||||
|
|
||||||
parent?.addChild(playerViewController)
|
parent?.addChild(playerViewController)
|
||||||
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@ -134,11 +146,13 @@ extension PlayerContainerView {
|
|||||||
playerViewController.view.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor),
|
playerViewController.view.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor),
|
||||||
playerViewController.view.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor),
|
playerViewController.view.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor),
|
||||||
playerViewController.view.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor),
|
playerViewController.view.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor),
|
||||||
touchBlockingView.widthAnchor.constraint(equalToConstant: floor(rect.width)).priority(.required - 1),
|
touchBlockingView.widthAnchor.constraint(equalToConstant: rect.width).priority(.required - 1),
|
||||||
])
|
])
|
||||||
containerHeightLayoutConstraint.constant = floor(rect.height)
|
containerHeightLayoutConstraint.constant = rect.height
|
||||||
containerHeightLayoutConstraint.isActive = true
|
containerHeightLayoutConstraint.isActive = true
|
||||||
|
|
||||||
|
playerViewController.view.frame.size = rect.size
|
||||||
|
|
||||||
contentWarningOverlayView.removeFromSuperview()
|
contentWarningOverlayView.removeFromSuperview()
|
||||||
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(contentWarningOverlayView)
|
addSubview(contentWarningOverlayView)
|
||||||
@ -149,19 +163,17 @@ extension PlayerContainerView {
|
|||||||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor)
|
contentWarningOverlayView.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
bringSubviewToFront(mediaTypeIndicotorView)
|
bringSubviewToFront(mediaTypeIndicatorView)
|
||||||
|
|
||||||
return playerViewController
|
return playerViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMediaKind(kind: VideoPlayerViewModel.Kind) {
|
func setMediaKind(kind: VideoPlayerViewModel.Kind) {
|
||||||
mediaTypeIndicotorView.setMediaKind(kind: kind)
|
mediaTypeIndicatorView.setMediaKind(kind: kind)
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.setMediaKind(kind: kind)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMediaIndicator(isHidden: Bool) {
|
func setMediaIndicator(isHidden: Bool) {
|
||||||
mediaTypeIndicotorView.alpha = isHidden ? 0 : 1
|
mediaTypeIndicatorView.alpha = isHidden ? 0 : 1
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.alpha = isHidden ? 0 : 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -253,6 +253,7 @@ extension StatusView {
|
|||||||
// header container: [icon | info]
|
// header container: [icon | info]
|
||||||
let headerContainerStackView = UIStackView()
|
let headerContainerStackView = UIStackView()
|
||||||
headerContainerStackView.axis = .horizontal
|
headerContainerStackView.axis = .horizontal
|
||||||
|
headerContainerStackView.spacing = 4
|
||||||
headerContainerStackView.addArrangedSubview(headerIconLabel)
|
headerContainerStackView.addArrangedSubview(headerIconLabel)
|
||||||
headerContainerStackView.addArrangedSubview(headerInfoLabel)
|
headerContainerStackView.addArrangedSubview(headerInfoLabel)
|
||||||
headerIconLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
headerIconLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
@ -17,10 +17,10 @@ protocol ActionToolbarContainerDelegate: AnyObject {
|
|||||||
|
|
||||||
final class ActionToolbarContainer: UIView {
|
final class ActionToolbarContainer: UIView {
|
||||||
|
|
||||||
let replyButton = HitTestExpandedButton()
|
let replyButton = HighlightDimmableButton()
|
||||||
let reblogButton = HitTestExpandedButton()
|
let reblogButton = HighlightDimmableButton()
|
||||||
let favoriteButton = HitTestExpandedButton()
|
let favoriteButton = HighlightDimmableButton()
|
||||||
let moreButton = HitTestExpandedButton()
|
let moreButton = HighlightDimmableButton()
|
||||||
|
|
||||||
var isReblogButtonHighlight: Bool = false {
|
var isReblogButtonHighlight: Bool = false {
|
||||||
didSet { isReblogButtonHighlightStateDidChange(to: isReblogButtonHighlight) }
|
didSet { isReblogButtonHighlightStateDidChange(to: isReblogButtonHighlight) }
|
||||||
@ -97,6 +97,7 @@ extension ActionToolbarContainer {
|
|||||||
button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
|
button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
|
||||||
button.setTitle("", for: .normal)
|
button.setTitle("", for: .normal)
|
||||||
button.setTitleColor(.secondaryLabel, for: .normal)
|
button.setTitleColor(.secondaryLabel, for: .normal)
|
||||||
|
button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
|
||||||
button.setInsets(forContentPadding: .zero, imageTitlePadding: style.buttonTitleImagePadding)
|
button.setInsets(forContentPadding: .zero, imageTitlePadding: style.buttonTitleImagePadding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class AudioContainerViewModel {
|
class AudioContainerViewModel {
|
||||||
|
|
||||||
static func configure(
|
static func configure(
|
||||||
cell: StatusCell,
|
cell: StatusCell,
|
||||||
audioAttachment: Attachment,
|
audioAttachment: Attachment,
|
||||||
@ -36,11 +37,12 @@ class AudioContainerViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
audioView.slider.maximumValue = Float(duration)
|
||||||
audioView.slider.publisher(for: .valueChanged)
|
audioView.slider.publisher(for: .valueChanged)
|
||||||
.sink { [weak audioService] slider in
|
.sink { [weak audioService] slider in
|
||||||
guard let audioService = audioService else { return }
|
guard let audioService = audioService else { return }
|
||||||
let slider = slider as! UISlider
|
let slider = slider as! UISlider
|
||||||
let time = Double(slider.value) * duration
|
let time = TimeInterval(slider.value)
|
||||||
audioService.seekToTime(time: time)
|
audioService.seekToTime(time: time)
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
@ -58,24 +60,24 @@ class AudioContainerViewModel {
|
|||||||
let audioView = cell.statusView.audioView
|
let audioView = cell.statusView.audioView
|
||||||
var lastCurrentTimeSubject: TimeInterval?
|
var lastCurrentTimeSubject: TimeInterval?
|
||||||
audioService.currentTimeSubject
|
audioService.currentTimeSubject
|
||||||
.throttle(for: 0.33, scheduler: DispatchQueue.main, latest: true)
|
.throttle(for: 0.008, scheduler: DispatchQueue.main, latest: true)
|
||||||
.compactMap { [weak audioService] time -> (TimeInterval, Float)? in
|
.compactMap { [weak audioService] time -> TimeInterval? in
|
||||||
defer {
|
defer {
|
||||||
lastCurrentTimeSubject = time
|
lastCurrentTimeSubject = time
|
||||||
}
|
}
|
||||||
guard audioAttachment === audioService?.attachment else { return nil }
|
guard audioAttachment === audioService?.attachment else { return nil }
|
||||||
guard let duration = audioAttachment.meta?.original?.duration else { return nil }
|
// guard let duration = audioAttachment.meta?.original?.duration else { return nil }
|
||||||
|
|
||||||
if let lastCurrentTimeSubject = lastCurrentTimeSubject, time != 0.0 {
|
if let lastCurrentTimeSubject = lastCurrentTimeSubject, time != 0.0 {
|
||||||
guard abs(time - lastCurrentTimeSubject) < 0.5 else { return nil } // debounce
|
guard abs(time - lastCurrentTimeSubject) < 0.5 else { return nil } // debounce
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !audioView.slider.isTracking else { return nil }
|
guard !audioView.slider.isTracking else { return nil }
|
||||||
return (time, Float(time / duration))
|
return TimeInterval(time)
|
||||||
}
|
}
|
||||||
.sink(receiveValue: { time, progress in
|
.sink(receiveValue: { time in
|
||||||
audioView.timeLabel.text = time.asString(style: .positional)
|
audioView.timeLabel.text = time.asString(style: .positional)
|
||||||
audioView.slider.setValue(progress, animated: true)
|
audioView.slider.setValue(Float(time), animated: true)
|
||||||
})
|
})
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
audioService.playbackState
|
audioService.playbackState
|
||||||
@ -98,14 +100,14 @@ class AudioContainerViewModel {
|
|||||||
switch playbackState {
|
switch playbackState {
|
||||||
case .stopped:
|
case .stopped:
|
||||||
audioView.playButton.isSelected = false
|
audioView.playButton.isSelected = false
|
||||||
audioView.slider.isEnabled = false
|
audioView.slider.isUserInteractionEnabled = false
|
||||||
audioView.slider.setValue(0, animated: false)
|
audioView.slider.setValue(0, animated: false)
|
||||||
case .paused:
|
case .paused:
|
||||||
audioView.playButton.isSelected = false
|
audioView.playButton.isSelected = false
|
||||||
audioView.slider.isEnabled = true
|
audioView.slider.isUserInteractionEnabled = true
|
||||||
case .playing, .readyToPlay:
|
case .playing, .readyToPlay:
|
||||||
audioView.playButton.isSelected = true
|
audioView.playButton.isSelected = true
|
||||||
audioView.slider.isEnabled = true
|
audioView.slider.isUserInteractionEnabled = true
|
||||||
default:
|
default:
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,14 @@ extension APIService {
|
|||||||
let targetStatusID = targetStatus.id
|
let targetStatusID = targetStatus.id
|
||||||
_targetStatusID = targetStatusID
|
_targetStatusID = targetStatusID
|
||||||
|
|
||||||
|
let favouritesCount: NSNumber
|
||||||
|
switch favoriteKind {
|
||||||
|
case .create:
|
||||||
|
favouritesCount = NSNumber(value: targetStatus.favouritesCount.intValue + 1)
|
||||||
|
case .destroy:
|
||||||
|
favouritesCount = NSNumber(value: max(0, targetStatus.favouritesCount.intValue - 1))
|
||||||
|
}
|
||||||
|
targetStatus.update(favouritesCount: favouritesCount)
|
||||||
targetStatus.update(liked: favoriteKind == .create, by: mastodonUser)
|
targetStatus.update(liked: favoriteKind == .create, by: mastodonUser)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,12 +29,17 @@ extension APIService {
|
|||||||
let targetStatusID = targetStatus.id
|
let targetStatusID = targetStatus.id
|
||||||
_targetStatusID = targetStatusID
|
_targetStatusID = targetStatusID
|
||||||
|
|
||||||
|
let reblogsCount: NSNumber
|
||||||
switch reblogKind {
|
switch reblogKind {
|
||||||
case .reblog:
|
case .reblog:
|
||||||
targetStatus.update(reblogged: true, by: mastodonUser)
|
targetStatus.update(reblogged: true, by: mastodonUser)
|
||||||
|
reblogsCount = NSNumber(value: targetStatus.reblogsCount.intValue + 1)
|
||||||
case .undoReblog:
|
case .undoReblog:
|
||||||
targetStatus.update(reblogged: false, by: mastodonUser)
|
targetStatus.update(reblogged: false, by: mastodonUser)
|
||||||
|
reblogsCount = NSNumber(value: max(0, targetStatus.reblogsCount.intValue - 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetStatus.update(reblogsCount: reblogsCount)
|
||||||
|
|
||||||
}
|
}
|
||||||
.tryMap { result in
|
.tryMap { result in
|
||||||
|
77
Mastodon/Service/StatusContentCacheService.swift
Normal file
77
Mastodon/Service/StatusContentCacheService.swift
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
//
|
||||||
|
// StatusContentCacheService.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class StatusContentCacheService {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let cache = NSCache<Key, ParseResultWrapper>()
|
||||||
|
|
||||||
|
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent)
|
||||||
|
|
||||||
|
func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> MastodonStatusContent.ParseResult? {
|
||||||
|
let key = Key(content: content, emojiDict: emojiDict)
|
||||||
|
return cache.object(forKey: key)?.parseResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefetch(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||||
|
let key = Key(content: content, emojiDict: emojiDict)
|
||||||
|
guard cache.object(forKey: key) == nil else { return }
|
||||||
|
MastodonStatusContent.parseResult(content: content, emojiDict: emojiDict)
|
||||||
|
.sink { [weak self] parseResult in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let parseResult = parseResult else { return }
|
||||||
|
let wrapper = ParseResultWrapper(parseResult: parseResult)
|
||||||
|
self.cache.setObject(wrapper, forKey: key)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusContentCacheService {
|
||||||
|
class Key: NSObject {
|
||||||
|
let content: String
|
||||||
|
let emojiDict: MastodonStatusContent.EmojiDict
|
||||||
|
|
||||||
|
init(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||||
|
self.content = content
|
||||||
|
self.emojiDict = emojiDict
|
||||||
|
}
|
||||||
|
|
||||||
|
override func isEqual(_ object: Any?) -> Bool {
|
||||||
|
guard let object = object as? Key else { return false }
|
||||||
|
return object.content == content
|
||||||
|
&& object.emojiDict == emojiDict
|
||||||
|
}
|
||||||
|
|
||||||
|
override var hash: Int {
|
||||||
|
return content.hashValue ^
|
||||||
|
emojiDict.hashValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParseResultWrapper: NSObject {
|
||||||
|
let parseResult: MastodonStatusContent.ParseResult
|
||||||
|
|
||||||
|
init(parseResult: MastodonStatusContent.ParseResult) {
|
||||||
|
self.parseResult = parseResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override func isEqual(_ object: Any?) -> Bool {
|
||||||
|
guard let object = object as? ParseResultWrapper else { return false }
|
||||||
|
return object.parseResult == parseResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override var hash: Int {
|
||||||
|
return parseResult.hashValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,7 @@ class AppContext: ObservableObject {
|
|||||||
|
|
||||||
let placeholderImageCacheService = PlaceholderImageCacheService()
|
let placeholderImageCacheService = PlaceholderImageCacheService()
|
||||||
let blurhashImageCacheService = BlurhashImageCacheService()
|
let blurhashImageCacheService = BlurhashImageCacheService()
|
||||||
|
let statusContentCacheService = StatusContentCacheService()
|
||||||
|
|
||||||
let documentStore: DocumentStore
|
let documentStore: DocumentStore
|
||||||
private var documentStoreSubscription: AnyCancellable!
|
private var documentStoreSubscription: AnyCancellable!
|
||||||
|
@ -10,6 +10,10 @@ import UIKit
|
|||||||
import UserNotifications
|
import UserNotifications
|
||||||
import AppShared
|
import AppShared
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import GDPerformanceView_Swift
|
||||||
|
#endif
|
||||||
|
|
||||||
@main
|
@main
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
@ -23,9 +27,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
UserDefaults.standard.setValue(UIApplication.appVersion(), forKey: "Mastodon.appVersion")
|
UserDefaults.standard.setValue(UIApplication.appVersion(), forKey: "Mastodon.appVersion")
|
||||||
UserDefaults.standard.setValue(UIApplication.appBuild(), forKey: "Mastodon.appBundle")
|
UserDefaults.standard.setValue(UIApplication.appBuild(), forKey: "Mastodon.appBundle")
|
||||||
|
|
||||||
|
// Setup notification
|
||||||
UNUserNotificationCenter.current().delegate = self
|
UNUserNotificationCenter.current().delegate = self
|
||||||
application.registerForRemoteNotifications()
|
application.registerForRemoteNotifications()
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
PerformanceMonitor.shared().start()
|
||||||
|
#endif
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
Podfile
1
Podfile
@ -16,6 +16,7 @@ target 'Mastodon' do
|
|||||||
|
|
||||||
# DEBUG
|
# DEBUG
|
||||||
pod 'FLEX', '~> 4.4.0', :configurations => ['Debug']
|
pod 'FLEX', '~> 4.4.0', :configurations => ['Debug']
|
||||||
|
pod 'GDPerformanceView-Swift', '~> 2.1.1', :configurations => ['Debug']
|
||||||
|
|
||||||
target 'MastodonTests' do
|
target 'MastodonTests' do
|
||||||
inherit! :search_paths
|
inherit! :search_paths
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- DateToolsSwift (5.0.0)
|
- DateToolsSwift (5.0.0)
|
||||||
- FLEX (4.4.1)
|
- FLEX (4.4.1)
|
||||||
|
- GDPerformanceView-Swift (2.1.1)
|
||||||
- Kanna (5.2.4)
|
- Kanna (5.2.4)
|
||||||
- Keys (1.0.1)
|
- Keys (1.0.1)
|
||||||
- SwiftGen (6.4.0)
|
- SwiftGen (6.4.0)
|
||||||
@ -9,6 +10,7 @@ PODS:
|
|||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- DateToolsSwift (~> 5.0.0)
|
- DateToolsSwift (~> 5.0.0)
|
||||||
- FLEX (~> 4.4.0)
|
- FLEX (~> 4.4.0)
|
||||||
|
- GDPerformanceView-Swift (~> 2.1.1)
|
||||||
- Kanna (~> 5.2.2)
|
- Kanna (~> 5.2.2)
|
||||||
- Keys (from `Pods/CocoaPodsKeys`)
|
- Keys (from `Pods/CocoaPodsKeys`)
|
||||||
- SwiftGen (~> 6.4.0)
|
- SwiftGen (~> 6.4.0)
|
||||||
@ -18,6 +20,7 @@ SPEC REPOS:
|
|||||||
trunk:
|
trunk:
|
||||||
- DateToolsSwift
|
- DateToolsSwift
|
||||||
- FLEX
|
- FLEX
|
||||||
|
- GDPerformanceView-Swift
|
||||||
- Kanna
|
- Kanna
|
||||||
- SwiftGen
|
- SwiftGen
|
||||||
- "UITextField+Shake"
|
- "UITextField+Shake"
|
||||||
@ -29,11 +32,12 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6
|
DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6
|
||||||
FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab
|
FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab
|
||||||
|
GDPerformanceView-Swift: 22d964fe40b19e3d914dba2586237d064de8fd77
|
||||||
Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f
|
Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f
|
||||||
Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9
|
Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9
|
||||||
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
|
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
|
||||||
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
|
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
|
||||||
|
|
||||||
PODFILE CHECKSUM: 0daad1778e56099e7a7e7ebe3d292d20051840fa
|
PODFILE CHECKSUM: 257c550231fcd1336a29f7835aa331171bb66ebd
|
||||||
|
|
||||||
COCOAPODS: 1.10.1
|
COCOAPODS: 1.10.1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user