feature: notification API and CoreData
This commit is contained in:
parent
239b6bac4f
commit
773bfb6dd2
|
@ -65,6 +65,21 @@
|
||||||
<attribute name="username" attributeType="String"/>
|
<attribute name="username" attributeType="String"/>
|
||||||
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
|
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="MastodonNotification" representedClassName=".MastodonNotification" syncable="YES">
|
||||||
|
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="id" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="type" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="id"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
<entity name="MastodonUser" representedClassName=".MastodonUser" syncable="YES">
|
<entity name="MastodonUser" representedClassName=".MastodonUser" syncable="YES">
|
||||||
<attribute name="acct" attributeType="String"/>
|
<attribute name="acct" attributeType="String"/>
|
||||||
<attribute name="avatar" attributeType="String"/>
|
<attribute name="avatar" attributeType="String"/>
|
||||||
|
@ -208,6 +223,7 @@
|
||||||
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
|
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
|
||||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||||
|
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="149"/>
|
||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
||||||
<element name="Mention" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="Mention" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||||
|
@ -217,4 +233,4 @@
|
||||||
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
||||||
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -0,0 +1,110 @@
|
||||||
|
//
|
||||||
|
// MastodonNotification.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
public final class MastodonNotification: NSManagedObject {
|
||||||
|
public typealias ID = UUID
|
||||||
|
@NSManaged public private(set) var identifier: ID
|
||||||
|
@NSManaged public private(set) var id: String
|
||||||
|
@NSManaged public private(set) var domain: String
|
||||||
|
@NSManaged public private(set) var createAt: Date
|
||||||
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
@NSManaged public private(set) var type: String
|
||||||
|
@NSManaged public private(set) var account: MastodonUser
|
||||||
|
@NSManaged public private(set) var status: Status?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonNotification {
|
||||||
|
public override func awakeFromInsert() {
|
||||||
|
super.awakeFromInsert()
|
||||||
|
setPrimitiveValue(UUID(), forKey: #keyPath(MastodonNotification.identifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func willSave() {
|
||||||
|
super.willSave()
|
||||||
|
setPrimitiveValue(Date(), forKey: #keyPath(MastodonNotification.updatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension MastodonNotification {
|
||||||
|
@discardableResult
|
||||||
|
static func insert(
|
||||||
|
into context: NSManagedObjectContext,
|
||||||
|
domain: String,
|
||||||
|
property: Property
|
||||||
|
) -> MastodonNotification {
|
||||||
|
let notification: MastodonNotification = context.insertObject()
|
||||||
|
notification.id = property.id
|
||||||
|
notification.createAt = property.createdAt
|
||||||
|
notification.updatedAt = property.createdAt
|
||||||
|
notification.type = property.type
|
||||||
|
notification.account = property.account
|
||||||
|
notification.status = property.status
|
||||||
|
notification.domain = domain
|
||||||
|
return notification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension MastodonNotification {
|
||||||
|
struct Property {
|
||||||
|
public init(id: String,
|
||||||
|
type: String,
|
||||||
|
account: MastodonUser,
|
||||||
|
status: Status?,
|
||||||
|
createdAt: Date) {
|
||||||
|
self.id = id
|
||||||
|
self.type = type
|
||||||
|
self.account = account
|
||||||
|
self.status = status
|
||||||
|
self.createdAt = createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
public let id: String
|
||||||
|
public let type: String
|
||||||
|
public let account: MastodonUser
|
||||||
|
public let status: Status?
|
||||||
|
public let createdAt: Date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonNotification {
|
||||||
|
static func predicate(domain: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.domain), domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func predicate(type: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.type), type)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func predicate(domain: String, type: String) -> NSPredicate {
|
||||||
|
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
MastodonNotification.predicate(domain: domain),
|
||||||
|
MastodonNotification.predicate(type: type)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
static func predicate(types: [String]) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K IN %@", #keyPath(MastodonNotification.type), types)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func predicate(domain: String, types: [String]) -> NSPredicate {
|
||||||
|
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
MastodonNotification.predicate(domain: domain),
|
||||||
|
MastodonNotification.predicate(types: types)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonNotification: Managed {
|
||||||
|
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||||
|
return [NSSortDescriptor(keyPath: \MastodonNotification.updatedAt, ascending: false)]
|
||||||
|
}
|
||||||
|
}
|
|
@ -321,6 +321,19 @@
|
||||||
},
|
},
|
||||||
"favorite": {
|
"favorite": {
|
||||||
"title": "Your Favorites"
|
"title": "Your Favorites"
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"title": {
|
||||||
|
"Everything": "Everything",
|
||||||
|
"Mentions": "Mentions"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"follow": "followed you",
|
||||||
|
"favourite": "favorited your post",
|
||||||
|
"reblog": "rebloged your post",
|
||||||
|
"poll": "Your poll has ended",
|
||||||
|
"mention": "mentioned you"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@
|
||||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
||||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||||
|
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */; };
|
||||||
|
2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */; };
|
||||||
2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A1C261D839600B44727 /* SearchHistory.swift */; };
|
2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A1C261D839600B44727 /* SearchHistory.swift */; };
|
||||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
|
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
|
||||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; };
|
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; };
|
||||||
|
@ -49,6 +51,8 @@
|
||||||
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; };
|
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; };
|
||||||
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; };
|
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; };
|
||||||
2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */; };
|
2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */; };
|
||||||
|
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35237926256D920031AF25 /* NotificationSection.swift */; };
|
||||||
|
2D35238126256F690031AF25 /* NotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35238026256F690031AF25 /* NotificationTableViewCell.swift */; };
|
||||||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; };
|
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; };
|
||||||
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; };
|
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; };
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; };
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; };
|
||||||
|
@ -76,6 +80,9 @@
|
||||||
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; };
|
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; };
|
||||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; };
|
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; };
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; };
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; };
|
||||||
|
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D607AD726242FC500B70763 /* NotificationViewModel.swift */; };
|
||||||
|
2D6125472625436B00299647 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6125462625436B00299647 /* Notification.swift */; };
|
||||||
|
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; };
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; };
|
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; };
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
||||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
||||||
|
@ -91,6 +98,7 @@
|
||||||
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; };
|
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; };
|
||||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; };
|
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; };
|
||||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
|
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
|
||||||
|
2D7867192625B77500211898 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7867182625B77500211898 /* NotificationItem.swift */; };
|
||||||
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */; };
|
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */; };
|
||||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
|
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
|
||||||
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
|
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
|
||||||
|
@ -409,6 +417,8 @@
|
||||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
||||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
||||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||||
|
2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+diffable.swift"; sourceTree = "<group>"; };
|
||||||
|
2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+LoadLatestState.swift"; sourceTree = "<group>"; };
|
||||||
2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
|
2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
|
||||||
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||||
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||||
|
@ -428,6 +438,8 @@
|
||||||
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = "<group>"; };
|
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = "<group>"; };
|
||||||
2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = "<group>"; };
|
2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = "<group>"; };
|
||||||
2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = "<group>"; };
|
2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
2D35237926256D920031AF25 /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = "<group>"; };
|
||||||
|
2D35238026256F690031AF25 /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = "<group>"; };
|
2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = "<group>"; };
|
||||||
2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = "<group>"; };
|
2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = "<group>"; };
|
||||||
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = "<group>"; };
|
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
@ -453,6 +465,9 @@
|
||||||
2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = "<group>"; };
|
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = "<group>"; };
|
||||||
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = "<group>"; };
|
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = "<group>"; };
|
||||||
|
2D607AD726242FC500B70763 /* NotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
2D6125462625436B00299647 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
|
||||||
|
2D61254C262547C200299647 /* APIService+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Notification.swift"; sourceTree = "<group>"; };
|
||||||
2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Status.swift"; sourceTree = "<group>"; };
|
2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Status.swift"; sourceTree = "<group>"; };
|
||||||
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
||||||
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = "<group>"; };
|
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -467,6 +482,7 @@
|
||||||
2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = "<group>"; };
|
2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = "<group>"; };
|
||||||
2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
||||||
|
2D7867182625B77500211898 /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = "<group>"; };
|
||||||
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Tag.swift"; sourceTree = "<group>"; };
|
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Tag.swift"; sourceTree = "<group>"; };
|
||||||
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = "<group>"; };
|
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = "<group>"; };
|
||||||
2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
||||||
|
@ -878,6 +894,14 @@
|
||||||
path = CollectionViewCell;
|
path = CollectionViewCell;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
2D35237F26256F470031AF25 /* TableViewCell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2D35238026256F690031AF25 /* NotificationTableViewCell.swift */,
|
||||||
|
);
|
||||||
|
path = TableViewCell;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
2D364F7025E66D5B00204FDC /* ResendEmail */ = {
|
2D364F7025E66D5B00204FDC /* ResendEmail */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1032,6 +1056,7 @@
|
||||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||||
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
|
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
|
||||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
||||||
|
2D35237926256D920031AF25 /* NotificationSection.swift */,
|
||||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||||
|
@ -1083,6 +1108,7 @@
|
||||||
children = (
|
children = (
|
||||||
2D7631B225C159F700929FB9 /* Item.swift */,
|
2D7631B225C159F700929FB9 /* Item.swift */,
|
||||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
||||||
|
2D7867182625B77500211898 /* NotificationItem.swift */,
|
||||||
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
||||||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||||
|
@ -1312,6 +1338,7 @@
|
||||||
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
|
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
|
||||||
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
||||||
DB9A488326034BD7008B817C /* APIService+Status.swift */,
|
DB9A488326034BD7008B817C /* APIService+Status.swift */,
|
||||||
|
2D61254C262547C200299647 /* APIService+Notification.swift */,
|
||||||
DB9A488F26035963008B817C /* APIService+Media.swift */,
|
DB9A488F26035963008B817C /* APIService+Media.swift */,
|
||||||
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */,
|
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */,
|
||||||
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
||||||
|
@ -1487,6 +1514,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB89BA2625C110B4008580ED /* Status.swift */,
|
DB89BA2625C110B4008580ED /* Status.swift */,
|
||||||
|
2D6125462625436B00299647 /* Notification.swift */,
|
||||||
2D0B7A1C261D839600B44727 /* SearchHistory.swift */,
|
2D0B7A1C261D839600B44727 /* SearchHistory.swift */,
|
||||||
DB8AF52425C131D1002E6C99 /* MastodonUser.swift */,
|
DB8AF52425C131D1002E6C99 /* MastodonUser.swift */,
|
||||||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */,
|
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */,
|
||||||
|
@ -1642,6 +1670,10 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
|
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
|
||||||
|
2D607AD726242FC500B70763 /* NotificationViewModel.swift */,
|
||||||
|
2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */,
|
||||||
|
2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */,
|
||||||
|
2D35237F26256F470031AF25 /* TableViewCell */,
|
||||||
);
|
);
|
||||||
path = Notification;
|
path = Notification;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2210,6 +2242,7 @@
|
||||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
|
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
|
||||||
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
|
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
|
||||||
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */,
|
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */,
|
||||||
|
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||||
|
@ -2244,6 +2277,7 @@
|
||||||
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
||||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||||
|
2D35238126256F690031AF25 /* NotificationTableViewCell.swift in Sources */,
|
||||||
DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */,
|
DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */,
|
||||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||||
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
||||||
|
@ -2263,6 +2297,7 @@
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
||||||
DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */,
|
DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */,
|
||||||
|
2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */,
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
|
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||||
|
@ -2321,6 +2356,7 @@
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
|
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
|
||||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||||
|
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||||
|
@ -2329,6 +2365,7 @@
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||||
|
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */,
|
||||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
||||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
||||||
|
@ -2358,6 +2395,7 @@
|
||||||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
||||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
||||||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */,
|
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */,
|
||||||
|
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
|
||||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */,
|
DB084B5725CBC56C00F898ED /* Status.swift in Sources */,
|
||||||
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
|
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
|
||||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
||||||
|
@ -2370,6 +2408,7 @@
|
||||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */,
|
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */,
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
|
||||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||||
|
2D7867192625B77500211898 /* NotificationItem.swift in Sources */,
|
||||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||||
2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */,
|
2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */,
|
||||||
|
@ -2472,6 +2511,7 @@
|
||||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */,
|
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */,
|
||||||
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */,
|
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */,
|
||||||
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
||||||
|
2D6125472625436B00299647 /* Notification.swift in Sources */,
|
||||||
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
||||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
||||||
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */,
|
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
//
|
||||||
|
// NotificationItem.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
enum NotificationItem {
|
||||||
|
|
||||||
|
case notification(ObjectID: NSManagedObjectID)
|
||||||
|
|
||||||
|
case bottomLoader
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationItem: Equatable {
|
||||||
|
static func == (lhs: NotificationItem, rhs: NotificationItem) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.bottomLoader, .bottomLoader):
|
||||||
|
return true
|
||||||
|
case (.notification(let idLeft),.notification(let idRight)):
|
||||||
|
return idLeft == idRight
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationItem: Hashable {
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case .notification(let id):
|
||||||
|
hasher.combine(id)
|
||||||
|
case .bottomLoader:
|
||||||
|
hasher.combine(String(describing: NotificationItem.bottomLoader.self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
//
|
||||||
|
// NotificationSection.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
enum NotificationSection: Equatable, Hashable {
|
||||||
|
case main
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationSection {
|
||||||
|
static func tableViewDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||||
|
managedObjectContext: NSManagedObjectContext
|
||||||
|
) -> UITableViewDiffableDataSource<NotificationSection, NotificationItem> {
|
||||||
|
|
||||||
|
return UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, notificationItem) -> UITableViewCell? in
|
||||||
|
switch notificationItem {
|
||||||
|
case .notification(let objectID):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell
|
||||||
|
let notification = managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||||
|
let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.type)
|
||||||
|
|
||||||
|
var actionText: String
|
||||||
|
var actionImageName: String
|
||||||
|
switch type {
|
||||||
|
case .follow:
|
||||||
|
actionText = L10n.Scene.Notification.Action.follow
|
||||||
|
actionImageName = "person.crop.circle.badge.checkmark"
|
||||||
|
case .favourite:
|
||||||
|
actionText = L10n.Scene.Notification.Action.favourite
|
||||||
|
actionImageName = "star.fill"
|
||||||
|
case .reblog:
|
||||||
|
actionText = L10n.Scene.Notification.Action.reblog
|
||||||
|
actionImageName = "arrow.2.squarepath"
|
||||||
|
case .mention:
|
||||||
|
actionText = L10n.Scene.Notification.Action.mention
|
||||||
|
actionImageName = "at"
|
||||||
|
case .poll:
|
||||||
|
actionText = L10n.Scene.Notification.Action.poll
|
||||||
|
actionImageName = "list.bullet"
|
||||||
|
default:
|
||||||
|
actionText = ""
|
||||||
|
actionImageName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
timestampUpdatePublisher
|
||||||
|
.sink { _ in
|
||||||
|
let timeText = notification.createAt.shortTimeAgoSinceNow
|
||||||
|
cell.actionLabel.text = actionText + " · " + timeText
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
cell.nameLabel.text = notification.account.displayName
|
||||||
|
|
||||||
|
if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) {
|
||||||
|
cell.actionImageView.image = actionImage
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
case .bottomLoader:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader
|
||||||
|
cell.startAnimating()
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -339,6 +339,26 @@ internal enum L10n {
|
||||||
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
|
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
internal enum Notification {
|
||||||
|
internal enum Action {
|
||||||
|
/// favorited your toot
|
||||||
|
internal static let favourite = L10n.tr("Localizable", "Scene.Notification.Action.Favourite")
|
||||||
|
/// followed you
|
||||||
|
internal static let follow = L10n.tr("Localizable", "Scene.Notification.Action.Follow")
|
||||||
|
/// mentioned you
|
||||||
|
internal static let mention = L10n.tr("Localizable", "Scene.Notification.Action.Mention")
|
||||||
|
/// Your poll has ended
|
||||||
|
internal static let poll = L10n.tr("Localizable", "Scene.Notification.Action.Poll")
|
||||||
|
/// boosted your toot
|
||||||
|
internal static let reblog = L10n.tr("Localizable", "Scene.Notification.Action.Reblog")
|
||||||
|
}
|
||||||
|
internal enum Title {
|
||||||
|
/// Everything
|
||||||
|
internal static let everything = L10n.tr("Localizable", "Scene.Notification.Title.Everything")
|
||||||
|
/// Mentions
|
||||||
|
internal static let mentions = L10n.tr("Localizable", "Scene.Notification.Title.Mentions")
|
||||||
|
}
|
||||||
|
}
|
||||||
internal enum Profile {
|
internal enum Profile {
|
||||||
/// %@ posts
|
/// %@ posts
|
||||||
internal static func subtitle(_ p1: Any) -> String {
|
internal static func subtitle(_ p1: Any) -> String {
|
||||||
|
|
|
@ -114,6 +114,13 @@ tap the link to confirm your account.";
|
||||||
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
|
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
|
||||||
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
|
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
|
||||||
"Scene.HomeTimeline.Title" = "Home";
|
"Scene.HomeTimeline.Title" = "Home";
|
||||||
|
"Scene.Notification.Action.Favourite" = "favorited your toot";
|
||||||
|
"Scene.Notification.Action.Follow" = "followed you";
|
||||||
|
"Scene.Notification.Action.Mention" = "mentioned you";
|
||||||
|
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
||||||
|
"Scene.Notification.Action.Reblog" = "boosted your toot";
|
||||||
|
"Scene.Notification.Title.Everything" = "Everything";
|
||||||
|
"Scene.Notification.Title.Mentions" = "Mentions";
|
||||||
"Scene.Profile.Dashboard.Followers" = "followers";
|
"Scene.Profile.Dashboard.Followers" = "followers";
|
||||||
"Scene.Profile.Dashboard.Following" = "following";
|
"Scene.Profile.Dashboard.Following" = "following";
|
||||||
"Scene.Profile.Dashboard.Posts" = "posts";
|
"Scene.Profile.Dashboard.Posts" = "posts";
|
||||||
|
|
|
@ -2,23 +2,147 @@
|
||||||
// NotificationViewController.swift
|
// NotificationViewController.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021-2-23.
|
// Created by sxiaojian on 2021/4/12.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import OSLog
|
||||||
|
|
||||||
final class NotificationViewController: UIViewController, NeedsDependency {
|
final class NotificationViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
private(set) lazy var viewModel = NotificationViewModel(context: context, coordinator: coordinator)
|
||||||
|
|
||||||
|
let segmentControl: UISegmentedControl = {
|
||||||
|
let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything,L10n.Scene.Notification.Title.mentions])
|
||||||
|
control.selectedSegmentIndex = 0
|
||||||
|
control.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .touchUpInside)
|
||||||
|
return control
|
||||||
|
}()
|
||||||
|
|
||||||
|
let tableView: UITableView = {
|
||||||
|
let tableView = ControlContainableTableView()
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self))
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationViewController {
|
extension NotificationViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
view.backgroundColor = Asset.Colors.Background.searchResult.color
|
||||||
|
navigationItem.titleView = segmentControl
|
||||||
|
view.addSubview(tableView)
|
||||||
|
tableView.constrain([
|
||||||
|
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
refreshControl.addTarget(self, action: #selector(NotificationViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
|
||||||
|
tableView.delegate = self
|
||||||
|
viewModel.tableView = tableView
|
||||||
|
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
||||||
|
viewModel.setupDiffableDataSource(for: tableView)
|
||||||
|
|
||||||
|
// bind refresh control
|
||||||
|
viewModel.isFetchingLatestNotification
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isFetching in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if !isFetching {
|
||||||
|
UIView.animate(withDuration: 0.5) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
// needs trigger manually after onboarding dismiss
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 {
|
||||||
|
self.viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
|
super.viewWillTransition(to: size, with: coordinator)
|
||||||
|
|
||||||
|
coordinator.animate { _ in
|
||||||
|
// do nothing
|
||||||
|
} completion: { _ in
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NotificationViewController {
|
||||||
|
@objc private func segmentedControlValueChanged(_ sender: UISegmentedControl) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, sender.selectedSegmentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
guard viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) else {
|
||||||
|
sender.endRefreshing()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
extension NotificationViewController: UITableViewDelegate {
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
return 68
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||||
|
extension NotificationViewController: ContentOffsetAdjustableTimelineViewControllerDelegate {
|
||||||
|
func navigationBar() -> UINavigationBar? {
|
||||||
|
return navigationController?.navigationBar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//// MARK: - UIScrollViewDelegate
|
||||||
|
//extension NotificationViewController {
|
||||||
|
// func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
// handleScrollViewDidScroll(scrollView)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
// typealias BottomLoaderTableViewCell = SearchBottomLoader
|
||||||
|
// typealias LoadingState = NotificationViewController.LoadOldestState.Loading
|
||||||
|
// var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||||
|
// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
|
||||||
|
//}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
//
|
||||||
|
// NotificationViewModel+LoadLatestState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import func QuartzCore.CACurrentMediaTime
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension NotificationViewModel {
|
||||||
|
class LoadLatestState: GKState {
|
||||||
|
weak var viewModel: NotificationViewModel?
|
||||||
|
|
||||||
|
init(viewModel: NotificationViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
viewModel?.loadLatestStateMachinePublisher.send(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewModel.LoadLatestState {
|
||||||
|
class Initial: NotificationViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: NotificationViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Fail.self || stateClass == Idle.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
// sign out when loading will enter here
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let query = Mastodon.API.Notifications.Query(
|
||||||
|
maxID: nil,
|
||||||
|
sinceID: nil,
|
||||||
|
minID: nil,
|
||||||
|
limit: nil,
|
||||||
|
excludeTypes: Mastodon.API.Notifications.allExcludeTypes(),
|
||||||
|
accountID: nil)
|
||||||
|
viewModel.context.apiService.allNotifications(
|
||||||
|
domain: activeMastodonAuthenticationBox.domain,
|
||||||
|
query: query,
|
||||||
|
mastodonAuthenticationBox: activeMastodonAuthenticationBox)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
viewModel.isFetchingLatestNotification.value = false
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
} receiveValue: { response in
|
||||||
|
if response.value.isEmpty {
|
||||||
|
viewModel.isFetchingLatestNotification.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: NotificationViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self || stateClass == Idle.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: NotificationViewModel.LoadLatestState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
//
|
||||||
|
// NotificationViewModel+diffable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
extension NotificationViewModel {
|
||||||
|
|
||||||
|
func setupDiffableDataSource(
|
||||||
|
for tableView: UITableView
|
||||||
|
) {
|
||||||
|
let timestampUpdatePublisher = Timer.publish(every: 30.0, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
.share()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
diffableDataSource = NotificationSection.tableViewDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
|
managedObjectContext: context.managedObjectContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewModel: NSFetchedResultsControllerDelegate {
|
||||||
|
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
guard let tableView = self.tableView else { return }
|
||||||
|
guard let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return }
|
||||||
|
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
let oldSnapshot = diffableDataSource.snapshot()
|
||||||
|
|
||||||
|
let predicate = fetchedResultsController.fetchRequest.predicate
|
||||||
|
let parentManagedObjectContext = fetchedResultsController.managedObjectContext
|
||||||
|
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||||
|
managedObjectContext.parent = parentManagedObjectContext
|
||||||
|
|
||||||
|
managedObjectContext.perform {
|
||||||
|
|
||||||
|
let notifications: [MastodonNotification] = {
|
||||||
|
let request = MastodonNotification.sortedFetchRequest
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
request.predicate = predicate
|
||||||
|
do {
|
||||||
|
return try managedObjectContext.fetch(request)
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var newSnapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
|
||||||
|
newSnapshot.appendSections([.main])
|
||||||
|
newSnapshot.appendItems(notifications.map({NotificationItem.notification(ObjectID: $0.objectID)}), toSection: .main)
|
||||||
|
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else {
|
||||||
|
diffableDataSource.apply(newSnapshot)
|
||||||
|
self.isFetchingLatestNotification.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diffableDataSource.apply(newSnapshot, animatingDifferences: false) {
|
||||||
|
tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false)
|
||||||
|
tableView.contentOffset.y = tableView.contentOffset.y - difference.offset
|
||||||
|
self.isFetchingLatestNotification.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Difference<T> {
|
||||||
|
let item: T
|
||||||
|
let sourceIndexPath: IndexPath
|
||||||
|
let targetIndexPath: IndexPath
|
||||||
|
let offset: CGFloat
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calculateReloadSnapshotDifference<T: Hashable>(
|
||||||
|
navigationBar: UINavigationBar,
|
||||||
|
tableView: UITableView,
|
||||||
|
oldSnapshot: NSDiffableDataSourceSnapshot<NotificationSection, T>,
|
||||||
|
newSnapshot: NSDiffableDataSourceSnapshot<NotificationSection, T>
|
||||||
|
) -> Difference<T>? {
|
||||||
|
guard oldSnapshot.numberOfItems != 0 else { return nil }
|
||||||
|
|
||||||
|
// old snapshot not empty. set source index path to first item if not match
|
||||||
|
let sourceIndexPath = UIViewController.topVisibleTableViewCellIndexPath(in: tableView, navigationBar: navigationBar) ?? IndexPath(row: 0, section: 0)
|
||||||
|
|
||||||
|
guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil }
|
||||||
|
|
||||||
|
let timelineItem = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row]
|
||||||
|
guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: timelineItem) else { return nil }
|
||||||
|
let targetIndexPath = IndexPath(row: itemIndex, section: 0)
|
||||||
|
|
||||||
|
let offset = UIViewController.tableViewCellOriginOffsetToWindowTop(in: tableView, at: sourceIndexPath, navigationBar: navigationBar)
|
||||||
|
return Difference(
|
||||||
|
item: timelineItem,
|
||||||
|
sourceIndexPath: sourceIndexPath,
|
||||||
|
targetIndexPath: targetIndexPath,
|
||||||
|
offset: offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
//
|
||||||
|
// NotificationViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import UIKit
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
|
|
||||||
|
final class NotificationViewModel: NSObject {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
weak var coordinator: SceneCoordinator!
|
||||||
|
weak var tableView: UITableView!
|
||||||
|
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||||
|
|
||||||
|
|
||||||
|
let activeMastodonAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||||
|
let fetchedResultsController: NSFetchedResultsController<MastodonNotification>!
|
||||||
|
let notificationPredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
|
||||||
|
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||||
|
|
||||||
|
let isFetchingLatestNotification = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
|
//output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<NotificationSection, NotificationItem>!
|
||||||
|
// top loader
|
||||||
|
private(set) lazy var loadLatestStateMachine: GKStateMachine = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
LoadLatestState.Initial(viewModel: self),
|
||||||
|
LoadLatestState.Loading(viewModel: self),
|
||||||
|
LoadLatestState.Fail(viewModel: self),
|
||||||
|
LoadLatestState.Idle(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(LoadLatestState.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
|
||||||
|
|
||||||
|
init(context: AppContext,coordinator: SceneCoordinator) {
|
||||||
|
self.coordinator = coordinator
|
||||||
|
self.context = context
|
||||||
|
self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||||
|
self.fetchedResultsController = {
|
||||||
|
let fetchRequest = MastodonNotification.sortedFetchRequest
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(MastodonNotification.status),#keyPath(MastodonNotification.account)]
|
||||||
|
let controller = NSFetchedResultsController(
|
||||||
|
fetchRequest: fetchRequest,
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
sectionNameKeyPath: nil,
|
||||||
|
cacheName: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
return controller
|
||||||
|
}()
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
self.fetchedResultsController.delegate = self
|
||||||
|
context.authenticationService.activeMastodonAuthenticationBox
|
||||||
|
.assign(to: \.value, on: activeMastodonAuthenticationBox)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
notificationPredicate
|
||||||
|
.compactMap{ $0 }
|
||||||
|
.sink { [weak self] predicate in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.fetchedResultsController.fetchRequest.predicate = predicate
|
||||||
|
do {
|
||||||
|
try self.fetchedResultsController.performFetch()
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
//
|
||||||
|
// NotificationTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
|
||||||
|
final class NotificationTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
static let actionImageBorderWidth: CGFloat = 3
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let avatatImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.layer.cornerRadius = 4
|
||||||
|
imageView.layer.cornerCurve = .continuous
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let actionImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.layer.cornerRadius = 4
|
||||||
|
imageView.layer.cornerCurve = .continuous
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
imageView.layer.borderWidth = NotificationTableViewCell.actionImageBorderWidth
|
||||||
|
imageView.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor
|
||||||
|
imageView.tintColor = Asset.Colors.Background.searchResult.color
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let actionLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let nameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.brandBlue.color
|
||||||
|
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||||
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
var nameLabelTop: NSLayoutConstraint!
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
avatatImageView.af.cancelImageRequest()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationTableViewCell {
|
||||||
|
|
||||||
|
func configure() {
|
||||||
|
contentView.addSubview(avatatImageView)
|
||||||
|
avatatImageView.pin(toSize: CGSize(width: 35, height: 35))
|
||||||
|
avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil)
|
||||||
|
|
||||||
|
contentView.addSubview(actionImageView)
|
||||||
|
actionImageView.pin(toSize: CGSize(width: 24, height: 24))
|
||||||
|
actionImageView.pin(top: 33, left: 33, bottom: nil, right: nil)
|
||||||
|
|
||||||
|
nameLabelTop = nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor)
|
||||||
|
nameLabel.constrain([
|
||||||
|
nameLabelTop,
|
||||||
|
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 61)
|
||||||
|
])
|
||||||
|
|
||||||
|
actionLabel.constrain([
|
||||||
|
actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4),
|
||||||
|
actionLabel.topAnchor.constraint(equalTo: nameLabel.topAnchor),
|
||||||
|
contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
public func nameLabelLayoutIn(center: Bool) {
|
||||||
|
if center {
|
||||||
|
nameLabelTop.constant = 24
|
||||||
|
} else {
|
||||||
|
nameLabelTop.constant = 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
self.actionImageView.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
//
|
||||||
|
// APIService+Notification.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/4/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
func allNotifications(
|
||||||
|
domain: String,
|
||||||
|
query: Mastodon.API.Notifications.Query,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
return Mastodon.API.Notifications.getNotifications(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
query: query,
|
||||||
|
authorization: authorization)
|
||||||
|
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> in
|
||||||
|
let log = OSLog.api
|
||||||
|
return self.backgroundManagedObjectContext.performChanges {
|
||||||
|
response.value.forEach { notification in
|
||||||
|
let (mastodonUser,isCreated) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log)
|
||||||
|
let flag = isCreated ? "+" : "-"
|
||||||
|
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)%s", (#file as NSString).lastPathComponent, #line, #function, flag, mastodonUser.id, mastodonUser.username)
|
||||||
|
var status: Status?
|
||||||
|
if let statusEntity = notification.status {
|
||||||
|
let (statusInCoreData,_,_) = APIService.CoreData.createOrMergeStatus(
|
||||||
|
into: self.backgroundManagedObjectContext,
|
||||||
|
for: nil,
|
||||||
|
domain: domain,
|
||||||
|
entity: statusEntity,
|
||||||
|
statusCache: nil,
|
||||||
|
userCache: nil,
|
||||||
|
networkDate: Date(),
|
||||||
|
log: log)
|
||||||
|
status = statusInCoreData
|
||||||
|
}
|
||||||
|
// use constrain to avoid repeated save
|
||||||
|
_ = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, property: MastodonNotification.Property(id: notification.id, type: notification.type.rawValue, account: mastodonUser, status: status, createdAt: Date()))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Notification]> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,19 @@
|
||||||
//
|
//
|
||||||
// File.swift
|
// File.swift
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// Created by BradGao on 2021/4/1.
|
// Created by BradGao on 2021/4/1.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
extension Mastodon.API.Notifications {
|
public extension Mastodon.API.Notifications {
|
||||||
static func notificationsEndpointURL(domain: String) -> URL {
|
internal static func notificationsEndpointURL(domain: String) -> URL {
|
||||||
Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent("notifications")
|
Mastodon.API.endpointURL(domain: domain).appendingPathComponent("notifications")
|
||||||
}
|
}
|
||||||
static func getNotificationEndpointURL(domain: String, notificationID: String) -> URL {
|
|
||||||
|
internal static func getNotificationEndpointURL(domain: String, notificationID: String) -> URL {
|
||||||
notificationsEndpointURL(domain: domain).appendingPathComponent(notificationID)
|
notificationsEndpointURL(domain: domain).appendingPathComponent(notificationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,15 +28,15 @@ extension Mastodon.API.Notifications {
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - session: `URLSession`
|
/// - session: `URLSession`
|
||||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
/// - query: `GetAllNotificationsQuery` with query parameters
|
/// - query: `NotificationsQuery` with query parameters
|
||||||
/// - authorization: User token
|
/// - authorization: User token
|
||||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||||
public static func getAll(
|
static func getNotifications(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
query: GetAllNotificationsQuery,
|
query: Mastodon.API.Notifications.Query,
|
||||||
authorization: Mastodon.API.OAuth.Authorization?
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> {
|
||||||
let request = Mastodon.API.get(
|
let request = Mastodon.API.get(
|
||||||
url: notificationsEndpointURL(domain: domain),
|
url: notificationsEndpointURL(domain: domain),
|
||||||
query: query,
|
query: query,
|
||||||
|
@ -63,12 +64,12 @@ extension Mastodon.API.Notifications {
|
||||||
/// - notificationID: ID of the notification.
|
/// - notificationID: ID of the notification.
|
||||||
/// - authorization: User token
|
/// - authorization: User token
|
||||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||||
public static func get(
|
static func getNotification(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
notificationID: String,
|
notificationID: String,
|
||||||
authorization: Mastodon.API.OAuth.Authorization?
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
|
||||||
let request = Mastodon.API.get(
|
let request = Mastodon.API.get(
|
||||||
url: getNotificationEndpointURL(domain: domain, notificationID: notificationID),
|
url: getNotificationEndpointURL(domain: domain, notificationID: notificationID),
|
||||||
query: nil,
|
query: nil,
|
||||||
|
@ -82,12 +83,22 @@ extension Mastodon.API.Notifications {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct GetAllNotificationsQuery: Codable, PagedQueryType, GetQuery {
|
static func allExcludeTypes() -> [Mastodon.Entity.Notification.NotificationType] {
|
||||||
|
[.follow]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func mentionsExcludeTypes() -> [Mastodon.Entity.Notification.NotificationType] {
|
||||||
|
[.follow, .followRequest, .favourite, .reblog, .poll]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Mastodon.API.Notifications {
|
||||||
|
struct Query: Codable, PagedQueryType, GetQuery {
|
||||||
public let maxID: Mastodon.Entity.Status.ID?
|
public let maxID: Mastodon.Entity.Status.ID?
|
||||||
public let sinceID: Mastodon.Entity.Status.ID?
|
public let sinceID: Mastodon.Entity.Status.ID?
|
||||||
public let minID: Mastodon.Entity.Status.ID?
|
public let minID: Mastodon.Entity.Status.ID?
|
||||||
public let limit: Int?
|
public let limit: Int?
|
||||||
public let excludeTypes: [String]?
|
public let excludeTypes: [Mastodon.Entity.Notification.NotificationType]?
|
||||||
public let accountID: String?
|
public let accountID: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
@ -95,7 +106,7 @@ extension Mastodon.API.Notifications {
|
||||||
sinceID: Mastodon.Entity.Status.ID? = nil,
|
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||||
minID: Mastodon.Entity.Status.ID? = nil,
|
minID: Mastodon.Entity.Status.ID? = nil,
|
||||||
limit: Int? = nil,
|
limit: Int? = nil,
|
||||||
excludeTypes: [String]? = nil,
|
excludeTypes: [Mastodon.Entity.Notification.NotificationType]? = nil,
|
||||||
accountID: String? = nil
|
accountID: String? = nil
|
||||||
) {
|
) {
|
||||||
self.maxID = maxID
|
self.maxID = maxID
|
||||||
|
@ -114,7 +125,7 @@ extension Mastodon.API.Notifications {
|
||||||
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
|
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
|
||||||
if let excludeTypes = excludeTypes {
|
if let excludeTypes = excludeTypes {
|
||||||
excludeTypes.forEach {
|
excludeTypes.forEach {
|
||||||
items.append(URLQueryItem(name: "exclude_types[]", value: $0))
|
items.append(URLQueryItem(name: "exclude_types[]", value: $0.rawValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
accountID.flatMap { items.append(URLQueryItem(name: "account_id", value: $0)) }
|
accountID.flatMap { items.append(URLQueryItem(name: "account_id", value: $0)) }
|
||||||
|
|
|
@ -37,6 +37,7 @@ extension Mastodon.Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.Entity.Notification {
|
extension Mastodon.Entity.Notification {
|
||||||
|
public typealias NotificationType = Type
|
||||||
public enum `Type`: RawRepresentable, Codable {
|
public enum `Type`: RawRepresentable, Codable {
|
||||||
case follow
|
case follow
|
||||||
case followRequest
|
case followRequest
|
||||||
|
|
Loading…
Reference in New Issue