Merge branch 'release/0.6.1'
This commit is contained in:
commit
bd13fa7e43
|
@ -101,6 +101,10 @@ extension MastodonNotification {
|
|||
])
|
||||
}
|
||||
}
|
||||
|
||||
public static func predicate(validTypesRaws types: [String]) -> NSPredicate {
|
||||
return NSPredicate(format: "%K IN %@", #keyPath(MastodonNotification.typeRaw), types)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@
|
|||
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; };
|
||||
5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; };
|
||||
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; };
|
||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; };
|
||||
5DF1054125F886D400D6C0D4 /* VideoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* VideoPlaybackService.swift */; };
|
||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; };
|
||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; };
|
||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */; };
|
||||
|
@ -732,7 +732,7 @@
|
|||
5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = "<group>"; };
|
||||
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = "<group>"; };
|
||||
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = "<group>"; };
|
||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = "<group>"; };
|
||||
5DF1054025F886D400D6C0D4 /* VideoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlaybackService.swift; sourceTree = "<group>"; };
|
||||
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
|
||||
5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerContainerView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1343,7 +1343,7 @@
|
|||
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
||||
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */,
|
||||
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
||||
5DF1054025F886D400D6C0D4 /* VideoPlaybackService.swift */,
|
||||
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
|
||||
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */,
|
||||
2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */,
|
||||
|
@ -2947,7 +2947,7 @@
|
|||
DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */,
|
||||
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
|
||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
||||
5DF1054125F886D400D6C0D4 /* VideoPlaybackService.swift in Sources */,
|
||||
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */,
|
||||
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||
|
@ -3614,7 +3614,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -3622,7 +3622,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
MARKETING_VERSION = 0.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -3641,7 +3641,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -3649,7 +3649,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
MARKETING_VERSION = 0.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -3904,7 +3904,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -3912,7 +3912,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
MARKETING_VERSION = 0.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -3927,7 +3927,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -3935,7 +3935,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
MARKETING_VERSION = 0.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
|
|
@ -30,14 +30,16 @@ extension NotificationSection {
|
|||
guard let dependency = dependency else { return nil }
|
||||
switch notificationItem {
|
||||
case .notification(let objectID, let attribute):
|
||||
|
||||
let notification = managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) else {
|
||||
// filter out invalid type using predicate
|
||||
assertionFailure()
|
||||
return nil
|
||||
return UITableViewCell()
|
||||
}
|
||||
let timeText = notification.createAt.slowedTimeAgoSinceNow
|
||||
|
||||
|
||||
let createAt = notification.createAt
|
||||
let timeText = createAt.slowedTimeAgoSinceNow
|
||||
|
||||
let actionText = type.actionText
|
||||
let actionImageName = type.actionImageName
|
||||
let color = type.color
|
||||
|
@ -57,23 +59,24 @@ extension NotificationSection {
|
|||
requestUserID: requestUserID,
|
||||
statusItemAttribute: attribute
|
||||
)
|
||||
cell.actionImageBackground.backgroundColor = color
|
||||
cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName
|
||||
cell.actionLabel.text = actionText + " · " + timeText
|
||||
timestampUpdatePublisher
|
||||
.sink { _ in
|
||||
let timeText = notification.createAt.slowedTimeAgoSinceNow
|
||||
.sink { [weak cell] _ in
|
||||
guard let cell = cell else { return }
|
||||
let timeText = createAt.slowedTimeAgoSinceNow
|
||||
cell.actionLabel.text = actionText + " · " + timeText
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.actionImageBackground.backgroundColor = color
|
||||
cell.actionLabel.text = actionText + " · " + timeText
|
||||
cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName
|
||||
if let url = notification.account.avatarImageURL() {
|
||||
cell.avatatImageView.af.setImage(
|
||||
cell.avatarImageView.af.setImage(
|
||||
withURL: url,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
)
|
||||
}
|
||||
cell.avatatImageView.gesture().sink { [weak cell] _ in
|
||||
cell.avatarImageView.gesture().sink { [weak cell] _ in
|
||||
cell?.delegate?.userAvatarDidPressed(notification: notification)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
@ -86,8 +89,9 @@ extension NotificationSection {
|
|||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell
|
||||
cell.delegate = delegate
|
||||
timestampUpdatePublisher
|
||||
.sink { _ in
|
||||
let timeText = notification.createAt.slowedTimeAgoSinceNow
|
||||
.sink { [weak cell] _ in
|
||||
guard let cell = cell else { return }
|
||||
let timeText = createAt.slowedTimeAgoSinceNow
|
||||
cell.actionLabel.text = actionText + " · " + timeText
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
protocol EmojiContinaer {
|
||||
protocol EmojiContainer {
|
||||
var emojisData: Data? { get }
|
||||
}
|
||||
|
||||
extension EmojiContinaer {
|
||||
extension EmojiContainer {
|
||||
|
||||
static func encode(emojis: [Mastodon.Entity.Emoji]) -> Data? {
|
||||
return try? JSONEncoder().encode(emojis)
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
protocol FieldContinaer {
|
||||
protocol FieldContainer {
|
||||
var fieldsData: Data? { get }
|
||||
}
|
||||
|
||||
extension FieldContinaer {
|
||||
extension FieldContainer {
|
||||
|
||||
static func encode(fields: [Mastodon.Entity.Field]) -> Data? {
|
||||
return try? JSONEncoder().encode(fields)
|
||||
|
|
|
@ -101,5 +101,5 @@ extension MastodonUser {
|
|||
}
|
||||
}
|
||||
|
||||
extension MastodonUser: EmojiContinaer { }
|
||||
extension MastodonUser: FieldContinaer { }
|
||||
extension MastodonUser: EmojiContainer { }
|
||||
extension MastodonUser: FieldContainer { }
|
||||
|
|
|
@ -88,4 +88,4 @@ extension Status {
|
|||
}
|
||||
}
|
||||
|
||||
extension Status: EmojiContinaer { }
|
||||
extension Status: EmojiContainer { }
|
||||
|
|
|
@ -17,7 +17,6 @@ enum MastodonStatusContent {
|
|||
static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult {
|
||||
let document: String = {
|
||||
var content = content
|
||||
content = content.replacingOccurrences(of: "<br/>", with: "\n")
|
||||
for (shortcode, url) in emojiDict {
|
||||
let emojiNode = "<span class=\"emoji\" href=\"\(url.absoluteString)\">\(shortcode)</span>"
|
||||
let pattern = ":\(shortcode):"
|
||||
|
@ -189,6 +188,14 @@ extension MastodonStatusContent {
|
|||
|
||||
static func parse(document: String) throws -> MastodonStatusContent.Node {
|
||||
let html = try HTML(html: document, encoding: .utf8)
|
||||
|
||||
// add `\r\n` explicit due to Kanna text missing it after convert to text
|
||||
// ref: https://github.com/tid-kijyun/Kanna/issues/150
|
||||
let brNodes = html.css("br").makeIterator()
|
||||
while let brNode = brNodes.next() {
|
||||
brNode.addNextSibling(try! HTML(html: "<span>\r\n</span>", encoding: .utf8).body!)
|
||||
}
|
||||
|
||||
let body = html.body ?? nil
|
||||
let text = body?.text ?? ""
|
||||
let level = 0
|
||||
|
|
|
@ -368,6 +368,7 @@ extension ComposeViewController {
|
|||
guard let self = self else { return }
|
||||
let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle)
|
||||
self.composeToolbarView.visibilityButton.setImage(image, for: .normal)
|
||||
self.composeToolbarView.activeVisibilityType.value = type
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
@ -676,7 +677,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
|||
updateAttributedString attributedString: NSAttributedString,
|
||||
completion: @escaping (NSAttributedString?) -> Void
|
||||
) {
|
||||
// FIXME: needs O(1) update completion to fix profermance issue
|
||||
// FIXME: needs O(1) update completion to fix performance issue
|
||||
DispatchQueue.global().async {
|
||||
let string = attributedString.string
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
|
||||
|
@ -1291,7 +1292,8 @@ extension ComposeViewController {
|
|||
case togglePoll
|
||||
case toggleContentWarning
|
||||
case selectVisibilityPublic
|
||||
case selectVisibilityUnlisted
|
||||
// TODO: remove selectVisibilityUnlisted from codebase
|
||||
// case selectVisibilityUnlisted
|
||||
case selectVisibilityPrivate
|
||||
case selectVisibilityDirect
|
||||
|
||||
|
@ -1305,7 +1307,7 @@ extension ComposeViewController {
|
|||
case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll
|
||||
case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning
|
||||
case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public)
|
||||
case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted)
|
||||
// case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted)
|
||||
case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private)
|
||||
case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct)
|
||||
}
|
||||
|
@ -1322,9 +1324,9 @@ extension ComposeViewController {
|
|||
case .togglePoll: return "p" // + shift + command
|
||||
case .toggleContentWarning: return "c" // + shift + command
|
||||
case .selectVisibilityPublic: return "1" // + command
|
||||
case .selectVisibilityUnlisted: return "2" // + command
|
||||
case .selectVisibilityPrivate: return "3" // + command
|
||||
case .selectVisibilityDirect: return "4" // + command
|
||||
// case .selectVisibilityUnlisted: return "2" // + command
|
||||
case .selectVisibilityPrivate: return "2" // + command
|
||||
case .selectVisibilityDirect: return "3" // + command
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1338,7 +1340,7 @@ extension ComposeViewController {
|
|||
case .togglePoll: return [.shift, .command]
|
||||
case .toggleContentWarning: return [.shift, .command]
|
||||
case .selectVisibilityPublic: return [.command]
|
||||
case .selectVisibilityUnlisted: return [.command]
|
||||
// case .selectVisibilityUnlisted: return [.command]
|
||||
case .selectVisibilityPrivate: return [.command]
|
||||
case .selectVisibilityDirect: return [.command]
|
||||
}
|
||||
|
@ -1390,8 +1392,8 @@ extension ComposeViewController {
|
|||
composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside)
|
||||
case .selectVisibilityPublic:
|
||||
viewModel.selectedStatusVisibility.value = .public
|
||||
case .selectVisibilityUnlisted:
|
||||
viewModel.selectedStatusVisibility.value = .unlisted
|
||||
// case .selectVisibilityUnlisted:
|
||||
// viewModel.selectedStatusVisibility.value = .unlisted
|
||||
case .selectVisibilityPrivate:
|
||||
viewModel.selectedStatusVisibility.value = .private
|
||||
case .selectVisibilityDirect:
|
||||
|
|
|
@ -92,7 +92,8 @@ final class ComposeViewModel {
|
|||
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
||||
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||
// end init
|
||||
if case let .reply(repliedToStatusObjectID) = composeKind {
|
||||
switch composeKind {
|
||||
case .reply(let repliedToStatusObjectID):
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = context.managedObjectContext.object(with: repliedToStatusObjectID) as? Status else { return }
|
||||
let composeAuthor: MastodonUser? = {
|
||||
|
@ -124,14 +125,13 @@ final class ComposeViewModel {
|
|||
self.preInsertedContent = preInsertedContent
|
||||
self.composeStatusAttribute.composeContent.value = preInsertedContent
|
||||
}
|
||||
|
||||
} else if case let .hashtag(text) = composeKind {
|
||||
let initialComposeContent = "#" + text
|
||||
case .hashtag(let hashtag):
|
||||
let initialComposeContent = "#" + hashtag
|
||||
UITextChecker.learnWord(initialComposeContent)
|
||||
let preInsertedContent = initialComposeContent + " "
|
||||
self.preInsertedContent = preInsertedContent
|
||||
self.composeStatusAttribute.composeContent.value = preInsertedContent
|
||||
} else if case let .mention(mastodonUserObjectID) = composeKind {
|
||||
case .mention(let mastodonUserObjectID):
|
||||
context.managedObjectContext.performAndWait {
|
||||
let mastodonUser = context.managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||
let initialComposeContent = "@" + mastodonUser.acct
|
||||
|
@ -140,7 +140,7 @@ final class ComposeViewModel {
|
|||
self.preInsertedContent = preInsertedContent
|
||||
self.composeStatusAttribute.composeContent.value = preInsertedContent
|
||||
}
|
||||
} else {
|
||||
case .post:
|
||||
self.preInsertedContent = nil
|
||||
}
|
||||
|
||||
|
@ -263,7 +263,7 @@ final class ComposeViewModel {
|
|||
}
|
||||
// if preInsertedContent plus a space is equal to the content, simply dismiss the modal
|
||||
if let preInsertedContent = self?.preInsertedContent {
|
||||
return content == (preInsertedContent + " ")
|
||||
return content == preInsertedContent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -374,11 +374,6 @@ final class ComposeViewModel {
|
|||
self.isPollToolbarButtonEnabled.value = !shouldPollDisable
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
if let preInsertedContent = preInsertedContent {
|
||||
// add a space after the injected text
|
||||
composeStatusAttribute.composeContent.send(preInsertedContent + " ")
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
|
||||
protocol ComposeToolbarViewDelegate: AnyObject {
|
||||
|
@ -19,6 +20,8 @@ protocol ComposeToolbarViewDelegate: AnyObject {
|
|||
|
||||
final class ComposeToolbarView: UIView {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
static let toolbarButtonSize: CGSize = CGSize(width: 44, height: 44)
|
||||
static let toolbarHeight: CGFloat = 44
|
||||
|
||||
|
@ -76,6 +79,8 @@ final class ComposeToolbarView: UIView {
|
|||
return label
|
||||
}()
|
||||
|
||||
let activeVisibilityType = CurrentValueSubject<VisibilitySelectionType, Never>(.public)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
|
@ -142,6 +147,15 @@ extension ComposeToolbarView {
|
|||
visibilityButton.showsMenuAsPrimaryAction = true
|
||||
|
||||
updateToolbarButtonUserInterfaceStyle()
|
||||
|
||||
// update menu when selected visibility type changed
|
||||
activeVisibilityType
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] type in
|
||||
guard let self = self else { return }
|
||||
self.visibilityButton.menu = self.createVisibilityContextMenu(interfaceStyle: self.traitCollection.userInterfaceStyle)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
|
@ -161,14 +175,15 @@ extension ComposeToolbarView {
|
|||
|
||||
enum VisibilitySelectionType: String, CaseIterable {
|
||||
case `public`
|
||||
case unlisted
|
||||
// TODO: remove unlisted option from codebase
|
||||
// case unlisted
|
||||
case `private`
|
||||
case direct
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .public: return L10n.Scene.Compose.Visibility.public
|
||||
case .unlisted: return L10n.Scene.Compose.Visibility.unlisted
|
||||
// case .unlisted: return L10n.Scene.Compose.Visibility.unlisted
|
||||
case .private: return L10n.Scene.Compose.Visibility.private
|
||||
case .direct: return L10n.Scene.Compose.Visibility.direct
|
||||
}
|
||||
|
@ -181,7 +196,7 @@ extension ComposeToolbarView {
|
|||
case .light: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
|
||||
default: return UIImage(systemName: "person.3.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
|
||||
}
|
||||
case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
|
||||
// case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
|
||||
case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
|
||||
case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .regular))!
|
||||
}
|
||||
|
@ -190,7 +205,7 @@ extension ComposeToolbarView {
|
|||
func imageNameForTimeline() -> String {
|
||||
switch self {
|
||||
case .public: return "person.3"
|
||||
case .unlisted: return "eye.slash"
|
||||
// case .unlisted: return "eye.slash"
|
||||
case .private: return "person.crop.circle.badge.plus"
|
||||
case .direct: return "at"
|
||||
}
|
||||
|
@ -199,7 +214,7 @@ extension ComposeToolbarView {
|
|||
var visibility: Mastodon.Entity.Status.Visibility {
|
||||
switch self {
|
||||
case .public: return .public
|
||||
case .unlisted: return .unlisted
|
||||
// case .unlisted: return .unlisted
|
||||
case .private: return .private
|
||||
case .direct: return .direct
|
||||
}
|
||||
|
@ -268,7 +283,8 @@ extension ComposeToolbarView {
|
|||
|
||||
private func createVisibilityContextMenu(interfaceStyle: UIUserInterfaceStyle) -> UIMenu {
|
||||
let children: [UIMenuElement] = VisibilitySelectionType.allCases.map { type in
|
||||
UIAction(title: type.title, image: type.image(interfaceStyle: interfaceStyle), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
|
||||
let state: UIMenuElement.State = activeVisibilityType.value == type ? .on : .off
|
||||
return UIAction(title: type.title, image: type.image(interfaceStyle: interfaceStyle), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: visibilitySelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, type.rawValue)
|
||||
self.delegate?.composeToolbarView(self, visibilityButtonDidPressed: self.visibilityButton, visibilitySelectionType: type)
|
||||
|
|
|
@ -9,6 +9,7 @@ import CoreData
|
|||
import CoreDataStack
|
||||
import os.log
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
|
||||
extension NotificationViewModel {
|
||||
func setupDiffableDataSource(
|
||||
|
@ -16,7 +17,7 @@ extension NotificationViewModel {
|
|||
delegate: NotificationTableViewCellDelegate,
|
||||
dependency: NeedsDependency
|
||||
) {
|
||||
let timestampUpdatePublisher = Timer.publish(every: 30.0, on: .main, in: .common)
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -44,7 +45,14 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate {
|
|||
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
let predicate = fetchedResultsController.fetchRequest.predicate
|
||||
let predicate: NSPredicate = {
|
||||
let notificationTypePredicate = MastodonNotification.predicate(
|
||||
validTypesRaws: Mastodon.Entity.Notification.NotificationType.knownCases.map { $0.rawValue }
|
||||
)
|
||||
return fetchedResultsController.fetchRequest.predicate.flatMap {
|
||||
NSCompoundPredicate(andPredicateWithSubpredicates: [$0, notificationTypePredicate])
|
||||
} ?? notificationTypePredicate
|
||||
}()
|
||||
let parentManagedObjectContext = fetchedResultsController.managedObjectContext
|
||||
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||
managedObjectContext.parent = parentManagedObjectContext
|
||||
|
@ -73,19 +81,6 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate {
|
|||
newSnapshot.appendSections([.main])
|
||||
let items: [NotificationItem] = notifications.map { notification in
|
||||
let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute()
|
||||
|
||||
// let attribute: Item.StatusAttribute = {
|
||||
// if let attribute = oldSnapshotAttributeDict[notification.objectID] {
|
||||
// return attribute
|
||||
// } else if let status = notification.status {
|
||||
// let attribute = Item.StatusAttribute()
|
||||
// let isSensitive = status.sensitive || !(status.spoilerText ?? "").isEmpty
|
||||
// attribute.isRevealing.value = !isSensitive
|
||||
// return attribute
|
||||
// } else {
|
||||
// return Item.StatusAttribute()
|
||||
// }
|
||||
// }()
|
||||
return NotificationItem.notification(objectID: notification.objectID, attribute: attribute)
|
||||
}
|
||||
newSnapshot.appendItems(items, toSection: .main)
|
||||
|
|
|
@ -17,7 +17,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
var pollCountdownSubscription: AnyCancellable?
|
||||
var delegate: NotificationTableViewCellDelegate?
|
||||
|
||||
let avatatImageView: UIImageView = {
|
||||
let avatarImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.layer.cornerRadius = 4
|
||||
imageView.layer.cornerCurve = .continuous
|
||||
|
@ -86,7 +86,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
avatatImageView.af.cancelImageRequest()
|
||||
avatarImageView.af.cancelImageRequest()
|
||||
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
|
||||
statusView.pollTableView.dataSource = nil
|
||||
statusView.playerContainerView.reset()
|
||||
|
@ -142,13 +142,13 @@ extension NotificationStatusTableViewCell {
|
|||
avatarContainer.widthAnchor.constraint(equalToConstant: 47).priority(.required - 1)
|
||||
])
|
||||
|
||||
avatarContainer.addSubview(avatatImageView)
|
||||
avatatImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addSubview(avatarImageView)
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
avatatImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||
avatatImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||
avatatImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
|
||||
avatatImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor)
|
||||
avatarImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||
avatarImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||
avatarImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
|
||||
avatarImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor)
|
||||
])
|
||||
|
||||
avatarContainer.addSubview(actionImageBackground)
|
||||
|
|
|
@ -13,7 +13,7 @@ extension APIService {
|
|||
|
||||
func uploadMedia(
|
||||
domain: String,
|
||||
query: Mastodon.API.Media.UploadMeidaQuery,
|
||||
query: Mastodon.API.Media.UploadMediaQuery,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
|
|
@ -27,7 +27,7 @@ extension APIService {
|
|||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
let requestMastodonUserID = authorizationBox.userID
|
||||
let query = Mastodon.API.Account.AccountStatuseseQuery(
|
||||
let query = Mastodon.API.Account.AccountStatusesQuery(
|
||||
maxID: maxID,
|
||||
sinceID: sinceID,
|
||||
excludeReplies: excludeReplies,
|
||||
|
|
|
@ -56,7 +56,7 @@ extension MastodonAttachmentService.UploadState {
|
|||
guard let file = service.file.value else { return }
|
||||
|
||||
let description = service.description.value
|
||||
let query = Mastodon.API.Media.UploadMeidaQuery(
|
||||
let query = Mastodon.API.Media.UploadMediaQuery(
|
||||
file: file,
|
||||
thumbnail: nil,
|
||||
description: description,
|
||||
|
|
|
@ -71,14 +71,14 @@ extension Mastodon.API.Account {
|
|||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - query: `AccountStatuseseQuery` with query parameters
|
||||
/// - query: `AccountStatusesQuery` with query parameters
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||
public static func statuses(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
query: AccountStatuseseQuery,
|
||||
query: AccountStatusesQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
|
@ -94,7 +94,7 @@ extension Mastodon.API.Account {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public struct AccountStatuseseQuery: GetQuery {
|
||||
public struct AccountStatusesQuery: GetQuery {
|
||||
public let maxID: Mastodon.Entity.Status.ID?
|
||||
public let sinceID: Mastodon.Entity.Status.ID?
|
||||
public let excludeReplies: Bool? // undocumented
|
||||
|
|
|
@ -33,7 +33,7 @@ extension Mastodon.API.Media {
|
|||
public static func uploadMedia(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: UploadMeidaQuery,
|
||||
query: UploadMediaQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization?
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||
var request = Mastodon.API.post(
|
||||
|
@ -56,7 +56,7 @@ extension Mastodon.API.Media {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public struct UploadMeidaQuery: PostQuery, PutQuery {
|
||||
public struct UploadMediaQuery: PostQuery, PutQuery {
|
||||
public let file: Mastodon.Query.MediaAttachment?
|
||||
public let thumbnail: Mastodon.Query.MediaAttachment?
|
||||
public let description: String?
|
||||
|
|
|
@ -49,6 +49,18 @@ extension Mastodon.Entity.Notification {
|
|||
|
||||
case _other(String)
|
||||
|
||||
public static var knownCases: [NotificationType] {
|
||||
return [
|
||||
.follow,
|
||||
.followRequest,
|
||||
.mention,
|
||||
.reblog,
|
||||
.favourite,
|
||||
.poll,
|
||||
.status
|
||||
]
|
||||
}
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "follow": self = .follow
|
||||
|
|
Loading…
Reference in New Issue