fixed: subscription API call

This commit is contained in:
ihugo 2021-04-12 21:42:43 +08:00
parent b2b8b70707
commit 23a06f04ab
9 changed files with 320 additions and 124 deletions

View File

@ -50,6 +50,7 @@ public extension Subscription {
setting.id = property.id
setting.endpoint = property.endpoint
setting.serverKey = property.serverKey
setting.type = property.type
return setting
}

View File

@ -12,6 +12,8 @@ import ActiveLabel
import CoreData
import CoreDataStack
// iTODO: when to ask permission to Use Notifications
class SettingsViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
@ -26,7 +28,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
let follow = L10n.Scene.Settings.Section.Notifications.Trigger.follow
let noOne = L10n.Scene.Settings.Section.Notifications.Trigger.noOne
let menu = UIMenu(
image: UIImage(systemName: "escape"),
image: nil,
identifier: nil,
options: .displayInline,
children: [
@ -173,7 +175,9 @@ class SettingsViewController: UIViewController, NeedsDependency {
}
private func setupTableView() {
viewModel.dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
viewModel.dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { [weak self] (tableView, indexPath, item) -> UITableViewCell? in
guard let self = self else { return nil }
switch item {
case .apperance(let item):
guard let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsAppearanceTableViewCell") as? SettingsAppearanceTableViewCell else {
@ -227,6 +231,10 @@ class SettingsViewController: UIViewController, NeedsDependency {
.store(in: &disposeBag)
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
}
// Mark: - Actions
@objc func doneButtonDidClick() {
dismiss(animated: true, completion: nil)
@ -306,38 +314,39 @@ extension SettingsViewController {
guard let settings = self.viewModel.setting.value else { return }
guard let triggerBy = settings.triggerBy else { return }
var values: [Bool?]?
if let alerts = settings.subscription?.first(where: { (s) -> Bool in
guard let alerts = settings.subscription?.first(where: { (s) -> Bool in
return s.type == settings.triggerBy
})?.alert {
var items = [Bool?]()
items.append(alerts.favourite)
items.append(alerts.follow)
items.append(alerts.reblog)
items.append(alerts.mention)
values = items
})?.alert else {
return
}
guard var alertValues = values else { return }
guard alertValues.count >= 4 else { return }
var alertValues = [Bool?]()
alertValues.append(alerts.favourite?.boolValue)
alertValues.append(alerts.follow?.boolValue)
alertValues.append(alerts.reblog?.boolValue)
alertValues.append(alerts.mention?.boolValue)
// need to update `alerts` to make update API with correct parameter
switch title {
case L10n.Scene.Settings.Section.Notifications.favorites:
alertValues[0] = isOn
alerts.favourite = NSNumber(booleanLiteral: isOn)
case L10n.Scene.Settings.Section.Notifications.follows:
alertValues[1] = isOn
alerts.follow = NSNumber(booleanLiteral: isOn)
case L10n.Scene.Settings.Section.Notifications.boosts:
alertValues[2] = isOn
alerts.reblog = NSNumber(booleanLiteral: isOn)
case L10n.Scene.Settings.Section.Notifications.mentions:
alertValues[3] = isOn
alerts.mention = NSNumber(booleanLiteral: isOn)
default: break
}
self.viewModel.alertUpdate.send((triggerBy: triggerBy, values: alertValues))
self.viewModel.updateSubscriptionSubject.send((triggerBy: triggerBy, values: alertValues))
}
}
extension SettingsViewController: SettingsAppearanceTableViewCellDelegate {
func settingsAppearanceCell(_ view: SettingsAppearanceTableViewCell, didSelect: SettingsItem.AppearanceMode) {
print("[SettingsViewController]: didSelect \(didSelect)")
guard let setting = self.viewModel.setting.value else { return }
context.managedObjectContext.performChanges {

View File

@ -20,6 +20,9 @@ class SettingsViewModel: NSObject, NeedsDependency {
var dataSource: UITableViewDiffableDataSource<SettingsSection, SettingsItem>!
var disposeBag = Set<AnyCancellable>()
var updateDisposeBag = Set<AnyCancellable>()
var createDisposeBag = Set<AnyCancellable>()
let viewDidLoad = PassthroughSubject<Void, Never>()
lazy var fetchResultsController: NSFetchedResultsController<Setting> = {
let fetchRequest = Setting.sortedFetchRequest
@ -42,10 +45,14 @@ class SettingsViewModel: NSObject, NeedsDependency {
}()
let setting = CurrentValueSubject<Setting?, Never>(nil)
/// trigger when
/// - init alerts
/// - change subscription status everytime
let alertUpdate = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
/// create a subscription when:
/// - does not has one
/// - does not find subscription for selected trigger when change trigger
let createSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
/// update a subscription when:
/// - change switch for specified alerts
let updateSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
lazy var notificationDefaultValue: [String: [Bool?]] = {
let followerSwitchItems: [Bool?] = [true, nil, true, true]
@ -77,7 +84,85 @@ class SettingsViewModel: NSObject, NeedsDependency {
}
func transform(input: Input?) -> Output? {
//guard let input = input else { return nil }
typealias SubscriptionResponse = Mastodon.Response.Content<Mastodon.Entity.Subscription>
createSubscriptionSubject
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] (arg) in
let (triggerBy, values) = arg
guard let self = self else {
return
}
guard let activeMastodonAuthenticationBox =
self.context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
guard values.count >= 4 else {
return
}
self.createDisposeBag.removeAll()
typealias Query = Mastodon.API.Notification.CreateSubscriptionQuery
let domain = activeMastodonAuthenticationBox.domain
let query = Query(
endpoint: "http://www.google.com",
p256dh: "BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk=",
auth: "4vQK-SvRAN5eo-8ASlrwA==",
favourite: values[0],
follow: values[1],
reblog: values[2],
mention: values[3],
poll: nil)
self.context.apiService.changeSubscription(
domain: domain,
mastodonAuthenticationBox: activeMastodonAuthenticationBox,
query: query,
triggerBy: triggerBy
)
.sink { (_) in
} receiveValue: { (_) in
}
.store(in: &self.createDisposeBag)
}
.store(in: &disposeBag)
updateSubscriptionSubject
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] (arg) in
let (triggerBy, values) = arg
guard let self = self else {
return
}
guard let activeMastodonAuthenticationBox =
self.context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
guard values.count >= 4 else {
return
}
self.updateDisposeBag.removeAll()
typealias Query = Mastodon.API.Notification.UpdateSubscriptionQuery
let domain = activeMastodonAuthenticationBox.domain
let query = Query(
favourite: values[0],
follow: values[1],
reblog: values[2],
mention: values[3],
poll: nil)
self.context.apiService.updateSubscription(
domain: domain,
mastodonAuthenticationBox: activeMastodonAuthenticationBox,
query: query,
triggerBy: triggerBy
)
.sink { (_) in
} receiveValue: { (_) in
}
.store(in: &self.updateDisposeBag)
}
.store(in: &disposeBag)
// build data for table view
buildDataSource()
@ -85,36 +170,6 @@ class SettingsViewModel: NSObject, NeedsDependency {
// request subsription data for updating or initialization
requestSubscription()
typealias SubscriptionResponse = Mastodon.Response.Content<Mastodon.Entity.Subscription>
alertUpdate
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.flatMap { [weak self] (arg) -> AnyPublisher<SubscriptionResponse, Error> in
let (triggerBy, values) = arg
guard let self = self else {
return Empty<SubscriptionResponse, Error>().eraseToAnyPublisher()
}
guard let activeMastodonAuthenticationBox =
self.context.authenticationService.activeMastodonAuthenticationBox.value else {
return Empty<SubscriptionResponse, Error>().eraseToAnyPublisher()
}
guard values.count >= 4 else {
return Empty<SubscriptionResponse, Error>().eraseToAnyPublisher()
}
typealias Query = Mastodon.API.Notification.CreateSubscriptionQuery
let domain = activeMastodonAuthenticationBox.domain
return self.context.apiService.changeSubscription(
domain: domain,
mastodonAuthenticationBox: activeMastodonAuthenticationBox,
query: Query(favourite: values[0], follow: values[1], reblog: values[2], mention: values[3], poll: nil),
triggerBy: triggerBy)
}
.sink { _ in
} receiveValue: { (subscription) in
}
.store(in: &disposeBag)
do {
try fetchResultsController.performFetch()
setting.value = fetchResultsController.fetchedObjects?.first
@ -141,15 +196,15 @@ class SettingsViewModel: NSObject, NeedsDependency {
return s.type == settings?.triggerBy
})?.alert {
var items = [Bool?]()
items.append(alerts.favourite)
items.append(alerts.follow)
items.append(alerts.reblog)
items.append(alerts.mention)
items.append(alerts.favourite?.boolValue)
items.append(alerts.follow?.boolValue)
items.append(alerts.reblog?.boolValue)
items.append(alerts.mention?.boolValue)
switches = items
} else if let triggerBy = settings?.triggerBy,
let values = self.notificationDefaultValue[triggerBy] {
switches = values
self.alertUpdate.send((triggerBy: triggerBy, values: values))
self.createSubscriptionSubject.send((triggerBy: triggerBy, values: values))
} else {
// fallback a default value
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
@ -178,7 +233,6 @@ class SettingsViewModel: NSObject, NeedsDependency {
L10n.Scene.Settings.Section.BoringZone.privacy]
var boringLinkItems = [SettingsItem]()
for l in boringLinks {
// FIXME: update color in both light and dark mode
let item = SettingsItem.boringZone(item: SettingsItem.Link(title: l, color: .systemBlue))
boringLinkItems.append(item)
}
@ -191,7 +245,6 @@ class SettingsViewModel: NSObject, NeedsDependency {
L10n.Scene.Settings.Section.SpicyZone.signOut]
var spicyLinkItems = [SettingsItem]()
for l in spicyLinks {
// FIXME: update color in both light and dark mode
let item = SettingsItem.boringZone(item: SettingsItem.Link(title: l, color: .systemRed))
spicyLinkItems.append(item)
}
@ -203,15 +256,11 @@ class SettingsViewModel: NSObject, NeedsDependency {
}
private func buildDataSource() {
setting.filter({ $0 != nil }).sink { [weak self] (settings) in
setting.sink { [weak self] (settings) in
guard let self = self else { return }
self.processDataSource(settings)
}
.store(in: &disposeBag)
// init with no subscription for notification
let settings: Setting? = nil
self.processDataSource(settings)
}
private func requestSubscription() {
@ -229,11 +278,22 @@ class SettingsViewModel: NSObject, NeedsDependency {
domain: domain,
mastodonAuthenticationBox: activeMastodonAuthenticationBox)
}
.sink { _ in
.sink { [weak self] competion in
if case .failure(_) = competion {
// create a subscription when doesn't has one
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
if let values = self?.notificationDefaultValue[anyone] {
self?.createSubscriptionSubject.send((triggerBy: anyone, values: values))
}
}
} receiveValue: { (subscription) in
}
.store(in: &disposeBag)
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
}
}
// MARK: - NSFetchedResultsControllerDelegate

View File

@ -86,12 +86,7 @@ class AppearanceView: UIView {
class SettingsAppearanceTableViewCell: UITableViewCell {
weak var delegate: SettingsAppearanceTableViewCellDelegate?
var appearance: SettingsItem.AppearanceMode = .automatic {
didSet {
guard let delegate = self.delegate else { return }
delegate.settingsAppearanceCell(self, didSelect: appearance)
}
}
var appearance: SettingsItem.AppearanceMode = .automatic
lazy var stackView: UIStackView = {
let view = UIStackView()
@ -203,5 +198,8 @@ class SettingsAppearanceTableViewCell: UITableViewCell {
if sender == darkTap {
appearance = .dark
}
guard let delegate = self.delegate else { return }
delegate.settingsAppearanceCell(self, didSelect: appearance)
}
}

View File

@ -62,5 +62,33 @@ extension APIService {
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
func updateSubscription(
domain: String,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
query: Mastodon.API.Notification.UpdateSubscriptionQuery,
triggerBy: String
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization
return Mastodon.API.Notification.updateSubscription(
session: session,
domain: domain,
authorization: authorization,
query: query
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> in
return self.backgroundManagedObjectContext.performChanges {
_ = APIService.CoreData.createOrMergeSubscription(
into: self.backgroundManagedObjectContext,
entity: response.value,
domain: domain,
triggerBy: triggerBy)
}
.setFailureType(to: Error.self)
.map { _ in return response }
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
}

View File

@ -94,11 +94,12 @@ extension APIService.CoreData {
type: triggerBy ?? setting.triggerBy ?? "")
let alertEntity = entity.alerts
let alert = SubscriptionAlerts.Property(
favourite: alertEntity.favourite,
follow: alertEntity.follow,
mention: alertEntity.mention,
poll: alertEntity.poll,
reblog: alertEntity.reblog)
favourite: alertEntity.favouriteNumber,
follow: alertEntity.followNumber,
mention: alertEntity.mentionNumber,
poll: alertEntity.pollNumber,
reblog: alertEntity.reblogNumber
)
if let oldSubscription = oldSubscription {
oldSubscription.updateIfNeed(property: property)
if nil == oldSubscription.alert {
@ -109,9 +110,10 @@ extension APIService.CoreData {
oldSubscription.alert?.updateIfNeed(property: alert)
}
if oldSubscription.alert?.hasChanges == true {
if oldSubscription.alert?.hasChanges == true || oldSubscription.hasChanges {
// don't expand subscription if add existed subscription
setting.mutableSetValue(forKey: #keyPath(Setting.subscription)).add(oldSubscription)
//setting.mutableSetValue(forKey: #keyPath(Setting.subscription)).add(oldSubscription)
oldSubscription.didUpdate(at: Date())
}
return (oldSubscription, false)
} else {

View File

@ -47,9 +47,9 @@ extension Mastodon.API.Notification {
.eraseToAnyPublisher()
}
/// Change types of notifications
/// Subscribe to push notifications
///
/// Using this endpoint to change types of notifications
/// Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.
///
/// - Since: 2.4.0
/// - Version: 3.3.0
@ -80,6 +80,40 @@ extension Mastodon.API.Notification {
}
.eraseToAnyPublisher()
}
/// Change types of notifications
///
/// Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.
///
/// - Since: 2.4.0
/// - Version: 3.3.0
/// # Last Update
/// 2021/4/9
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/notifications/push/)
/// - Parameters:
/// - session: `URLSession`
/// - domain: Mastodon instance domain. e.g. "example.com"
/// - authorization: User token. Could be nil if status is public
/// - Returns: `AnyPublisher` contains `Poll` nested in the response
public static func updateSubscription(
session: URLSession,
domain: String,
authorization: Mastodon.API.OAuth.Authorization?,
query: UpdateSubscriptionQuery
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
let request = Mastodon.API.put(
url: pushEndpointURL(domain: domain),
query: query,
authorization: authorization
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: Mastodon.Entity.Subscription.self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
}
extension Mastodon.API.Notification {
@ -88,27 +122,56 @@ extension Mastodon.API.Notification {
var contentType: String?
var body: Data?
let follow: Bool?
let favourite: Bool?
let reblog: Bool?
let mention: Bool?
let poll: Bool?
// iTODO: missing parameters
// subscription[endpoint]
// subscription[keys][p256dh]
// subscription[keys][auth]
public init(favourite: Bool?,
follow: Bool?,
reblog: Bool?,
mention: Bool?,
poll: Bool?) {
self.follow = follow
self.favourite = favourite
self.reblog = reblog
self.mention = mention
self.poll = poll
public init(
endpoint: String,
p256dh: String,
auth: String,
favourite: Bool?,
follow: Bool?,
reblog: Bool?,
mention: Bool?,
poll: Bool?
) {
queryItems = [URLQueryItem]()
queryItems?.append(URLQueryItem(name: "subscription[endpoint]", value: endpoint))
queryItems?.append(URLQueryItem(name: "subscription[keys][p256dh]", value: p256dh))
queryItems?.append(URLQueryItem(name: "subscription[keys][auth]", value: auth))
if let followValue = follow?.queryItemValue {
let followItem = URLQueryItem(name: "data[alerts][follow]", value: followValue)
queryItems?.append(followItem)
}
if let favouriteValue = favourite?.queryItemValue {
let favouriteItem = URLQueryItem(name: "data[alerts][favourite]", value: favouriteValue)
queryItems?.append(favouriteItem)
}
if let reblogValue = reblog?.queryItemValue {
let reblogItem = URLQueryItem(name: "data[alerts][reblog]", value: reblogValue)
queryItems?.append(reblogItem)
}
if let mentionValue = mention?.queryItemValue {
let mentionItem = URLQueryItem(name: "data[alerts][mention]", value: mentionValue)
queryItems?.append(mentionItem)
}
}
}
public struct UpdateSubscriptionQuery: PutQuery {
var queryItems: [URLQueryItem]?
var contentType: String?
var body: Data?
public init(
favourite: Bool?,
follow: Bool?,
reblog: Bool?,
mention: Bool?,
poll: Bool?
) {
queryItems = [URLQueryItem]()
if let followValue = follow?.queryItemValue {

View File

@ -32,11 +32,46 @@ extension Mastodon.Entity {
}
public struct Alerts: Codable {
public let follow: Bool
public let favourite: Bool
public let reblog: Bool
public let mention: Bool
public let poll: Bool
public let follow: Bool?
public let favourite: Bool?
public let reblog: Bool?
public let mention: Bool?
public let poll: Bool?
public var followNumber: NSNumber? {
guard let value = follow else { return nil }
return NSNumber(booleanLiteral: value)
}
public var favouriteNumber: NSNumber? {
guard let value = favourite else { return nil }
return NSNumber(booleanLiteral: value)
}
public var reblogNumber: NSNumber? {
guard let value = reblog else { return nil }
return NSNumber(booleanLiteral: value)
}
public var mentionNumber: NSNumber? {
guard let value = mention else { return nil }
return NSNumber(booleanLiteral: value)
}
public var pollNumber: NSNumber? {
guard let value = poll else { return nil }
return NSNumber(booleanLiteral: value)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var id = try? container.decode(String.self, forKey: .id)
if nil == id, let numId = try? container.decode(Int.self, forKey: .id) {
id = String(numId)
}
self.id = id ?? ""
endpoint = try container.decode(String.self, forKey: .endpoint)
alerts = try container.decode(Alerts.self, forKey: .alerts)
serverKey = try container.decode(String.self, forKey: .serverKey)
}
}
}

View File

@ -11,11 +11,11 @@ import CoreData
@objc(SubscriptionAlerts)
public final class SubscriptionAlerts: NSManagedObject {
@NSManaged public var follow: Bool
@NSManaged public var favourite: Bool
@NSManaged public var reblog: Bool
@NSManaged public var mention: Bool
@NSManaged public var poll: Bool
@NSManaged public var follow: NSNumber?
@NSManaged public var favourite: NSNumber?
@NSManaged public var reblog: NSNumber?
@NSManaged public var mention: NSNumber?
@NSManaged public var poll: NSNumber?
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
@ -48,35 +48,35 @@ public extension SubscriptionAlerts {
return alerts
}
func update(favourite: Bool) {
func update(favourite: NSNumber?) {
guard self.favourite != favourite else { return }
self.favourite = favourite
didUpdate(at: Date())
}
func update(follow: Bool) {
func update(follow: NSNumber?) {
guard self.follow != follow else { return }
self.follow = follow
didUpdate(at: Date())
}
func update(mention: Bool) {
func update(mention: NSNumber?) {
guard self.mention != mention else { return }
self.mention = mention
didUpdate(at: Date())
}
func update(poll: Bool) {
func update(poll: NSNumber?) {
guard self.poll != poll else { return }
self.poll = poll
didUpdate(at: Date())
}
func update(reblog: Bool) {
func update(reblog: NSNumber?) {
guard self.reblog != reblog else { return }
self.reblog = reblog
@ -86,18 +86,18 @@ public extension SubscriptionAlerts {
public extension SubscriptionAlerts {
struct Property {
public let favourite: Bool
public let follow: Bool
public let mention: Bool
public let poll: Bool
public let reblog: Bool
public let favourite: NSNumber?
public let follow: NSNumber?
public let mention: NSNumber?
public let poll: NSNumber?
public let reblog: NSNumber?
public init(favourite: Bool?, follow: Bool?, mention: Bool?, poll: Bool?, reblog: Bool?) {
self.favourite = favourite ?? true
self.follow = follow ?? true
self.mention = mention ?? true
self.poll = poll ?? true
self.reblog = reblog ?? true
public init(favourite: NSNumber?, follow: NSNumber?, mention: NSNumber?, poll: NSNumber?, reblog: NSNumber?) {
self.favourite = favourite
self.follow = follow
self.mention = mention
self.poll = poll
self.reblog = reblog
}
}