feat: adapt the app to async & await. Update timeline UI

This commit is contained in:
CMK 2022-01-27 21:23:39 +08:00
parent b8181b83c9
commit b76b3a2750
754 changed files with 29810 additions and 25213 deletions

View File

@ -17,6 +17,6 @@
<key>CFBundleShortVersionString</key>
<string>1.3.0</string>
<key>CFBundleVersion</key>
<string>90</string>
<string>91</string>
</dict>
</plist>

View File

@ -0,0 +1,6 @@
sources:
- .
templates:
- ./Template
output:
Generated

View File

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>CoreData 2.xcdatamodel</string>
<string>CoreData 3.xcdatamodel</string>
</dict>
</plist>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19206" systemVersion="20G165" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21C52" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Application" representedClassName=".Application" syncable="YES">
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>

View File

@ -0,0 +1,283 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Application" representedClassName=".Application" syncable="YES">
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="vapidKey" optional="YES" attributeType="String"/>
<attribute name="website" optional="YES" attributeType="String"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="application" inverseEntity="Status"/>
</entity>
<entity name="DomainBlock" representedClassName=".DomainBlock" syncable="YES">
<attribute name="blockedDomain" attributeType="String"/>
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="userID" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="userID"/>
<constraint value="domain"/>
<constraint value="blockedDomain"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Emoji" representedClassName=".Emoji" syncable="YES">
<attribute name="category" optional="YES" attributeType="String"/>
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="shortcode" attributeType="String"/>
<attribute name="staticURL" attributeType="String"/>
<attribute name="url" attributeType="String"/>
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES"/>
</entity>
<entity name="Feed" representedClassName=".Feed" syncable="YES">
<attribute name="acctRaw" optional="YES" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hasMore" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isLoadingMore" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="kindRaw" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="notification" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Notification" inverseName="feeds" inverseEntity="Notification"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="feeds" inverseEntity="Status"/>
</entity>
<entity name="HomeTimelineIndex" representedClassName=".HomeTimelineIndex" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="hasMore" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="userID" attributeType="String"/>
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
</entity>
<entity name="Instance" representedClassName=".Instance" syncable="YES">
<attribute name="configurationRaw" optional="YES" attributeType="Binary"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
</entity>
<entity name="MastodonAuthentication" representedClassName=".MastodonAuthentication" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="appAccessToken" attributeType="String"/>
<attribute name="clientID" attributeType="String"/>
<attribute name="clientSecret" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userAccessToken" attributeType="String"/>
<attribute name="userID" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<relationship name="instance" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Instance" inverseName="authentications" inverseEntity="Instance"/>
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
</entity>
<entity name="MastodonUser" representedClassName=".MastodonUser" syncable="YES">
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" attributeType="String"/>
<attribute name="avatarStatic" optional="YES" attributeType="String"/>
<attribute name="bot" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="displayName" attributeType="String"/>
<attribute name="domain" attributeType="String"/>
<attribute name="emojis" optional="YES" attributeType="Binary"/>
<attribute name="fields" optional="YES" attributeType="Binary"/>
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="header" attributeType="String"/>
<attribute name="headerStatic" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="suspended" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<relationship name="blocking" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blockingBy" inverseEntity="MastodonUser"/>
<relationship name="blockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blocking" inverseEntity="MastodonUser"/>
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="bookmarkedBy" inverseEntity="Status"/>
<relationship name="domainBlocking" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlockingBy" inverseEntity="MastodonUser"/>
<relationship name="domainBlockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlocking" inverseEntity="MastodonUser"/>
<relationship name="endorsed" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsedBy" inverseEntity="MastodonUser"/>
<relationship name="endorsedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsed" inverseEntity="MastodonUser"/>
<relationship name="favourite" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="favouritedBy" inverseEntity="Status"/>
<relationship name="following" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followingBy" inverseEntity="MastodonUser"/>
<relationship name="followingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="following" inverseEntity="MastodonUser"/>
<relationship name="followRequested" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequestedBy" inverseEntity="MastodonUser"/>
<relationship name="followRequestedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequested" inverseEntity="MastodonUser"/>
<relationship name="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
<relationship name="muted" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="mutedBy" inverseEntity="Status"/>
<relationship name="muting" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mutingBy" inverseEntity="MastodonUser"/>
<relationship name="mutingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muting" inverseEntity="MastodonUser"/>
<relationship name="notifications" toMany="YES" deletionRule="Nullify" destinationEntity="Notification" inverseName="account" inverseEntity="Notification"/>
<relationship name="pinnedStatus" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="pinnedBy" inverseEntity="Status"/>
<relationship name="privateNotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="to" inverseEntity="PrivateNote"/>
<relationship name="privateNotesTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="from" inverseEntity="PrivateNote"/>
<relationship name="reblogged" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="account" inverseEntity="SearchHistory"/>
<relationship name="statuses" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="author" inverseEntity="Status"/>
<relationship name="votePollOptions" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
<relationship name="votePolls" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
</entity>
<entity name="Notification" representedClassName=".Notification" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="typeRaw" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String"/>
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="notifications" inverseEntity="MastodonUser"/>
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="notification" inverseEntity="Feed"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="notifications" inverseEntity="Status"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Poll" representedClassName=".Poll" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="expired" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="String"/>
<attribute name="isVoting" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="multiple" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="votersCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="options" toMany="YES" deletionRule="Cascade" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="poll" inverseEntity="Status"/>
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePolls" inverseEntity="MastodonUser"/>
</entity>
<entity name="PollOption" representedClassName=".PollOption" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isSelected" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="title" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="votesCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="poll" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
</entity>
<entity name="PrivateNote" representedClassName=".PrivateNote" syncable="YES">
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/>
<relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
</entity>
<entity name="SearchHistory" representedClassName=".SearchHistory" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String" defaultValueString=""/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="searchHistories" inverseEntity="MastodonUser"/>
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="searchHistories" inverseEntity="Tag"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="searchHistories" inverseEntity="Status"/>
</entity>
<entity name="Setting" representedClassName=".Setting" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="preferredStaticAvatar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="preferredStaticEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="preferredTrueBlackDarkMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="preferredUsingDefaultBrowser" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String"/>
<relationship name="subscriptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Subscription" inverseName="setting" inverseEntity="Subscription"/>
</entity>
<entity name="Status" representedClassName=".Status" syncable="YES">
<attribute name="attachments" optional="YES" attributeType="Binary"/>
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="emojis" optional="YES" attributeType="Binary"/>
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
<attribute name="language" optional="YES" attributeType="String"/>
<attribute name="mentions" optional="YES" attributeType="Binary"/>
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="repliesCount" optional="YES" attributeType="Integer 64" usesScalarValueType="NO"/>
<attribute name="revealedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="spoilerText" optional="YES" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="visibilityRaw" optional="YES" attributeType="String" elementID="visibility"/>
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="status" inverseEntity="Application"/>
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="statuses" inverseEntity="MastodonUser"/>
<relationship name="bookmarkedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
<relationship name="favouritedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="status" inverseEntity="Feed"/>
<relationship name="mutedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
<relationship name="notifications" toMany="YES" deletionRule="Cascade" destinationEntity="Notification" inverseName="status" inverseEntity="Notification"/>
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedStatus" inverseEntity="MastodonUser"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Poll" inverseName="status" inverseEntity="Poll"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogFrom" inverseEntity="Status"/>
<relationship name="reblogFrom" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
<relationship name="rebloggedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
<relationship name="replyFrom" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/>
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Cascade" destinationEntity="SearchHistory" inverseName="status" inverseEntity="SearchHistory"/>
</entity>
<entity name="Subscription" representedClassName=".Subscription" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="endpoint" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="policyRaw" attributeType="String"/>
<attribute name="serverKey" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userToken" optional="YES" attributeType="String"/>
<relationship name="alert" maxCount="1" deletionRule="Cascade" destinationEntity="SubscriptionAlerts" inverseName="subscription" inverseEntity="SubscriptionAlerts"/>
<relationship name="setting" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Setting" inverseName="subscriptions" inverseEntity="Setting"/>
</entity>
<entity name="SubscriptionAlerts" representedClassName=".SubscriptionAlerts" syncable="YES">
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="favouriteRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="followRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="followRequestRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mentionRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="pollRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="reblogRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="subscription" maxCount="1" deletionRule="Nullify" destinationEntity="Subscription" inverseName="alert" inverseEntity="Subscription"/>
</entity>
<entity name="Tag" representedClassName=".Tag" syncable="YES">
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="histories" optional="YES" attributeType="Binary"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" attributeType="String"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
</entity>
<elements>
<element name="Application" positionX="0" positionY="0" width="128" height="104"/>
<element name="DomainBlock" positionX="45" positionY="162" width="128" height="89"/>
<element name="Emoji" positionX="0" positionY="0" width="128" height="134"/>
<element name="Feed" positionX="54" positionY="171" width="128" height="149"/>
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
<element name="Instance" positionX="45" positionY="162" width="128" height="104"/>
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="224"/>
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="734"/>
<element name="Notification" positionX="9" positionY="162" width="128" height="164"/>
<element name="Poll" positionX="0" positionY="0" width="128" height="224"/>
<element name="PollOption" positionX="0" positionY="0" width="128" height="149"/>
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="149"/>
<element name="Setting" positionX="72" positionY="162" width="128" height="164"/>
<element name="Status" positionX="0" positionY="0" width="128" height="599"/>
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
<element name="Tag" positionX="0" positionY="0" width="128" height="149"/>
</elements>
</model>

View File

@ -0,0 +1,198 @@
//
// Feed.swift
// CoreDataStack
//
// Created by MainasuK on 2022-1-11.
//
import Foundation
import CoreData
final public class Feed: NSManagedObject {
@NSManaged public private(set) var acctRaw: String
// sourcery: autoGenerateProperty
public var acct: Acct {
get {
Acct(rawValue: acctRaw) ?? .none
}
set {
acctRaw = newValue.rawValue
}
}
@NSManaged public private(set) var kindRaw: String
// sourcery: autoGenerateProperty
public var kind: Kind {
get {
Kind(rawValue: kindRaw) ?? .none
}
set {
kindRaw = newValue.rawValue
}
}
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var hasMore: Bool
// sourcery: autoUpdatableObject
@NSManaged public private(set) var isLoadingMore: Bool
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var createdAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date
// one-to-one relationship
@NSManaged public private(set) var status: Status?
@NSManaged public private(set) var notification: Notification?
}
extension Feed {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property
) -> Feed {
let object: Feed = context.insertObject()
object.configure(property: property)
return object
}
}
extension Feed: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \Feed.createdAt, ascending: false)]
}
}
extension Feed {
static func predicate(kind: Kind) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Feed.kindRaw), kind.rawValue)
}
static func predicate(acct: Acct) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Feed.acctRaw), acct.rawValue)
}
public static func predicate(kind: Kind, acct: Acct) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
Feed.predicate(kind: kind),
Feed.predicate(acct: acct)
])
}
public static func nonePredicate() -> NSPredicate {
return predicate(kind: .none, acct: .none)
}
public static func hasMorePredicate() -> NSPredicate {
return NSPredicate(format: "%K == YES", #keyPath(Feed.hasMore))
}
public static func hasNotificationPredicate() -> NSPredicate {
return NSPredicate(format: "%K != nil", #keyPath(Feed.notification))
}
public static func notificationTypePredicate(types: [MastodonNotificationType]) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
hasNotificationPredicate(),
NSPredicate(
format: "%K.%K IN %@",
#keyPath(Feed.notification),
#keyPath(Notification.typeRaw),
types.map { $0.rawValue }
)
])
}
}
// MARK: - AutoGenerateProperty
extension Feed: AutoGenerateProperty {
// sourcery:inline:Feed.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
public let acct: Acct
public let kind: Kind
public let hasMore: Bool
public let createdAt: Date
public let updatedAt: Date
public init(
acct: Acct,
kind: Kind,
hasMore: Bool,
createdAt: Date,
updatedAt: Date
) {
self.acct = acct
self.kind = kind
self.hasMore = hasMore
self.createdAt = createdAt
self.updatedAt = updatedAt
}
}
public func configure(property: Property) {
self.acct = property.acct
self.kind = property.kind
self.hasMore = property.hasMore
self.createdAt = property.createdAt
self.updatedAt = property.updatedAt
}
public func update(property: Property) {
update(hasMore: property.hasMore)
update(createdAt: property.createdAt)
update(updatedAt: property.updatedAt)
}
// sourcery:end
}
// MARK: - AutoUpdatableObject
extension Feed: AutoUpdatableObject {
// sourcery:inline:Feed.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
public func update(hasMore: Bool) {
if self.hasMore != hasMore {
self.hasMore = hasMore
}
}
public func update(isLoadingMore: Bool) {
if self.isLoadingMore != isLoadingMore {
self.isLoadingMore = isLoadingMore
}
}
public func update(createdAt: Date) {
if self.createdAt != createdAt {
self.createdAt = createdAt
}
}
public func update(updatedAt: Date) {
if self.updatedAt != updatedAt {
self.updatedAt = updatedAt
}
}
// sourcery:end
}
public protocol FeedIndexable {
var feeds: Set<Feed> { get }
func feed(kind: Feed.Kind, acct: Feed.Acct) -> Feed?
}
extension FeedIndexable {
public func feed(kind: Feed.Kind, acct: Feed.Acct) -> Feed? {
return feeds.first(where: { feed in
feed.kind == kind && feed.acct == acct
})
}
}

View File

@ -1,126 +0,0 @@
//
// Attachment.swift
// CoreDataStack
//
// Created by MainasuK Cirno on 2021-2-23.
//
import CoreData
import Foundation
public final class Attachment: NSManagedObject {
public typealias ID = String
@NSManaged public private(set) var id: ID
@NSManaged public private(set) var domain: String
@NSManaged public private(set) var typeRaw: String
@NSManaged public private(set) var url: String
@NSManaged public private(set) var previewURL: String?
@NSManaged public private(set) var remoteURL: String?
@NSManaged public private(set) var metaData: Data?
@NSManaged public private(set) var textURL: String?
@NSManaged public private(set) var descriptionString: String?
@NSManaged public private(set) var blurhash: String?
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
@NSManaged public private(set) var index: NSNumber
// many-to-one relationship
@NSManaged public private(set) var status: Status?
}
public extension Attachment {
override func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(Date(), forKey: #keyPath(Attachment.createdAt))
}
@discardableResult
static func insert(
into context: NSManagedObjectContext,
property: Property
) -> Attachment {
let attachment: Attachment = context.insertObject()
attachment.domain = property.domain
attachment.index = property.index
attachment.id = property.id
attachment.typeRaw = property.typeRaw
attachment.url = property.url
attachment.previewURL = property.previewURL
attachment.remoteURL = property.remoteURL
attachment.metaData = property.metaData
attachment.textURL = property.textURL
attachment.descriptionString = property.descriptionString
attachment.blurhash = property.blurhash
attachment.updatedAt = property.networkDate
return attachment
}
func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}
}
public extension Attachment {
struct Property {
public let domain: String
public let index: NSNumber
public let id: ID
public let typeRaw: String
public let url: String
public let previewURL: String?
public let remoteURL: String?
public let metaData: Data?
public let textURL: String?
public let descriptionString: String?
public let blurhash: String?
public let networkDate: Date
public init(
domain: String,
index: Int,
id: Attachment.ID,
typeRaw: String,
url: String,
previewURL: String?,
remoteURL: String?,
metaData: Data?,
textURL: String?,
descriptionString: String?,
blurhash: String?,
networkDate: Date
) {
self.domain = domain
self.index = NSNumber(value: index)
self.id = id
self.typeRaw = typeRaw
self.url = url
self.previewURL = previewURL
self.remoteURL = remoteURL
self.metaData = metaData
self.textURL = textURL
self.descriptionString = descriptionString
self.blurhash = blurhash
self.networkDate = networkDate
}
}
}
extension Attachment: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \Attachment.createdAt, ascending: false)]
}
}

View File

@ -0,0 +1,604 @@
//
// MastodonUser.swift
// CoreDataStack
//
// Created by MainasuK Cirno on 2021/1/27.
//
import CoreData
import Foundation
final public class MastodonUser: NSManagedObject {
public typealias ID = String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var identifier: ID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var domain: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var id: ID
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var acct: String
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var username: String
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var displayName: String
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var avatar: String
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var avatarStatic: String?
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var header: String
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var headerStatic: String?
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var note: String?
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var url: String?
@NSManaged public private(set) var emojisData: Data?
@NSManaged public private(set) var fieldsData: Data?
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var statusesCount: Int64
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var followingCount: Int64
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var followersCount: Int64
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var locked: Bool
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var bot: Bool
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var suspended: Bool
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var createdAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date
// one-to-one relationship
@NSManaged public private(set) var pinnedStatus: Status?
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
// one-to-many relationship
@NSManaged public private(set) var statuses: Set<Status>
@NSManaged public private(set) var notifications: Set<Notification>
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
// many-to-many relationship
@NSManaged public private(set) var favourite: Set<Status>
@NSManaged public private(set) var reblogged: Set<Status>
@NSManaged public private(set) var muted: Set<Status>
@NSManaged public private(set) var bookmarked: Set<Status>
@NSManaged public private(set) var votePollOptions: Set<PollOption>
@NSManaged public private(set) var votePolls: Set<Poll>
// relationships
@NSManaged public private(set) var following: Set<MastodonUser>
@NSManaged public private(set) var followingBy: Set<MastodonUser>
@NSManaged public private(set) var followRequested: Set<MastodonUser>
@NSManaged public private(set) var followRequestedBy: Set<MastodonUser>
@NSManaged public private(set) var muting: Set<MastodonUser>
@NSManaged public private(set) var mutingBy: Set<MastodonUser>
@NSManaged public private(set) var blocking: Set<MastodonUser>
@NSManaged public private(set) var blockingBy: Set<MastodonUser>
@NSManaged public private(set) var endorsed: Set<MastodonUser>
@NSManaged public private(set) var endorsedBy: Set<MastodonUser>
@NSManaged public private(set) var domainBlocking: Set<MastodonUser>
@NSManaged public private(set) var domainBlockingBy: Set<MastodonUser>
}
extension MastodonUser {
// sourcery: autoUpdatableObject, autoGenerateProperty
@objc public var emojis: [MastodonEmoji] {
get {
let keyPath = #keyPath(MastodonUser.emojis)
willAccessValue(forKey: keyPath)
let _data = primitiveValue(forKey: keyPath) as? Data
didAccessValue(forKey: keyPath)
do {
guard let data = _data else { return [] }
let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data)
return emojis
} catch {
assertionFailure(error.localizedDescription)
return []
}
}
set {
let keyPath = #keyPath(MastodonUser.emojis)
let data = try? JSONEncoder().encode(newValue)
willChangeValue(forKey: keyPath)
setPrimitiveValue(data, forKey: keyPath)
didChangeValue(forKey: keyPath)
}
}
// sourcery: autoUpdatableObject, autoGenerateProperty
@objc public var fields: [MastodonField] {
get {
let keyPath = #keyPath(MastodonUser.fields)
willAccessValue(forKey: keyPath)
let _data = primitiveValue(forKey: keyPath) as? Data
didAccessValue(forKey: keyPath)
do {
guard let data = _data else { return [] }
let fields = try JSONDecoder().decode([MastodonField].self, from: data)
return fields
} catch {
assertionFailure(error.localizedDescription)
return []
}
}
set {
let keyPath = #keyPath(MastodonUser.fields)
let data = try? JSONEncoder().encode(newValue)
willChangeValue(forKey: keyPath)
setPrimitiveValue(data, forKey: keyPath)
didChangeValue(forKey: keyPath)
}
}
}
extension MastodonUser {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property
) -> MastodonUser {
let object: MastodonUser = context.insertObject()
object.configure(property: property)
return object
}
}
extension MastodonUser: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \MastodonUser.createdAt, ascending: false)]
}
}
extension MastodonUser {
static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.domain), domain)
}
static func predicate(id: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.id), id)
}
public static func predicate(domain: String, id: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonUser.predicate(domain: domain),
MastodonUser.predicate(id: id)
])
}
static func predicate(ids: [String]) -> NSPredicate {
return NSPredicate(format: "%K IN %@", #keyPath(MastodonUser.id), ids)
}
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonUser.predicate(domain: domain),
MastodonUser.predicate(ids: ids)
])
}
static func predicate(username: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.username), username)
}
public static func predicate(domain: String, username: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonUser.predicate(domain: domain),
MastodonUser.predicate(username: username)
])
}
}
extension MastodonUser {
public func findSearchHistory(
domain: String,
userID: MastodonUser.ID
) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == domain
&& searchHistory.userID == userID
}
}
public func findSearchHistory(for user: MastodonUser) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == user.domain
&& searchHistory.userID == user.id
}
}
}
// MARK: - AutoGenerateProperty
extension MastodonUser: AutoGenerateProperty {
// sourcery:inline:MastodonUser.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
public let identifier: ID
public let domain: String
public let id: ID
public let acct: String
public let username: String
public let displayName: String
public let avatar: String
public let avatarStatic: String?
public let header: String
public let headerStatic: String?
public let note: String?
public let url: String?
public let statusesCount: Int64
public let followingCount: Int64
public let followersCount: Int64
public let locked: Bool
public let bot: Bool
public let suspended: Bool
public let createdAt: Date
public let updatedAt: Date
public let emojis: [MastodonEmoji]
public let fields: [MastodonField]
public init(
identifier: ID,
domain: String,
id: ID,
acct: String,
username: String,
displayName: String,
avatar: String,
avatarStatic: String?,
header: String,
headerStatic: String?,
note: String?,
url: String?,
statusesCount: Int64,
followingCount: Int64,
followersCount: Int64,
locked: Bool,
bot: Bool,
suspended: Bool,
createdAt: Date,
updatedAt: Date,
emojis: [MastodonEmoji],
fields: [MastodonField]
) {
self.identifier = identifier
self.domain = domain
self.id = id
self.acct = acct
self.username = username
self.displayName = displayName
self.avatar = avatar
self.avatarStatic = avatarStatic
self.header = header
self.headerStatic = headerStatic
self.note = note
self.url = url
self.statusesCount = statusesCount
self.followingCount = followingCount
self.followersCount = followersCount
self.locked = locked
self.bot = bot
self.suspended = suspended
self.createdAt = createdAt
self.updatedAt = updatedAt
self.emojis = emojis
self.fields = fields
}
}
public func configure(property: Property) {
self.identifier = property.identifier
self.domain = property.domain
self.id = property.id
self.acct = property.acct
self.username = property.username
self.displayName = property.displayName
self.avatar = property.avatar
self.avatarStatic = property.avatarStatic
self.header = property.header
self.headerStatic = property.headerStatic
self.note = property.note
self.url = property.url
self.statusesCount = property.statusesCount
self.followingCount = property.followingCount
self.followersCount = property.followersCount
self.locked = property.locked
self.bot = property.bot
self.suspended = property.suspended
self.createdAt = property.createdAt
self.updatedAt = property.updatedAt
self.emojis = property.emojis
self.fields = property.fields
}
public func update(property: Property) {
update(acct: property.acct)
update(username: property.username)
update(displayName: property.displayName)
update(avatar: property.avatar)
update(avatarStatic: property.avatarStatic)
update(header: property.header)
update(headerStatic: property.headerStatic)
update(note: property.note)
update(url: property.url)
update(statusesCount: property.statusesCount)
update(followingCount: property.followingCount)
update(followersCount: property.followersCount)
update(locked: property.locked)
update(bot: property.bot)
update(suspended: property.suspended)
update(createdAt: property.createdAt)
update(updatedAt: property.updatedAt)
update(emojis: property.emojis)
update(fields: property.fields)
}
// sourcery:end
}
//extension MastodonUser {
// public struct Property {
// public let identifier: String
// public let domain: String
//
// public let id: String
// public let acct: String
// public let username: String
// public let displayName: String
// public let avatar: String
// public let avatarStatic: String?
// public let header: String
// public let headerStatic: String?
// public let note: String?
// public let url: String?
// public let emojisData: Data?
// public let fieldsData: Data?
// public let statusesCount: Int
// public let followingCount: Int
// public let followersCount: Int
// public let locked: Bool
// public let bot: Bool?
// public let suspended: Bool?
//
// public let createdAt: Date
// public let networkDate: Date
//
// public init(
// id: String,
// domain: String,
// acct: String,
// username: String,
// displayName: String,
// avatar: String,
// avatarStatic: String?,
// header: String,
// headerStatic: String?,
// note: String?,
// url: String?,
// emojisData: Data?,
// fieldsData: Data?,
// statusesCount: Int,
// followingCount: Int,
// followersCount: Int,
// locked: Bool,
// bot: Bool?,
// suspended: Bool?,
// createdAt: Date,
// networkDate: Date
// ) {
// self.identifier = id + "@" + domain
// self.domain = domain
// self.id = id
// self.acct = acct
// self.username = username
// self.displayName = displayName
// self.avatar = avatar
// self.avatarStatic = avatarStatic
// self.header = header
// self.headerStatic = headerStatic
// self.note = note
// self.url = url
// self.emojisData = emojisData
// self.fieldsData = fieldsData
// self.statusesCount = statusesCount
// self.followingCount = followingCount
// self.followersCount = followersCount
// self.locked = locked
// self.bot = bot
// self.suspended = suspended
// self.createdAt = createdAt
// self.networkDate = networkDate
// }
// }
//}
// MARK: - AutoUpdatableObject
extension MastodonUser: AutoUpdatableObject {
// sourcery:inline:MastodonUser.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
public func update(acct: String) {
if self.acct != acct {
self.acct = acct
}
}
public func update(username: String) {
if self.username != username {
self.username = username
}
}
public func update(displayName: String) {
if self.displayName != displayName {
self.displayName = displayName
}
}
public func update(avatar: String) {
if self.avatar != avatar {
self.avatar = avatar
}
}
public func update(avatarStatic: String?) {
if self.avatarStatic != avatarStatic {
self.avatarStatic = avatarStatic
}
}
public func update(header: String) {
if self.header != header {
self.header = header
}
}
public func update(headerStatic: String?) {
if self.headerStatic != headerStatic {
self.headerStatic = headerStatic
}
}
public func update(note: String?) {
if self.note != note {
self.note = note
}
}
public func update(url: String?) {
if self.url != url {
self.url = url
}
}
public func update(statusesCount: Int64) {
if self.statusesCount != statusesCount {
self.statusesCount = statusesCount
}
}
public func update(followingCount: Int64) {
if self.followingCount != followingCount {
self.followingCount = followingCount
}
}
public func update(followersCount: Int64) {
if self.followersCount != followersCount {
self.followersCount = followersCount
}
}
public func update(locked: Bool) {
if self.locked != locked {
self.locked = locked
}
}
public func update(bot: Bool) {
if self.bot != bot {
self.bot = bot
}
}
public func update(suspended: Bool) {
if self.suspended != suspended {
self.suspended = suspended
}
}
public func update(createdAt: Date) {
if self.createdAt != createdAt {
self.createdAt = createdAt
}
}
public func update(updatedAt: Date) {
if self.updatedAt != updatedAt {
self.updatedAt = updatedAt
}
}
public func update(emojis: [MastodonEmoji]) {
if self.emojis != emojis {
self.emojis = emojis
}
}
public func update(fields: [MastodonField]) {
if self.fields != fields {
self.fields = fields
}
}
// sourcery:end
public func update(isFollowing: Bool, by mastodonUser: MastodonUser) {
if isFollowing {
if !self.followingBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).add(mastodonUser)
}
} else {
if self.followingBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).remove(mastodonUser)
}
}
}
public func update(isFollowRequested: Bool, by mastodonUser: MastodonUser) {
if isFollowRequested {
if !self.followRequestedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).add(mastodonUser)
}
} else {
if self.followRequestedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).remove(mastodonUser)
}
}
}
public func update(isMuting: Bool, by mastodonUser: MastodonUser) {
if isMuting {
if !self.mutingBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).add(mastodonUser)
}
} else {
if self.mutingBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).remove(mastodonUser)
}
}
}
public func update(isBlocking: Bool, by mastodonUser: MastodonUser) {
if isBlocking {
if !self.blockingBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).add(mastodonUser)
}
} else {
if self.blockingBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).remove(mastodonUser)
}
}
}
public func update(isEndorsed: Bool, by mastodonUser: MastodonUser) {
if isEndorsed {
if !self.endorsedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).add(mastodonUser)
}
} else {
if self.endorsedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).remove(mastodonUser)
}
}
}
public func update(isDomainBlocking: Bool, by mastodonUser: MastodonUser) {
if isDomainBlocking {
if !self.domainBlockingBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).add(mastodonUser)
}
} else {
if self.domainBlockingBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).remove(mastodonUser)
}
}
}
}

View File

@ -0,0 +1,207 @@
//
// Notification.swift
// CoreDataStack
//
// Created by sxiaojian on 2021/4/13.
//
import Foundation
import CoreData
public final class Notification: NSManagedObject {
public typealias ID = String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var id: ID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var typeRaw: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var domain: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var userID: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var createAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date
// one-to-one relationship
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var account: MastodonUser
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var status: Status?
// many-to-one relationship
@NSManaged public private(set) var feeds: Set<Feed>
}
extension Notification: FeedIndexable { }
extension Notification {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
relationship: Relationship
) -> Notification {
let object: Notification = context.insertObject()
object.configure(property: property)
object.configure(relationship: relationship)
return object
}
}
extension Notification: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \Notification.createAt, ascending: false)]
}
}
extension Notification {
static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Notification.domain), domain)
}
static func predicate(userID: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Notification.userID), userID)
}
static func predicate(id: ID) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Notification.id), id)
}
static func predicate(typeRaw: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Notification.typeRaw), typeRaw)
}
public static func predicate(
domain: String,
userID: String,
id: ID
) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
Notification.predicate(domain: domain),
Notification.predicate(userID: userID),
Notification.predicate(id: id)
])
}
public static func predicate(
domain: String,
userID: String,
typeRaw: String? = nil
) -> NSPredicate {
if let typeRaw = typeRaw {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
Notification.predicate(domain: domain),
Notification.predicate(typeRaw: typeRaw),
Notification.predicate(userID: userID),
])
} else {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
Notification.predicate(domain: domain),
Notification.predicate(userID: userID)
])
}
}
public static func predicate(validTypesRaws types: [String]) -> NSPredicate {
return NSPredicate(format: "%K IN %@", #keyPath(Notification.typeRaw), types)
}
}
// MARK: - AutoGenerateProperty
extension Notification: AutoGenerateProperty {
// sourcery:inline:Notification.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
public let id: ID
public let typeRaw: String
public let domain: String
public let userID: String
public let createAt: Date
public let updatedAt: Date
public init(
id: ID,
typeRaw: String,
domain: String,
userID: String,
createAt: Date,
updatedAt: Date
) {
self.id = id
self.typeRaw = typeRaw
self.domain = domain
self.userID = userID
self.createAt = createAt
self.updatedAt = updatedAt
}
}
public func configure(property: Property) {
self.id = property.id
self.typeRaw = property.typeRaw
self.domain = property.domain
self.userID = property.userID
self.createAt = property.createAt
self.updatedAt = property.updatedAt
}
public func update(property: Property) {
update(updatedAt: property.updatedAt)
}
// sourcery:end
}
// MARK: - AutoGenerateRelationship
extension Notification: AutoGenerateRelationship {
// sourcery:inline:Notification.AutoGenerateRelationship
// Generated using Sourcery
// DO NOT EDIT
public struct Relationship {
public let account: MastodonUser
public let status: Status?
public init(
account: MastodonUser,
status: Status?
) {
self.account = account
self.status = status
}
}
public func configure(relationship: Relationship) {
self.account = relationship.account
self.status = relationship.status
}
// sourcery:end
}
// MARK: - AutoUpdatableObject
extension Notification: AutoUpdatableObject {
// sourcery:inline:Notification.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
public func update(updatedAt: Date) {
if self.updatedAt != updatedAt {
self.updatedAt = updatedAt
}
}
// sourcery:end
}
extension Notification {
public func attach(feed: Feed) {
mutableSetValue(forKey: #keyPath(Notification.feeds)).add(feed)
}
}

View File

@ -0,0 +1,326 @@
//
// Poll.swift
// CoreDataStack
//
// Created by MainasuK Cirno on 2021-3-2.
//
import Foundation
import CoreData
public final class Poll: NSManagedObject {
public typealias ID = String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var domain: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var id: ID
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var expiresAt: Date?
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var expired: Bool
// sourcery: autoGenerateProperty
@NSManaged public private(set) var multiple: Bool
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var votesCount: Int64
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var votersCount: Int64
// sourcery: autoGenerateProperty
@NSManaged public private(set) var createdAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date
// sourcery: autoUpdatableObject
@NSManaged public private(set) var isVoting: Bool
// one-to-one relationship
@NSManaged public private(set) var status: Status
// one-to-many relationship
@NSManaged public private(set) var options: Set<PollOption>
// many-to-many relationship
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
}
extension Poll {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property
) -> Poll {
let object: Poll = context.insertObject()
object.configure(property: property)
return object
}
}
extension Poll: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \Poll.createdAt, ascending: false)]
}
}
extension Poll {
static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Poll.domain), domain)
}
static func predicate(id: ID) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Poll.id), id)
}
static func predicate(ids: [ID]) -> NSPredicate {
return NSPredicate(format: "%K IN %@", #keyPath(Poll.id), ids)
}
public static func predicate(domain: String, id: ID) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(id: id)
])
}
public static func predicate(domain: String, ids: [ID]) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(ids: ids)
])
}
}
//extension Poll {
//
// public override func awakeFromInsert() {
// super.awakeFromInsert()
// setPrimitiveValue(Date(), forKey: #keyPath(Poll.createdAt))
// }
//
// @discardableResult
// public static func insert(
// into context: NSManagedObjectContext,
// property: Property,
// votedBy: MastodonUser?,
// options: [PollOption]
// ) -> Poll {
// let poll: Poll = context.insertObject()
//
// poll.id = property.id
// poll.expiresAt = property.expiresAt
// poll.expired = property.expired
// poll.multiple = property.multiple
// poll.votesCount = property.votesCount
// poll.votersCount = property.votersCount
//
//
// poll.updatedAt = property.networkDate
//
// if let votedBy = votedBy {
// poll.mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(votedBy)
// }
// poll.mutableSetValue(forKey: #keyPath(Poll.options)).addObjects(from: options)
//
// return poll
// }
//
// public func update(expiresAt: Date?) {
// if self.expiresAt != expiresAt {
// self.expiresAt = expiresAt
// }
// }
//
// public func update(expired: Bool) {
// if self.expired != expired {
// self.expired = expired
// }
// }
//
// public func update(votesCount: Int) {
// if self.votesCount.intValue != votesCount {
// self.votesCount = NSNumber(value: votesCount)
// }
// }
//
// public func update(votersCount: Int?) {
// if self.votersCount?.intValue != votersCount {
// self.votersCount = votersCount.flatMap { NSNumber(value: $0) }
// }
// }
//
// public func update(voted: Bool, by: MastodonUser) {
// if voted {
// if !(votedBy ?? Set()).contains(by) {
// mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(by)
// }
// } else {
// if (votedBy ?? Set()).contains(by) {
// mutableSetValue(forKey: #keyPath(Poll.votedBy)).remove(by)
// }
// }
// }
//
// public func didUpdate(at networkDate: Date) {
// self.updatedAt = networkDate
// }
//
//}
//extension Poll {
// public struct Property {
// public let id: ID
// public let expiresAt: Date?
// public let expired: Bool
// public let multiple: Bool
// public let votesCount: NSNumber
// public let votersCount: NSNumber?
//
// public let networkDate: Date
//
// public init(
// id: Poll.ID,
// expiresAt: Date?,
// expired: Bool,
// multiple: Bool,
// votesCount: Int,
// votersCount: Int?,
// networkDate: Date
// ) {
// self.id = id
// self.expiresAt = expiresAt
// self.expired = expired
// self.multiple = multiple
// self.votesCount = NSNumber(value: votesCount)
// self.votersCount = votersCount.flatMap { NSNumber(value: $0) }
// self.networkDate = networkDate
// }
// }
//}
// MARK: - AutoGenerateProperty
extension Poll: AutoGenerateProperty {
// sourcery:inline:Poll.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
public let domain: String
public let id: ID
public let expiresAt: Date?
public let expired: Bool
public let multiple: Bool
public let votesCount: Int64
public let votersCount: Int64
public let createdAt: Date
public let updatedAt: Date
public init(
domain: String,
id: ID,
expiresAt: Date?,
expired: Bool,
multiple: Bool,
votesCount: Int64,
votersCount: Int64,
createdAt: Date,
updatedAt: Date
) {
self.domain = domain
self.id = id
self.expiresAt = expiresAt
self.expired = expired
self.multiple = multiple
self.votesCount = votesCount
self.votersCount = votersCount
self.createdAt = createdAt
self.updatedAt = updatedAt
}
}
public func configure(property: Property) {
self.domain = property.domain
self.id = property.id
self.expiresAt = property.expiresAt
self.expired = property.expired
self.multiple = property.multiple
self.votesCount = property.votesCount
self.votersCount = property.votersCount
self.createdAt = property.createdAt
self.updatedAt = property.updatedAt
}
public func update(property: Property) {
update(expiresAt: property.expiresAt)
update(expired: property.expired)
update(votesCount: property.votesCount)
update(votersCount: property.votersCount)
update(updatedAt: property.updatedAt)
}
// sourcery:end
}
// MARK: - AutoUpdatableObject
extension Poll: AutoUpdatableObject {
// sourcery:inline:Poll.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
public func update(expiresAt: Date?) {
if self.expiresAt != expiresAt {
self.expiresAt = expiresAt
}
}
public func update(expired: Bool) {
if self.expired != expired {
self.expired = expired
}
}
public func update(votesCount: Int64) {
if self.votesCount != votesCount {
self.votesCount = votesCount
}
}
public func update(votersCount: Int64) {
if self.votersCount != votersCount {
self.votersCount = votersCount
}
}
public func update(updatedAt: Date) {
if self.updatedAt != updatedAt {
self.updatedAt = updatedAt
}
}
public func update(isVoting: Bool) {
if self.isVoting != isVoting {
self.isVoting = isVoting
}
}
// sourcery:end
public func update(voted: Bool, by: MastodonUser) {
if voted {
if !(votedBy ?? Set()).contains(by) {
mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(by)
}
} else {
if (votedBy ?? Set()).contains(by) {
mutableSetValue(forKey: #keyPath(Poll.votedBy)).remove(by)
}
}
}
public func attach(options: [PollOption]) {
for option in options {
guard !self.options.contains(option) else { continue }
self.mutableSetValue(forKey: #keyPath(Poll.options)).add(option)
}
}
}

View File

@ -0,0 +1,199 @@
//
// PollOption.swift
// CoreDataStack
//
// Created by MainasuK Cirno on 2021-3-2.
//
import Foundation
import CoreData
public final class PollOption: NSManagedObject {
// sourcery: autoGenerateProperty
@NSManaged public private(set) var index: Int64
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var title: String
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var votesCount: Int64
// sourcery: autoGenerateProperty
@NSManaged public private(set) var createdAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date
// sourcery: autoUpdatableObject
@NSManaged public private(set) var isSelected: Bool
// many-to-one relationship
@NSManaged public private(set) var poll: Poll
// many-to-many relationship
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
}
extension PollOption {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property
) -> PollOption {
let object: PollOption = context.insertObject()
object.configure(property: property)
return object
}
}
extension PollOption: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \PollOption.createdAt, ascending: false)]
}
}
//extension PollOption {
//
// public override func awakeFromInsert() {
// super.awakeFromInsert()
// setPrimitiveValue(Date(), forKey: #keyPath(PollOption.createdAt))
// }
//
// @discardableResult
// public static func insert(
// into context: NSManagedObjectContext,
// property: Property,
// votedBy: MastodonUser?
// ) -> PollOption {
// let option: PollOption = context.insertObject()
//
// option.index = property.index
// option.title = property.title
// option.votesCount = property.votesCount
// option.updatedAt = property.networkDate
//
// if let votedBy = votedBy {
// option.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(votedBy)
// }
//
// return option
// }
//
// public func update(votesCount: Int?) {
// if self.votesCount?.intValue != votesCount {
// self.votesCount = votesCount.flatMap { NSNumber(value: $0) }
// }
// }
//
// public func didUpdate(at networkDate: Date) {
// self.updatedAt = networkDate
// }
//
//}
//extension PollOption {
// public struct Property {
// public let index: NSNumber
// public let title: String
// public let votesCount: NSNumber?
//
// public let networkDate: Date
//
// public init(index: Int, title: String, votesCount: Int?, networkDate: Date) {
// self.index = NSNumber(value: index)
// self.title = title
// self.votesCount = votesCount.flatMap { NSNumber(value: $0) }
// self.networkDate = networkDate
// }
// }
//}
//
// MARK: - AutoGenerateProperty
extension PollOption: AutoGenerateProperty {
// sourcery:inline:PollOption.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
public let index: Int64
public let title: String
public let votesCount: Int64
public let createdAt: Date
public let updatedAt: Date
public init(
index: Int64,
title: String,
votesCount: Int64,
createdAt: Date,
updatedAt: Date
) {
self.index = index
self.title = title
self.votesCount = votesCount
self.createdAt = createdAt
self.updatedAt = updatedAt
}
}
public func configure(property: Property) {
self.index = property.index
self.title = property.title
self.votesCount = property.votesCount
self.createdAt = property.createdAt
self.updatedAt = property.updatedAt
}
public func update(property: Property) {
update(title: property.title)
update(votesCount: property.votesCount)
update(updatedAt: property.updatedAt)
}
// sourcery:end
}
// MARK: - AutoUpdatableObject
extension PollOption: AutoUpdatableObject {
// sourcery:inline:PollOption.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
public func update(title: String) {
if self.title != title {
self.title = title
}
}
public func update(votesCount: Int64) {
if self.votesCount != votesCount {
self.votesCount = votesCount
}
}
public func update(updatedAt: Date) {
if self.updatedAt != updatedAt {
self.updatedAt = updatedAt
}
}
public func update(isSelected: Bool) {
if self.isSelected != isSelected {
self.isSelected = isSelected
}
}
// sourcery:end
public func update(voted: Bool, by: MastodonUser) {
if voted {
if !(self.votedBy ?? Set()).contains(by) {
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(by)
}
} else {
if (self.votedBy ?? Set()).contains(by) {
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).remove(by)
}
}
}
}

View File

@ -0,0 +1,158 @@
//
// SearchHistory.swift
// CoreDataStack
//
// Created by sxiaojian on 2021/4/7.
//
import Foundation
import CoreData
public final class SearchHistory: NSManagedObject {
public typealias ID = UUID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var identifier: ID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var domain: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var userID: MastodonUser.ID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var createAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date
// many-to-one relationship
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var account: MastodonUser?
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var hashtag: Tag?
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var status: Status?
}
extension SearchHistory {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
relationship: Relationship
) -> SearchHistory {
let object: SearchHistory = context.insertObject()
object.configure(property: property)
object.configure(relationship: relationship)
return object
}
}
extension SearchHistory: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \SearchHistory.updatedAt, ascending: false)]
}
}
extension SearchHistory {
static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.domain), domain)
}
static func predicate(userID: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.userID), userID)
}
public static func predicate(domain: String, userID: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(userID: userID)
])
}
}
// MARK: - AutoGenerateProperty
extension SearchHistory: AutoGenerateProperty {
// sourcery:inline:SearchHistory.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
public let identifier: ID
public let domain: String
public let userID: MastodonUser.ID
public let createAt: Date
public let updatedAt: Date
public init(
identifier: ID,
domain: String,
userID: MastodonUser.ID,
createAt: Date,
updatedAt: Date
) {
self.identifier = identifier
self.domain = domain
self.userID = userID
self.createAt = createAt
self.updatedAt = updatedAt
}
}
public func configure(property: Property) {
self.identifier = property.identifier
self.domain = property.domain
self.userID = property.userID
self.createAt = property.createAt
self.updatedAt = property.updatedAt
}
public func update(property: Property) {
update(updatedAt: property.updatedAt)
}
// sourcery:end
}
// MARK: - AutoGenerateRelationship
extension SearchHistory: AutoGenerateRelationship {
// sourcery:inline:SearchHistory.AutoGenerateRelationship
// Generated using Sourcery
// DO NOT EDIT
public struct Relationship {
public let account: MastodonUser?
public let hashtag: Tag?
public let status: Status?
public init(
account: MastodonUser?,
hashtag: Tag?,
status: Status?
) {
self.account = account
self.hashtag = hashtag
self.status = status
}
}
public func configure(relationship: Relationship) {
self.account = relationship.account
self.hashtag = relationship.hashtag
self.status = relationship.status
}
// sourcery:end
}
// MARK: - AutoUpdatableObject
extension SearchHistory: AutoUpdatableObject {
// sourcery:inline:SearchHistory.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
public func update(updatedAt: Date) {
if self.updatedAt != updatedAt {
self.updatedAt = updatedAt
}
}
// sourcery:end
}

View File

@ -0,0 +1,802 @@
//
// Status.swift
// CoreDataStack
//
// Created by MainasuK Cirno on 2021/1/27.
//
import CoreData
import Foundation
public final class Status: NSManagedObject {
public typealias ID = String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var identifier: ID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var domain: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var id: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var uri: String
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var createdAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var content: String
@NSManaged public private(set) var visibilityRaw: String
// sourcery: autoUpdatableObject, autoGenerateProperty
public var visibility: MastodonVisibility {
get {
let rawValue = visibilityRaw
return MastodonVisibility(rawValue: rawValue) ?? ._other(rawValue)
}
set {
visibilityRaw = newValue.rawValue
}
}
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var sensitive: Bool
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var spoilerText: String?
@NSManaged public private(set) var application: Application?
// Informational
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var reblogsCount: Int64
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var favouritesCount: Int64
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var repliesCount: Int64
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var url: String?
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var inReplyToID: Status.ID?
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var inReplyToAccountID: MastodonUser.ID?
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var language: String? // (ISO 639 Part 1 two-letter language code)
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var text: String?
// many-to-one relationship
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var author: MastodonUser
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var reblog: Status?
// sourcery: autoUpdatableObject
@NSManaged public private(set) var replyTo: Status?
// many-to-many relationship
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>
@NSManaged public private(set) var rebloggedBy: Set<MastodonUser>
@NSManaged public private(set) var mutedBy: Set<MastodonUser>
@NSManaged public private(set) var bookmarkedBy: Set<MastodonUser>
// one-to-one relationship
@NSManaged public private(set) var pinnedBy: MastodonUser?
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var poll: Poll?
// one-to-many relationship
@NSManaged public private(set) var feeds: Set<Feed>
@NSManaged public private(set) var reblogFrom: Set<Status>
// @NSManaged public private(set) var mentions: Set<Mention>?
// @NSManaged public private(set) var homeTimelineIndexes: Set<HomeTimelineIndex>?
// @NSManaged public private(set) var mediaAttachments: Set<Attachment>?
@NSManaged public private(set) var replyFrom: Set<Status>
@NSManaged public private(set) var notifications: Set<Notification>
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var deletedAt: Date?
// sourcery: autoUpdatableObject
@NSManaged public private(set) var revealedAt: Date?
}
extension Status {
// sourcery: autoUpdatableObject, autoGenerateProperty
@objc public var attachments: [MastodonAttachment] {
get {
let keyPath = #keyPath(Status.attachments)
willAccessValue(forKey: keyPath)
let _data = primitiveValue(forKey: keyPath) as? Data
didAccessValue(forKey: keyPath)
do {
guard let data = _data else { return [] }
let attachments = try JSONDecoder().decode([MastodonAttachment].self, from: data)
return attachments
} catch {
assertionFailure(error.localizedDescription)
return []
}
}
set {
let keyPath = #keyPath(Status.attachments)
let data = try? JSONEncoder().encode(newValue)
willChangeValue(forKey: keyPath)
setPrimitiveValue(data, forKey: keyPath)
didChangeValue(forKey: keyPath)
}
}
// sourcery: autoUpdatableObject, autoGenerateProperty
@objc public var emojis: [MastodonEmoji] {
get {
let keyPath = #keyPath(Status.emojis)
willAccessValue(forKey: keyPath)
let _data = primitiveValue(forKey: keyPath) as? Data
didAccessValue(forKey: keyPath)
do {
guard let data = _data else { return [] }
let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data)
return emojis
} catch {
assertionFailure(error.localizedDescription)
return []
}
}
set {
let keyPath = #keyPath(Status.emojis)
let data = try? JSONEncoder().encode(newValue)
willChangeValue(forKey: keyPath)
setPrimitiveValue(data, forKey: keyPath)
didChangeValue(forKey: keyPath)
}
}
// sourcery: autoUpdatableObject, autoGenerateProperty
@objc public var mentions: [MastodonMention] {
get {
let keyPath = #keyPath(Status.mentions)
willAccessValue(forKey: keyPath)
let _data = primitiveValue(forKey: keyPath) as? Data
didAccessValue(forKey: keyPath)
do {
guard let data = _data else { return [] }
let emojis = try JSONDecoder().decode([MastodonMention].self, from: data)
return emojis
} catch {
assertionFailure(error.localizedDescription)
return []
}
}
set {
let keyPath = #keyPath(Status.mentions)
let data = try? JSONEncoder().encode(newValue)
willChangeValue(forKey: keyPath)
setPrimitiveValue(data, forKey: keyPath)
didChangeValue(forKey: keyPath)
}
}
}
extension Status: FeedIndexable { }
extension Status {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
relationship: Relationship
) -> Status {
let object: Status = context.insertObject()
object.configure(property: property)
object.configure(relationship: relationship)
return object
}
// @discardableResult
// public static func insert(
// into context: NSManagedObjectContext,
// property: Property,
// author: MastodonUser,
// reblog: Status?,
// application: Application?,
// replyTo: Status?,
// poll: Poll?,
// mentions: [Mention]?,
// mediaAttachments: [Attachment]?,
// favouritedBy: MastodonUser?,
// rebloggedBy: MastodonUser?,
// mutedBy: MastodonUser?,
// bookmarkedBy: MastodonUser?,
// pinnedBy: MastodonUser?
// ) -> Status {
// let status: Status = context.insertObject()
//
// status.identifier = property.identifier
// status.domain = property.domain
//
// status.id = property.id
// status.uri = property.uri
// status.createdAt = property.createdAt
// status.content = property.content
//
// status.visibility = property.visibility
// status.sensitive = property.sensitive
// status.spoilerText = property.spoilerText
// status.application = application
//
// status.emojisData = property.emojisData
//
// status.reblogsCount = property.reblogsCount
// status.favouritesCount = property.favouritesCount
// status.repliesCount = property.repliesCount
//
// status.url = property.url
// status.inReplyToID = property.inReplyToID
// status.inReplyToAccountID = property.inReplyToAccountID
//
// status.language = property.language
// status.text = property.text
//
// status.author = author
// status.reblog = reblog
//
// status.pinnedBy = pinnedBy
// status.poll = poll
//
// if let mentions = mentions {
// status.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions)
// }
// if let mediaAttachments = mediaAttachments {
// status.mutableSetValue(forKey: #keyPath(Status.mediaAttachments)).addObjects(from: mediaAttachments)
// }
// if let favouritedBy = favouritedBy {
// status.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(favouritedBy)
// }
// if let rebloggedBy = rebloggedBy {
// status.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(rebloggedBy)
// }
// if let mutedBy = mutedBy {
// status.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mutedBy)
// }
// if let bookmarkedBy = bookmarkedBy {
// status.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(bookmarkedBy)
// }
//
// status.updatedAt = property.networkDate
//
// return status
// }
//
// public func update(emojisData: Data?) {
// if self.emojisData != emojisData {
// self.emojisData = emojisData
// }
// }
//
// public func update(reblogsCount: NSNumber) {
// if self.reblogsCount.intValue != reblogsCount.intValue {
// self.reblogsCount = reblogsCount
// }
// }
//
// public func update(favouritesCount: NSNumber) {
// if self.favouritesCount.intValue != favouritesCount.intValue {
// self.favouritesCount = favouritesCount
// }
// }
//
// public func update(repliesCount: NSNumber?) {
// guard let count = repliesCount else {
// return
// }
// if self.repliesCount?.intValue != count.intValue {
// self.repliesCount = repliesCount
// }
// }
//
// public func update(replyTo: Status?) {
// if self.replyTo != replyTo {
// self.replyTo = replyTo
// }
// }
//
// public func update(liked: Bool, by mastodonUser: MastodonUser) {
// if liked {
// if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(mastodonUser)
// }
// } else {
// if (self.favouritedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).remove(mastodonUser)
// }
// }
// }
//
// public func update(reblogged: Bool, by mastodonUser: MastodonUser) {
// if reblogged {
// if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(mastodonUser)
// }
// } else {
// if (self.rebloggedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).remove(mastodonUser)
// }
// }
// }
//
// public func update(muted: Bool, by mastodonUser: MastodonUser) {
// if muted {
// if !(self.mutedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mastodonUser)
// }
// } else {
// if (self.mutedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).remove(mastodonUser)
// }
// }
// }
//
// public func update(bookmarked: Bool, by mastodonUser: MastodonUser) {
// if bookmarked {
// if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(mastodonUser)
// }
// } else {
// if (self.bookmarkedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).remove(mastodonUser)
// }
// }
// }
//
// public func didUpdate(at networkDate: Date) {
// self.updatedAt = networkDate
// }
}
extension Status: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \Status.createdAt, ascending: false)]
}
}
extension Status {
static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Status.domain), domain)
}
static func predicate(id: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Status.id), id)
}
public static func predicate(domain: String, id: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(id: id)
])
}
static func predicate(ids: [String]) -> NSPredicate {
return NSPredicate(format: "%K IN %@", #keyPath(Status.id), ids)
}
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(ids: ids)
])
}
public static func notDeleted() -> NSPredicate {
return NSPredicate(format: "%K == nil", #keyPath(Status.deletedAt))
}
public static func deleted() -> NSPredicate {
return NSPredicate(format: "%K != nil", #keyPath(Status.deletedAt))
}
}
// MARK: - AutoGenerateProperty
extension Status: AutoGenerateProperty {
// sourcery:inline:Status.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
public let identifier: ID
public let domain: String
public let id: String
public let uri: String
public let createdAt: Date
public let content: String
public let visibility: MastodonVisibility
public let sensitive: Bool
public let spoilerText: String?
public let reblogsCount: Int64
public let favouritesCount: Int64
public let repliesCount: Int64
public let url: String?
public let inReplyToID: Status.ID?
public let inReplyToAccountID: MastodonUser.ID?
public let language: String?
public let text: String?
public let updatedAt: Date
public let deletedAt: Date?
public let attachments: [MastodonAttachment]
public let emojis: [MastodonEmoji]
public let mentions: [MastodonMention]
public init(
identifier: ID,
domain: String,
id: String,
uri: String,
createdAt: Date,
content: String,
visibility: MastodonVisibility,
sensitive: Bool,
spoilerText: String?,
reblogsCount: Int64,
favouritesCount: Int64,
repliesCount: Int64,
url: String?,
inReplyToID: Status.ID?,
inReplyToAccountID: MastodonUser.ID?,
language: String?,
text: String?,
updatedAt: Date,
deletedAt: Date?,
attachments: [MastodonAttachment],
emojis: [MastodonEmoji],
mentions: [MastodonMention]
) {
self.identifier = identifier
self.domain = domain
self.id = id
self.uri = uri
self.createdAt = createdAt
self.content = content
self.visibility = visibility
self.sensitive = sensitive
self.spoilerText = spoilerText
self.reblogsCount = reblogsCount
self.favouritesCount = favouritesCount
self.repliesCount = repliesCount
self.url = url
self.inReplyToID = inReplyToID
self.inReplyToAccountID = inReplyToAccountID
self.language = language
self.text = text
self.updatedAt = updatedAt
self.deletedAt = deletedAt
self.attachments = attachments
self.emojis = emojis
self.mentions = mentions
}
}
public func configure(property: Property) {
self.identifier = property.identifier
self.domain = property.domain
self.id = property.id
self.uri = property.uri
self.createdAt = property.createdAt
self.content = property.content
self.visibility = property.visibility
self.sensitive = property.sensitive
self.spoilerText = property.spoilerText
self.reblogsCount = property.reblogsCount
self.favouritesCount = property.favouritesCount
self.repliesCount = property.repliesCount
self.url = property.url
self.inReplyToID = property.inReplyToID
self.inReplyToAccountID = property.inReplyToAccountID
self.language = property.language
self.text = property.text
self.updatedAt = property.updatedAt
self.deletedAt = property.deletedAt
self.attachments = property.attachments
self.emojis = property.emojis
self.mentions = property.mentions
}
public func update(property: Property) {
update(createdAt: property.createdAt)
update(content: property.content)
update(visibility: property.visibility)
update(sensitive: property.sensitive)
update(spoilerText: property.spoilerText)
update(reblogsCount: property.reblogsCount)
update(favouritesCount: property.favouritesCount)
update(repliesCount: property.repliesCount)
update(url: property.url)
update(inReplyToID: property.inReplyToID)
update(inReplyToAccountID: property.inReplyToAccountID)
update(language: property.language)
update(text: property.text)
update(updatedAt: property.updatedAt)
update(deletedAt: property.deletedAt)
update(attachments: property.attachments)
update(emojis: property.emojis)
update(mentions: property.mentions)
}
// sourcery:end
}
// MARK: - AutoGenerateRelationship
extension Status: AutoGenerateRelationship {
// sourcery:inline:Status.AutoGenerateRelationship
// Generated using Sourcery
// DO NOT EDIT
public struct Relationship {
public let author: MastodonUser
public let reblog: Status?
public let poll: Poll?
public init(
author: MastodonUser,
reblog: Status?,
poll: Poll?
) {
self.author = author
self.reblog = reblog
self.poll = poll
}
}
public func configure(relationship: Relationship) {
self.author = relationship.author
self.reblog = relationship.reblog
self.poll = relationship.poll
}
// sourcery:end
}
// MARK: - AutoUpdatableObject
extension Status: AutoUpdatableObject {
// sourcery:inline:Status.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
public func update(createdAt: Date) {
if self.createdAt != createdAt {
self.createdAt = createdAt
}
}
public func update(content: String) {
if self.content != content {
self.content = content
}
}
public func update(visibility: MastodonVisibility) {
if self.visibility != visibility {
self.visibility = visibility
}
}
public func update(sensitive: Bool) {
if self.sensitive != sensitive {
self.sensitive = sensitive
}
}
public func update(spoilerText: String?) {
if self.spoilerText != spoilerText {
self.spoilerText = spoilerText
}
}
public func update(reblogsCount: Int64) {
if self.reblogsCount != reblogsCount {
self.reblogsCount = reblogsCount
}
}
public func update(favouritesCount: Int64) {
if self.favouritesCount != favouritesCount {
self.favouritesCount = favouritesCount
}
}
public func update(repliesCount: Int64) {
if self.repliesCount != repliesCount {
self.repliesCount = repliesCount
}
}
public func update(url: String?) {
if self.url != url {
self.url = url
}
}
public func update(inReplyToID: Status.ID?) {
if self.inReplyToID != inReplyToID {
self.inReplyToID = inReplyToID
}
}
public func update(inReplyToAccountID: MastodonUser.ID?) {
if self.inReplyToAccountID != inReplyToAccountID {
self.inReplyToAccountID = inReplyToAccountID
}
}
public func update(language: String?) {
if self.language != language {
self.language = language
}
}
public func update(text: String?) {
if self.text != text {
self.text = text
}
}
public func update(replyTo: Status?) {
if self.replyTo != replyTo {
self.replyTo = replyTo
}
}
public func update(updatedAt: Date) {
if self.updatedAt != updatedAt {
self.updatedAt = updatedAt
}
}
public func update(deletedAt: Date?) {
if self.deletedAt != deletedAt {
self.deletedAt = deletedAt
}
}
public func update(revealedAt: Date?) {
if self.revealedAt != revealedAt {
self.revealedAt = revealedAt
}
}
public func update(attachments: [MastodonAttachment]) {
if self.attachments != attachments {
self.attachments = attachments
}
}
public func update(emojis: [MastodonEmoji]) {
if self.emojis != emojis {
self.emojis = emojis
}
}
public func update(mentions: [MastodonMention]) {
if self.mentions != mentions {
self.mentions = mentions
}
}
// sourcery:end
public func update(liked: Bool, by mastodonUser: MastodonUser) {
if liked {
if !self.favouritedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(mastodonUser)
}
} else {
if self.favouritedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).remove(mastodonUser)
}
}
}
public func update(reblogged: Bool, by mastodonUser: MastodonUser) {
if reblogged {
if !self.rebloggedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(mastodonUser)
}
} else {
if self.rebloggedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).remove(mastodonUser)
}
}
}
public func update(muted: Bool, by mastodonUser: MastodonUser) {
if muted {
if !self.mutedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mastodonUser)
}
} else {
if self.mutedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).remove(mastodonUser)
}
}
}
public func update(bookmarked: Bool, by mastodonUser: MastodonUser) {
if bookmarked {
if !self.bookmarkedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(mastodonUser)
}
} else {
if self.bookmarkedBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).remove(mastodonUser)
}
}
}
public func update(isReveal: Bool) {
revealedAt = isReveal ? Date() : nil
}
}
extension Status {
public func attach(feed: Feed) {
mutableSetValue(forKey: #keyPath(Status.feeds)).add(feed)
}
}
//extension Status {
// public struct Property {
//
// public let identifier: ID
// public let domain: String
//
// public let id: String
// public let uri: String
// public let createdAt: Date
// public let content: String
//
// public let visibility: String?
// public let sensitive: Bool
// public let spoilerText: String?
//
// public let emojisData: Data?
//
// public let reblogsCount: NSNumber
// public let favouritesCount: NSNumber
// public let repliesCount: NSNumber?
//
// public let url: String?
// public let inReplyToID: Status.ID?
// public let inReplyToAccountID: MastodonUser.ID?
// public let language: String? // (ISO 639 Part @1 two-letter language code)
// public let text: String?
//
// public let networkDate: Date
//
// public init(
// domain: String,
// id: String,
// uri: String,
// createdAt: Date,
// content: String,
// visibility: String?,
// sensitive: Bool,
// spoilerText: String?,
// emojisData: Data?,
// reblogsCount: NSNumber,
// favouritesCount: NSNumber,
// repliesCount: NSNumber?,
// url: String?,
// inReplyToID: Status.ID?,
// inReplyToAccountID: MastodonUser.ID?,
// language: String?,
// text: String?,
// networkDate: Date
// ) {
// self.identifier = id + "@" + domain
// self.domain = domain
// self.id = id
// self.uri = uri
// self.createdAt = createdAt
// self.content = content
// self.visibility = visibility
// self.sensitive = sensitive
// self.spoilerText = spoilerText
// self.emojisData = emojisData
// self.reblogsCount = reblogsCount
// self.favouritesCount = favouritesCount
// self.repliesCount = repliesCount
// self.url = url
// self.inReplyToID = inReplyToID
// self.inReplyToAccountID = inReplyToAccountID
// self.language = language
// self.text = text
// self.networkDate = networkDate
// }
//
// }
//}
//

View File

@ -0,0 +1,218 @@
//
// Tag.swift
// CoreDataStack
//
// Created by sxiaojian on 2021/2/1.
//
import CoreData
import Foundation
public final class Tag: NSManagedObject {
public typealias ID = UUID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var identifier: ID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var domain: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var createAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date
// sourcery: autoGenerateProperty
@NSManaged public private(set) var name: String
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var url: String
// one-to-one relationship
// many-to-many relationship
// one-to-many relationship
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
}
extension Tag {
// sourcery: autoUpdatableObject, autoGenerateProperty
@objc public var histories: [MastodonTagHistory] {
get {
let keyPath = #keyPath(Tag.histories)
willAccessValue(forKey: keyPath)
let _data = primitiveValue(forKey: keyPath) as? Data
didAccessValue(forKey: keyPath)
do {
guard let data = _data else { return [] }
let attachments = try JSONDecoder().decode([MastodonTagHistory].self, from: data)
return attachments
} catch {
assertionFailure(error.localizedDescription)
return []
}
}
set {
let keyPath = #keyPath(Tag.histories)
let data = try? JSONEncoder().encode(newValue)
willChangeValue(forKey: keyPath)
setPrimitiveValue(data, forKey: keyPath)
didChangeValue(forKey: keyPath)
}
}
}
extension Tag {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property
) -> Tag {
let object: Tag = context.insertObject()
object.configure(property: property)
return object
}
}
extension Tag: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
[NSSortDescriptor(keyPath: \Tag.createAt, ascending: false)]
}
}
public extension Tag {
static func predicate(domain: String) -> NSPredicate {
NSPredicate(format: "%K == %@", #keyPath(Tag.domain), domain)
}
static func predicate(name: String) -> NSPredicate {
NSPredicate(format: "%K == %@", #keyPath(Tag.name), name)
}
static func predicate(domain: String, name: String) -> NSPredicate {
NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(name: name),
])
}
}
// MARK: - AutoGenerateProperty
extension Tag: AutoGenerateProperty {
// sourcery:inline:Tag.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
public let identifier: ID
public let domain: String
public let createAt: Date
public let updatedAt: Date
public let name: String
public let url: String
public let histories: [MastodonTagHistory]
public init(
identifier: ID,
domain: String,
createAt: Date,
updatedAt: Date,
name: String,
url: String,
histories: [MastodonTagHistory]
) {
self.identifier = identifier
self.domain = domain
self.createAt = createAt
self.updatedAt = updatedAt
self.name = name
self.url = url
self.histories = histories
}
}
public func configure(property: Property) {
self.identifier = property.identifier
self.domain = property.domain
self.createAt = property.createAt
self.updatedAt = property.updatedAt
self.name = property.name
self.url = property.url
self.histories = property.histories
}
public func update(property: Property) {
update(updatedAt: property.updatedAt)
update(url: property.url)
update(histories: property.histories)
}
// sourcery:end
}
// MARK: - AutoUpdatableObject
extension Tag: AutoUpdatableObject {
// sourcery:inline:Tag.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
public func update(updatedAt: Date) {
if self.updatedAt != updatedAt {
self.updatedAt = updatedAt
}
}
public func update(url: String) {
if self.url != url {
self.url = url
}
}
public func update(histories: [MastodonTagHistory]) {
if self.histories != histories {
self.histories = histories
}
}
// sourcery:end
}
extension Tag {
public func findSearchHistory(domain: String, userID: MastodonUser.ID) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == domain
&& searchHistory.userID == userID
}
}
public func findSearchHistory(for user: MastodonUser) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == user.domain
&& searchHistory.userID == user.id
}
}
}
public extension Tag {
// func updateHistory(index: Int, day: Date, uses: String, account: String) {
// let histories = self.histories.sorted {
// $0.createAt.compare($1.createAt) == .orderedAscending
// }
// guard index < histories.count else { return }
// let history = histories[index]
// history.update(day: day)
// history.update(uses: uses)
// history.update(accounts: account)
// }
//
// func appendHistory(history: History) {
// self.mutableSetValue(forKeyPath: #keyPath(Tag.histories)).add(history)
// }
//
// func update(url: String) {
// if self.url != url {
// self.url = url
// }
// }
}

View File

@ -1,407 +0,0 @@
//
// MastodonUser.swift
// CoreDataStack
//
// Created by MainasuK Cirno on 2021/1/27.
//
import CoreData
import Foundation
final public class MastodonUser: NSManagedObject {
public typealias ID = String
@NSManaged public private(set) var identifier: ID
@NSManaged public private(set) var domain: String
@NSManaged public private(set) var id: ID
@NSManaged public private(set) var acct: String
@NSManaged public private(set) var username: String
@NSManaged public private(set) var displayName: String
@NSManaged public private(set) var avatar: String
@NSManaged public private(set) var avatarStatic: String?
@NSManaged public private(set) var header: String
@NSManaged public private(set) var headerStatic: String?
@NSManaged public private(set) var note: String?
@NSManaged public private(set) var url: String?
@NSManaged public private(set) var emojisData: Data?
@NSManaged public private(set) var fieldsData: Data?
@NSManaged public private(set) var statusesCount: NSNumber
@NSManaged public private(set) var followingCount: NSNumber
@NSManaged public private(set) var followersCount: NSNumber
@NSManaged public private(set) var locked: Bool
@NSManaged public private(set) var bot: Bool
@NSManaged public private(set) var suspended: Bool
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
// one-to-one relationship
@NSManaged public private(set) var pinnedStatus: Status?
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
// one-to-many relationship
@NSManaged public private(set) var statuses: Set<Status>?
@NSManaged public private(set) var notifications: Set<MastodonNotification>?
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
// many-to-many relationship
@NSManaged public private(set) var favourite: Set<Status>?
@NSManaged public private(set) var reblogged: Set<Status>?
@NSManaged public private(set) var muted: Set<Status>?
@NSManaged public private(set) var bookmarked: Set<Status>?
@NSManaged public private(set) var votePollOptions: Set<PollOption>?
@NSManaged public private(set) var votePolls: Set<Poll>?
// relationships
@NSManaged public private(set) var following: Set<MastodonUser>?
@NSManaged public private(set) var followingBy: Set<MastodonUser>?
@NSManaged public private(set) var followRequested: Set<MastodonUser>?
@NSManaged public private(set) var followRequestedBy: Set<MastodonUser>?
@NSManaged public private(set) var muting: Set<MastodonUser>?
@NSManaged public private(set) var mutingBy: Set<MastodonUser>?
@NSManaged public private(set) var blocking: Set<MastodonUser>?
@NSManaged public private(set) var blockingBy: Set<MastodonUser>?
@NSManaged public private(set) var endorsed: Set<MastodonUser>?
@NSManaged public private(set) var endorsedBy: Set<MastodonUser>?
@NSManaged public private(set) var domainBlocking: Set<MastodonUser>?
@NSManaged public private(set) var domainBlockingBy: Set<MastodonUser>?
}
extension MastodonUser {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property
) -> MastodonUser {
let user: MastodonUser = context.insertObject()
user.identifier = property.identifier
user.domain = property.domain
user.id = property.id
user.acct = property.acct
user.username = property.username
user.displayName = property.displayName
user.avatar = property.avatar
user.avatarStatic = property.avatarStatic
user.header = property.header
user.headerStatic = property.headerStatic
user.note = property.note
user.url = property.url
user.emojisData = property.emojisData
user.fieldsData = property.fieldsData
user.statusesCount = NSNumber(value: property.statusesCount)
user.followingCount = NSNumber(value: property.followingCount)
user.followersCount = NSNumber(value: property.followersCount)
user.locked = property.locked
user.bot = property.bot ?? false
user.suspended = property.suspended ?? false
// Mastodon do not provide relationship on the `Account`
// Update relationship via attribute updating interface
user.createdAt = property.createdAt
user.updatedAt = property.networkDate
return user
}
public func update(acct: String) {
if self.acct != acct {
self.acct = acct
}
}
public func update(username: String) {
if self.username != username {
self.username = username
}
}
public func update(displayName: String) {
if self.displayName != displayName {
self.displayName = displayName
}
}
public func update(avatar: String) {
if self.avatar != avatar {
self.avatar = avatar
}
}
public func update(avatarStatic: String?) {
if self.avatarStatic != avatarStatic {
self.avatarStatic = avatarStatic
}
}
public func update(header: String) {
if self.header != header {
self.header = header
}
}
public func update(headerStatic: String?) {
if self.headerStatic != headerStatic {
self.headerStatic = headerStatic
}
}
public func update(note: String?) {
if self.note != note {
self.note = note
}
}
public func update(url: String?) {
if self.url != url {
self.url = url
}
}
public func update(emojisData: Data?) {
if self.emojisData != emojisData {
self.emojisData = emojisData
}
}
public func update(fieldsData: Data?) {
if self.fieldsData != fieldsData {
self.fieldsData = fieldsData
}
}
public func update(statusesCount: Int) {
if self.statusesCount.intValue != statusesCount {
self.statusesCount = NSNumber(value: statusesCount)
}
}
public func update(followingCount: Int) {
if self.followingCount.intValue != followingCount {
self.followingCount = NSNumber(value: followingCount)
}
}
public func update(followersCount: Int) {
if self.followersCount.intValue != followersCount {
self.followersCount = NSNumber(value: followersCount)
}
}
public func update(locked: Bool) {
if self.locked != locked {
self.locked = locked
}
}
public func update(bot: Bool) {
if self.bot != bot {
self.bot = bot
}
}
public func update(suspended: Bool) {
if self.suspended != suspended {
self.suspended = suspended
}
}
public func update(isFollowing: Bool, by mastodonUser: MastodonUser) {
if isFollowing {
if !(self.followingBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).add(mastodonUser)
}
} else {
if (self.followingBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).remove(mastodonUser)
}
}
}
public func update(isFollowRequested: Bool, by mastodonUser: MastodonUser) {
if isFollowRequested {
if !(self.followRequestedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).add(mastodonUser)
}
} else {
if (self.followRequestedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).remove(mastodonUser)
}
}
}
public func update(isMuting: Bool, by mastodonUser: MastodonUser) {
if isMuting {
if !(self.mutingBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).add(mastodonUser)
}
} else {
if (self.mutingBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).remove(mastodonUser)
}
}
}
public func update(isBlocking: Bool, by mastodonUser: MastodonUser) {
if isBlocking {
if !(self.blockingBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).add(mastodonUser)
}
} else {
if (self.blockingBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).remove(mastodonUser)
}
}
}
public func update(isEndorsed: Bool, by mastodonUser: MastodonUser) {
if isEndorsed {
if !(self.endorsedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).add(mastodonUser)
}
} else {
if (self.endorsedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).remove(mastodonUser)
}
}
}
public func update(isDomainBlocking: Bool, by mastodonUser: MastodonUser) {
if isDomainBlocking {
if !(self.domainBlockingBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).add(mastodonUser)
}
} else {
if (self.domainBlockingBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).remove(mastodonUser)
}
}
}
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}
}
extension MastodonUser {
public func findSearchHistory(domain: String, userID: MastodonUser.ID) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == domain
&& searchHistory.userID == userID
}
}
}
extension MastodonUser {
public struct Property {
public let identifier: String
public let domain: String
public let id: String
public let acct: String
public let username: String
public let displayName: String
public let avatar: String
public let avatarStatic: String?
public let header: String
public let headerStatic: String?
public let note: String?
public let url: String?
public let emojisData: Data?
public let fieldsData: Data?
public let statusesCount: Int
public let followingCount: Int
public let followersCount: Int
public let locked: Bool
public let bot: Bool?
public let suspended: Bool?
public let createdAt: Date
public let networkDate: Date
public init(
id: String,
domain: String,
acct: String,
username: String,
displayName: String,
avatar: String,
avatarStatic: String?,
header: String,
headerStatic: String?,
note: String?,
url: String?,
emojisData: Data?,
fieldsData: Data?,
statusesCount: Int,
followingCount: Int,
followersCount: Int,
locked: Bool,
bot: Bool?,
suspended: Bool?,
createdAt: Date,
networkDate: Date
) {
self.identifier = id + "@" + domain
self.domain = domain
self.id = id
self.acct = acct
self.username = username
self.displayName = displayName
self.avatar = avatar
self.avatarStatic = avatarStatic
self.header = header
self.headerStatic = headerStatic
self.note = note
self.url = url
self.emojisData = emojisData
self.fieldsData = fieldsData
self.statusesCount = statusesCount
self.followingCount = followingCount
self.followersCount = followersCount
self.locked = locked
self.bot = bot
self.suspended = suspended
self.createdAt = createdAt
self.networkDate = networkDate
}
}
}
extension MastodonUser: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \MastodonUser.createdAt, ascending: false)]
}
}
extension MastodonUser {
static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.domain), domain)
}
static func predicate(id: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.id), id)
}
public static func predicate(domain: String, id: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonUser.predicate(domain: domain),
MastodonUser.predicate(id: id)
])
}
static func predicate(ids: [String]) -> NSPredicate {
return NSPredicate(format: "%K IN %@", #keyPath(MastodonUser.id), ids)
}
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonUser.predicate(domain: domain),
MastodonUser.predicate(ids: ids)
])
}
static func predicate(username: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.username), username)
}
public static func predicate(domain: String, username: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonUser.predicate(domain: domain),
MastodonUser.predicate(username: username)
])
}
}

View File

@ -1,71 +0,0 @@
//
// Mention.swift
// CoreDataStack
//
// Created by sxiaojian on 2021/2/1.
//
import CoreData
import Foundation
public final class Mention: NSManagedObject {
public typealias ID = UUID
@NSManaged public private(set) var index: NSNumber
@NSManaged public private(set) var identifier: ID
@NSManaged public private(set) var id: String
@NSManaged public private(set) var createAt: Date
@NSManaged public private(set) var username: String
@NSManaged public private(set) var acct: String
@NSManaged public private(set) var url: String
// many-to-one relationship
@NSManaged public private(set) var status: Status
}
public extension Mention {
override func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(UUID(), forKey: #keyPath(Mention.identifier))
}
@discardableResult
static func insert(
into context: NSManagedObjectContext,
property: Property,
index: Int
) -> Mention {
let mention: Mention = context.insertObject()
mention.index = NSNumber(value: index)
mention.id = property.id
mention.username = property.username
mention.acct = property.acct
mention.url = property.url
return mention
}
}
public extension Mention {
struct Property {
public let id: String
public let username: String
public let acct: String
public let url: String
public init(id: String, username: String, acct: String, url: String) {
self.id = id
self.username = username
self.acct = acct
self.url = url
}
}
}
extension Mention: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \Mention.createAt, ascending: false)]
}
}

View File

@ -1,115 +0,0 @@
//
// 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 createAt: Date
@NSManaged public private(set) var updatedAt: Date
@NSManaged public private(set) var typeRaw: String
@NSManaged public private(set) var account: MastodonUser
@NSManaged public private(set) var status: Status?
@NSManaged public private(set) var domain: String
@NSManaged public private(set) var userID: String
}
extension MastodonNotification {
public override func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(UUID(), forKey: #keyPath(MastodonNotification.identifier))
}
}
public extension MastodonNotification {
@discardableResult
static func insert(
into context: NSManagedObjectContext,
domain: String,
userID: String,
networkDate: Date,
property: Property
) -> MastodonNotification {
let notification: MastodonNotification = context.insertObject()
notification.id = property.id
notification.createAt = property.createdAt
notification.updatedAt = networkDate
notification.typeRaw = property.typeRaw
notification.account = property.account
notification.status = property.status
notification.domain = domain
notification.userID = userID
return notification
}
}
public extension MastodonNotification {
struct Property {
public init(id: String,
typeRaw: String,
account: MastodonUser,
status: Status?,
createdAt: Date
) {
self.id = id
self.typeRaw = typeRaw
self.account = account
self.status = status
self.createdAt = createdAt
}
public let id: String
public let typeRaw: 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(userID: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.userID), userID)
}
static func predicate(typeRaw: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.typeRaw), typeRaw)
}
public static func predicate(domain: String, userID: String, typeRaw: String? = nil) -> NSPredicate {
if let typeRaw = typeRaw {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonNotification.predicate(domain: domain),
MastodonNotification.predicate(typeRaw: typeRaw),
MastodonNotification.predicate(userID: userID),
])
} else {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonNotification.predicate(domain: domain),
MastodonNotification.predicate(userID: userID)
])
}
}
public static func predicate(validTypesRaws types: [String]) -> NSPredicate {
return NSPredicate(format: "%K IN %@", #keyPath(MastodonNotification.typeRaw), types)
}
}
extension MastodonNotification: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \MastodonNotification.createAt, ascending: false)]
}
}

View File

@ -1,145 +0,0 @@
//
// Poll.swift
// CoreDataStack
//
// Created by MainasuK Cirno on 2021-3-2.
//
import Foundation
import CoreData
public final class Poll: NSManagedObject {
public typealias ID = String
@NSManaged public private(set) var id: ID
@NSManaged public private(set) var expiresAt: Date?
@NSManaged public private(set) var expired: Bool
@NSManaged public private(set) var multiple: Bool
@NSManaged public private(set) var votesCount: NSNumber
@NSManaged public private(set) var votersCount: NSNumber?
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
// one-to-one relationship
@NSManaged public private(set) var status: Status
// one-to-many relationship
@NSManaged public private(set) var options: Set<PollOption>
// many-to-many relationship
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
}
extension Poll {
public override func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(Date(), forKey: #keyPath(Poll.createdAt))
}
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
votedBy: MastodonUser?,
options: [PollOption]
) -> Poll {
let poll: Poll = context.insertObject()
poll.id = property.id
poll.expiresAt = property.expiresAt
poll.expired = property.expired
poll.multiple = property.multiple
poll.votesCount = property.votesCount
poll.votersCount = property.votersCount
poll.updatedAt = property.networkDate
if let votedBy = votedBy {
poll.mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(votedBy)
}
poll.mutableSetValue(forKey: #keyPath(Poll.options)).addObjects(from: options)
return poll
}
public func update(expiresAt: Date?) {
if self.expiresAt != expiresAt {
self.expiresAt = expiresAt
}
}
public func update(expired: Bool) {
if self.expired != expired {
self.expired = expired
}
}
public func update(votesCount: Int) {
if self.votesCount.intValue != votesCount {
self.votesCount = NSNumber(value: votesCount)
}
}
public func update(votersCount: Int?) {
if self.votersCount?.intValue != votersCount {
self.votersCount = votersCount.flatMap { NSNumber(value: $0) }
}
}
public func update(voted: Bool, by: MastodonUser) {
if voted {
if !(votedBy ?? Set()).contains(by) {
mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(by)
}
} else {
if (votedBy ?? Set()).contains(by) {
mutableSetValue(forKey: #keyPath(Poll.votedBy)).remove(by)
}
}
}
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}
}
extension Poll {
public struct Property {
public let id: ID
public let expiresAt: Date?
public let expired: Bool
public let multiple: Bool
public let votesCount: NSNumber
public let votersCount: NSNumber?
public let networkDate: Date
public init(
id: Poll.ID,
expiresAt: Date?,
expired: Bool,
multiple: Bool,
votesCount: Int,
votersCount: Int?,
networkDate: Date
) {
self.id = id
self.expiresAt = expiresAt
self.expired = expired
self.multiple = multiple
self.votesCount = NSNumber(value: votesCount)
self.votersCount = votersCount.flatMap { NSNumber(value: $0) }
self.networkDate = networkDate
}
}
}
extension Poll: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \Poll.createdAt, ascending: false)]
}
}

View File

@ -1,98 +0,0 @@
//
// PollOption.swift
// CoreDataStack
//
// Created by MainasuK Cirno on 2021-3-2.
//
import Foundation
import CoreData
public final class PollOption: NSManagedObject {
@NSManaged public private(set) var index: NSNumber
@NSManaged public private(set) var title: String
@NSManaged public private(set) var votesCount: NSNumber?
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
// many-to-one relationship
@NSManaged public private(set) var poll: Poll
// many-to-many relationship
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
}
extension PollOption {
public override func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(Date(), forKey: #keyPath(PollOption.createdAt))
}
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
votedBy: MastodonUser?
) -> PollOption {
let option: PollOption = context.insertObject()
option.index = property.index
option.title = property.title
option.votesCount = property.votesCount
option.updatedAt = property.networkDate
if let votedBy = votedBy {
option.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(votedBy)
}
return option
}
public func update(votesCount: Int?) {
if self.votesCount?.intValue != votesCount {
self.votesCount = votesCount.flatMap { NSNumber(value: $0) }
}
}
public func update(voted: Bool, by: MastodonUser) {
if voted {
if !(self.votedBy ?? Set()).contains(by) {
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(by)
}
} else {
if (self.votedBy ?? Set()).contains(by) {
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).remove(by)
}
}
}
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}
}
extension PollOption {
public struct Property {
public let index: NSNumber
public let title: String
public let votesCount: NSNumber?
public let networkDate: Date
public init(index: Int, title: String, votesCount: Int?, networkDate: Date) {
self.index = NSNumber(value: index)
self.title = title
self.votesCount = votesCount.flatMap { NSNumber(value: $0) }
self.networkDate = networkDate
}
}
}
extension PollOption: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \PollOption.createdAt, ascending: false)]
}
}

View File

@ -1,118 +0,0 @@
//
// SearchHistory.swift
// CoreDataStack
//
// Created by sxiaojian on 2021/4/7.
//
import Foundation
import CoreData
public final class SearchHistory: NSManagedObject {
public typealias ID = UUID
@NSManaged public private(set) var identifier: ID
@NSManaged public private(set) var domain: String
@NSManaged public private(set) var userID: MastodonUser.ID
@NSManaged public private(set) var createAt: Date
@NSManaged public private(set) var updatedAt: Date
// many-to-one relationship
@NSManaged public private(set) var account: MastodonUser?
@NSManaged public private(set) var hashtag: Tag?
@NSManaged public private(set) var status: Status?
}
extension SearchHistory {
public override func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(UUID(), forKey: #keyPath(SearchHistory.identifier))
setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.createAt))
setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.updatedAt))
}
// public override func willSave() {
// super.willSave()
// setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.updatedAt))
// }
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
account: MastodonUser
) -> SearchHistory {
let searchHistory: SearchHistory = context.insertObject()
searchHistory.domain = property.domain
searchHistory.userID = property.userID
searchHistory.account = account
return searchHistory
}
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
hashtag: Tag
) -> SearchHistory {
let searchHistory: SearchHistory = context.insertObject()
searchHistory.domain = property.domain
searchHistory.userID = property.userID
searchHistory.hashtag = hashtag
return searchHistory
}
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
status: Status
) -> SearchHistory {
let searchHistory: SearchHistory = context.insertObject()
searchHistory.domain = property.domain
searchHistory.userID = property.userID
searchHistory.status = status
return searchHistory
}
}
extension SearchHistory {
public func update(updatedAt: Date) {
setValue(updatedAt, forKey: #keyPath(SearchHistory.updatedAt))
}
}
extension SearchHistory {
public struct Property {
public let domain: String
public let userID: MastodonUser.ID
public init(domain: String, userID: MastodonUser.ID) {
self.domain = domain
self.userID = userID
}
}
}
extension SearchHistory: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \SearchHistory.updatedAt, ascending: false)]
}
}
extension SearchHistory {
static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.domain), domain)
}
static func predicate(userID: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.userID), userID)
}
public static func predicate(domain: String, userID: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(userID: userID)
])
}
}

View File

@ -1,355 +0,0 @@
//
// Status.swift
// CoreDataStack
//
// Created by MainasuK Cirno on 2021/1/27.
//
import CoreData
import Foundation
public final class Status: NSManagedObject {
public typealias ID = String
@NSManaged public private(set) var identifier: ID
@NSManaged public private(set) var domain: String
@NSManaged public private(set) var id: String
@NSManaged public private(set) var uri: String
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var content: String
@NSManaged public private(set) var visibility: String?
@NSManaged public private(set) var sensitive: Bool
@NSManaged public private(set) var spoilerText: String?
@NSManaged public private(set) var application: Application?
@NSManaged public private(set) var emojisData: Data?
// Informational
@NSManaged public private(set) var reblogsCount: NSNumber
@NSManaged public private(set) var favouritesCount: NSNumber
@NSManaged public private(set) var repliesCount: NSNumber?
@NSManaged public private(set) var url: String?
@NSManaged public private(set) var inReplyToID: Status.ID?
@NSManaged public private(set) var inReplyToAccountID: MastodonUser.ID?
@NSManaged public private(set) var language: String? // (ISO 639 Part 1 two-letter language code)
@NSManaged public private(set) var text: String?
// many-to-one relationship
@NSManaged public private(set) var author: MastodonUser
@NSManaged public private(set) var reblog: Status?
@NSManaged public private(set) var replyTo: Status?
// many-to-many relationship
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>?
@NSManaged public private(set) var rebloggedBy: Set<MastodonUser>?
@NSManaged public private(set) var mutedBy: Set<MastodonUser>?
@NSManaged public private(set) var bookmarkedBy: Set<MastodonUser>?
// one-to-one relationship
@NSManaged public private(set) var pinnedBy: MastodonUser?
@NSManaged public private(set) var poll: Poll?
// one-to-many relationship
@NSManaged public private(set) var reblogFrom: Set<Status>?
@NSManaged public private(set) var mentions: Set<Mention>?
@NSManaged public private(set) var homeTimelineIndexes: Set<HomeTimelineIndex>?
@NSManaged public private(set) var mediaAttachments: Set<Attachment>?
@NSManaged public private(set) var replyFrom: Set<Status>?
@NSManaged public private(set) var inNotifications: Set<MastodonNotification>?
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
@NSManaged public private(set) var updatedAt: Date
@NSManaged public private(set) var deletedAt: Date?
@NSManaged public private(set) var revealedAt: Date?
}
extension Status {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
author: MastodonUser,
reblog: Status?,
application: Application?,
replyTo: Status?,
poll: Poll?,
mentions: [Mention]?,
mediaAttachments: [Attachment]?,
favouritedBy: MastodonUser?,
rebloggedBy: MastodonUser?,
mutedBy: MastodonUser?,
bookmarkedBy: MastodonUser?,
pinnedBy: MastodonUser?
) -> Status {
let status: Status = context.insertObject()
status.identifier = property.identifier
status.domain = property.domain
status.id = property.id
status.uri = property.uri
status.createdAt = property.createdAt
status.content = property.content
status.visibility = property.visibility
status.sensitive = property.sensitive
status.spoilerText = property.spoilerText
status.application = application
status.emojisData = property.emojisData
status.reblogsCount = property.reblogsCount
status.favouritesCount = property.favouritesCount
status.repliesCount = property.repliesCount
status.url = property.url
status.inReplyToID = property.inReplyToID
status.inReplyToAccountID = property.inReplyToAccountID
status.language = property.language
status.text = property.text
status.author = author
status.reblog = reblog
status.pinnedBy = pinnedBy
status.poll = poll
if let mentions = mentions {
status.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions)
}
if let mediaAttachments = mediaAttachments {
status.mutableSetValue(forKey: #keyPath(Status.mediaAttachments)).addObjects(from: mediaAttachments)
}
if let favouritedBy = favouritedBy {
status.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(favouritedBy)
}
if let rebloggedBy = rebloggedBy {
status.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(rebloggedBy)
}
if let mutedBy = mutedBy {
status.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mutedBy)
}
if let bookmarkedBy = bookmarkedBy {
status.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(bookmarkedBy)
}
status.updatedAt = property.networkDate
return status
}
public func update(emojisData: Data?) {
if self.emojisData != emojisData {
self.emojisData = emojisData
}
}
public func update(reblogsCount: NSNumber) {
if self.reblogsCount.intValue != reblogsCount.intValue {
self.reblogsCount = reblogsCount
}
}
public func update(favouritesCount: NSNumber) {
if self.favouritesCount.intValue != favouritesCount.intValue {
self.favouritesCount = favouritesCount
}
}
public func update(repliesCount: NSNumber?) {
guard let count = repliesCount else {
return
}
if self.repliesCount?.intValue != count.intValue {
self.repliesCount = repliesCount
}
}
public func update(replyTo: Status?) {
if self.replyTo != replyTo {
self.replyTo = replyTo
}
}
public func update(liked: Bool, by mastodonUser: MastodonUser) {
if liked {
if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(mastodonUser)
}
} else {
if (self.favouritedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).remove(mastodonUser)
}
}
}
public func update(reblogged: Bool, by mastodonUser: MastodonUser) {
if reblogged {
if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(mastodonUser)
}
} else {
if (self.rebloggedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).remove(mastodonUser)
}
}
}
public func update(muted: Bool, by mastodonUser: MastodonUser) {
if muted {
if !(self.mutedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mastodonUser)
}
} else {
if (self.mutedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).remove(mastodonUser)
}
}
}
public func update(bookmarked: Bool, by mastodonUser: MastodonUser) {
if bookmarked {
if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(mastodonUser)
}
} else {
if (self.bookmarkedBy ?? Set()).contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).remove(mastodonUser)
}
}
}
public func update(isReveal: Bool) {
revealedAt = isReveal ? Date() : nil
}
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}
}
extension Status {
public struct Property {
public let identifier: ID
public let domain: String
public let id: String
public let uri: String
public let createdAt: Date
public let content: String
public let visibility: String?
public let sensitive: Bool
public let spoilerText: String?
public let emojisData: Data?
public let reblogsCount: NSNumber
public let favouritesCount: NSNumber
public let repliesCount: NSNumber?
public let url: String?
public let inReplyToID: Status.ID?
public let inReplyToAccountID: MastodonUser.ID?
public let language: String? // (ISO 639 Part @1 two-letter language code)
public let text: String?
public let networkDate: Date
public init(
domain: String,
id: String,
uri: String,
createdAt: Date,
content: String,
visibility: String?,
sensitive: Bool,
spoilerText: String?,
emojisData: Data?,
reblogsCount: NSNumber,
favouritesCount: NSNumber,
repliesCount: NSNumber?,
url: String?,
inReplyToID: Status.ID?,
inReplyToAccountID: MastodonUser.ID?,
language: String?,
text: String?,
networkDate: Date
) {
self.identifier = id + "@" + domain
self.domain = domain
self.id = id
self.uri = uri
self.createdAt = createdAt
self.content = content
self.visibility = visibility
self.sensitive = sensitive
self.spoilerText = spoilerText
self.emojisData = emojisData
self.reblogsCount = reblogsCount
self.favouritesCount = favouritesCount
self.repliesCount = repliesCount
self.url = url
self.inReplyToID = inReplyToID
self.inReplyToAccountID = inReplyToAccountID
self.language = language
self.text = text
self.networkDate = networkDate
}
}
}
extension Status: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \Status.createdAt, ascending: false)]
}
}
extension Status {
static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Status.domain), domain)
}
static func predicate(id: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Status.id), id)
}
public static func predicate(domain: String, id: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(id: id)
])
}
static func predicate(ids: [String]) -> NSPredicate {
return NSPredicate(format: "%K IN %@", #keyPath(Status.id), ids)
}
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(ids: ids)
])
}
public static func notDeleted() -> NSPredicate {
return NSPredicate(format: "%K == nil", #keyPath(Status.deletedAt))
}
public static func deleted() -> NSPredicate {
return NSPredicate(format: "%K != nil", #keyPath(Status.deletedAt))
}
}

View File

@ -1,112 +0,0 @@
//
// Tag.swift
// CoreDataStack
//
// Created by sxiaojian on 2021/2/1.
//
import CoreData
import Foundation
public final class Tag: NSManagedObject {
public typealias ID = UUID
@NSManaged public private(set) var identifier: ID
@NSManaged public private(set) var createAt: Date
@NSManaged public private(set) var updatedAt: Date
@NSManaged public private(set) var name: String
@NSManaged public private(set) var url: String
// one-to-one relationship
// many-to-many relationship
// one-to-many relationship
@NSManaged public private(set) var histories: Set<History>?
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
}
public extension Tag {
override func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(UUID(), forKey: #keyPath(Tag.identifier))
setPrimitiveValue(Date(), forKey: #keyPath(Tag.createAt))
setPrimitiveValue(Date(), forKey: #keyPath(Tag.updatedAt))
}
override func willSave() {
super.willSave()
setPrimitiveValue(Date(), forKey: #keyPath(Tag.updatedAt))
}
@discardableResult
static func insert(
into context: NSManagedObjectContext,
property: Property
) -> Tag {
let tag: Tag = context.insertObject()
tag.name = property.name
tag.url = property.url
if let histories = property.histories {
tag.mutableSetValue(forKey: #keyPath(Tag.histories)).addObjects(from: histories)
}
return tag
}
}
extension Tag {
public func findSearchHistory(domain: String, userID: MastodonUser.ID) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == domain
&& searchHistory.userID == userID
}
}
}
public extension Tag {
struct Property {
public let name: String
public let url: String
public let histories: [History]?
public init(name: String, url: String, histories: [History]?) {
self.name = name
self.url = url
self.histories = histories
}
}
}
public extension Tag {
func updateHistory(index: Int, day: Date, uses: String, account: String) {
guard let histories = self.histories?.sorted(by: {
$0.createAt.compare($1.createAt) == .orderedAscending
}) else { return }
let history = histories[index]
history.update(day: day)
history.update(uses: uses)
history.update(accounts: account)
}
func appendHistory(history: History) {
self.mutableSetValue(forKeyPath: #keyPath(Tag.histories)).add(history)
}
func update(url: String) {
if self.url != url {
self.url = url
}
}
}
extension Tag: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
[NSSortDescriptor(keyPath: \Tag.createAt, ascending: false)]
}
}
public extension Tag {
static func predicate(name: String) -> NSPredicate {
NSPredicate(format: "%K == %@", #keyPath(Tag.name), name)
}
}

View File

@ -0,0 +1,46 @@
//
// Feed+Acct.swift
// Feed+Acct
//
// Created by Cirno MainasuK on 2021-8-26.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
extension Feed {
public enum Acct: RawRepresentable {
case none
case mastodon(domain: String, userID: MastodonUser.ID)
public init?(rawValue: String) {
let components = rawValue.split(separator: "@", maxSplits: 2)
guard components.count == 3 else { return nil }
let userID = String(components[1]).escape
let domain = String(components[2]).escape
switch components[0] {
case "M":
self = .mastodon(domain: domain, userID: userID)
default:
self = .none
}
}
public var rawValue: String {
switch self {
case .none:
return "none@userID@domain"
case .mastodon(let domain, let userID):
return "M@\(userID.escape)@\(domain.escape)"
}
}
}
}
extension String {
fileprivate var escape: String {
replacingOccurrences(of: "@", with: "_at_")
}
}

View File

@ -0,0 +1,17 @@
//
// Feed+Kind.swift
// CoreDataStack
//
// Created by MainasuK on 2022-1-11.
//
import Foundation
extension Feed {
public enum Kind: String, CaseIterable, Hashable {
case none
case home
case notificationAll
case notificationMentions
}
}

View File

@ -0,0 +1,58 @@
//
// MastodonAttachment.swift
// MastodonAttachment
//
// Created by Cirno MainasuK on 2021-8-30.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
import CoreGraphics
public final class MastodonAttachment: NSObject, Codable {
public typealias ID = String
public let id: ID
public let kind: Kind
public let size: CGSize
public let focus: CGPoint?
public let blurhash: String?
public let assetURL: String?
public let previewURL: String?
public let textURL: String?
public let durationMS: Int?
public let altDescription: String?
public init(
id: MastodonAttachment.ID,
kind: MastodonAttachment.Kind,
size: CGSize,
focus: CGPoint?,
blurhash: String?,
assetURL: String?,
previewURL: String?,
textURL: String?,
durationMS: Int?,
altDescription: String?
) {
self.id = id
self.kind = kind
self.size = size
self.focus = focus
self.blurhash = blurhash
self.assetURL = assetURL
self.previewURL = previewURL
self.textURL = textURL
self.durationMS = durationMS
self.altDescription = altDescription
}
}
extension MastodonAttachment {
public enum Kind: String, Codable {
case image
case video
case gifv
case audio
}
}

View File

@ -0,0 +1,30 @@
//
// MastodonEmoji.swift
// MastodonEmoji
//
// Created by Cirno MainasuK on 2021-9-2.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
public final class MastodonEmoji: NSObject, Codable {
public let code: String
public let url: String
public let staticURL: String
public let visibleInPicker: Bool
public let category: String?
public init(code:
String, url:
String, staticURL:
String, visibleInPicker:
Bool, category: String?
) {
self.code = code
self.url = url
self.staticURL = staticURL
self.visibleInPicker = visibleInPicker
self.category = category
}
}

View File

@ -0,0 +1,25 @@
//
// MastodonField.swift
// CoreDataStack
//
// Created by Cirno MainasuK on 2021-9-18.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
public final class MastodonField: NSObject, Codable {
public let name: String
public let value: String
public let verifiedAt: Date?
public init(
name: String,
value: String,
verifiedAt: Date?
) {
self.name = name
self.value = value
self.verifiedAt = verifiedAt
}
}

View File

@ -0,0 +1,31 @@
//
// MastodonMention.swift
// CoreDataStack
//
// Created by MainasuK on 2022-1-17.
//
import Foundation
public final class MastodonMention: NSObject, Codable {
public typealias ID = String
public let id: ID
public let username: String
public let acct: String
public let url: String
public init(
id: MastodonMention.ID,
username: String,
acct: String,
url: String
) {
self.id = id
self.username = username
self.acct = acct
self.url = url
}
}

View File

@ -0,0 +1,46 @@
//
// MastodonNotificationType.swift
// CoreDataStack
//
// Created by MainasuK on 2022-1-21.
//
import Foundation
public enum MastodonNotificationType: RawRepresentable {
case follow
case followRequest
case mention
case reblog
case favourite // same to API
case poll
case status
case _other(String)
public init?(rawValue: String) {
switch rawValue {
case "follow": self = .follow
case "followRequest": self = .followRequest
case "mention": self = .mention
case "reblog": self = .reblog
case "favourite": self = .favourite
case "poll": self = .poll
case "status": self = .status
default: self = ._other(rawValue)
}
}
public var rawValue: String {
switch self {
case .follow: return "follow"
case .followRequest: return "followRequest"
case .mention: return "mention"
case .reblog: return "reblog"
case .favourite: return "favourite"
case .poll: return "poll"
case .status: return "status"
case ._other(let value): return value
}
}
}

View File

@ -0,0 +1,24 @@
//
// MastodonTagHistory.swift
// CoreDataStack
//
// Created by MainasuK on 2022-1-20.
//
import Foundation
public final class MastodonTagHistory: NSObject, Codable {
/// UNIX timestamp on midnight of the given day
public let day: Date
public let uses: String
public let accounts: String
public init(day: Date, uses: String, accounts: String) {
self.day = day
self.uses = uses
self.accounts = accounts
}
}

View File

@ -0,0 +1,38 @@
//
// MastodonVisibility.swift
// MastodonVisibility
//
// Created by Cirno MainasuK on 2021-8-27.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
public enum MastodonVisibility: RawRepresentable {
case `public`
case unlisted
case `private`
case direct
case _other(String)
public init?(rawValue: String) {
switch rawValue {
case "public": self = .public
case "unlisted": self = .unlisted
case "private": self = .private
case "direct": self = .direct
default: self = ._other(rawValue)
}
}
public var rawValue: String {
switch self {
case .public: return "public"
case .unlisted: return "unlisted"
case .private: return "private"
case .direct: return "direct"
case ._other(let value): return value
}
}
}

View File

@ -47,3 +47,66 @@ extension NSManagedObjectContext {
}
}
}
extension NSManagedObjectContext {
public func perform<T>(block: @escaping () throws -> T) async throws -> T {
if #available(iOSApplicationExtension 15.0, *) {
return try await perform(schedule: .enqueued) {
try block()
}
} else {
return try await withCheckedThrowingContinuation { continuation in
self.perform {
do {
let value = try block()
continuation.resume(returning: value)
} catch {
continuation.resume(throwing: error)
}
}
} // end return
}
}
public func performChanges<T>(block: @escaping () throws -> T) async throws -> T {
if #available(iOS 15.0, *) {
return try await perform(schedule: .enqueued) {
let value = try block()
try self.saveOrRollback()
return value
}
} else {
return try await withCheckedThrowingContinuation { continuation in
self.perform {
do {
let value = try block()
try self.saveOrRollback()
continuation.resume(returning: value)
} catch {
continuation.resume(throwing: error)
}
}
} // end return
}
} // end func
}
extension NSManagedObjectContext {
static let objectCacheKey = "ObjectCacheKey"
private typealias ObjectCache = [String: NSManagedObject]
public func cache(
_ object: NSManagedObject?,
key: String
) {
var cache = userInfo[NSManagedObjectContext.objectCacheKey] as? ObjectCache ?? [:]
cache[key] = object
userInfo[NSManagedObjectContext.objectCacheKey] = cache
}
public func cache(froKey key: String) -> NSManagedObject? {
guard let cache = userInfo[NSManagedObjectContext.objectCacheKey] as? ObjectCache
else { return nil }
return cache[key]
}
}

View File

@ -17,6 +17,6 @@
<key>CFBundleShortVersionString</key>
<string>1.3.0</string>
<key>CFBundleVersion</key>
<string>90</string>
<string>91</string>
</dict>
</plist>

View File

@ -10,10 +10,10 @@ import CoreData
public struct ManagedObjectContextObjectsDidChangeNotification {
public let notification: Notification
public let notification: Foundation.Notification
public let managedObjectContext: NSManagedObjectContext
public init?(notification: Notification) {
public init?(notification: Foundation.Notification) {
guard notification.name == .NSManagedObjectContextObjectsDidChange,
let managedObjectContext = notification.object as? NSManagedObjectContext else {
return nil

View File

@ -2,7 +2,8 @@
// ManagedObjectObserver.swift
// CoreDataStack
//
// Created by sxiaojian on 2021/2/8.
// Created by Cirno MainasuK on 2020-6-12.
// Copyright © 2020 Dimension. All rights reserved.
//
import Foundation
@ -15,6 +16,26 @@ final public class ManagedObjectObserver {
extension ManagedObjectObserver {
public static func observe(context: NSManagedObjectContext) -> AnyPublisher<Changes, Error> {
return NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context)
.tryMap { notification in
guard let notification = ManagedObjectContextObjectsDidChangeNotification(notification: notification) else {
throw Error.notManagedObjectChangeNotification
}
let changeTypes = ManagedObjectObserver.changeTypes(in: notification)
return Changes(
changeTypes: changeTypes,
changeNotification: notification
)
}
.mapError { error -> Error in
return (error as? Error) ?? .unknown(error)
}
.eraseToAnyPublisher()
}
public static func observe(object: NSManagedObject) -> AnyPublisher<Change, Error> {
guard let context = object.managedObjectContext else {
return Fail(error: .noManagedObjectContext).eraseToAnyPublisher()
@ -41,10 +62,26 @@ extension ManagedObjectObserver {
}
extension ManagedObjectObserver {
private static func changeTypes(in notification: ManagedObjectContextObjectsDidChangeNotification) -> [ChangeType] {
var changeTypes: [ChangeType] = []
let deleted = notification.deletedObjects.union(notification.invalidedObjects)
for object in deleted {
changeTypes.append(.delete(object))
}
let updated = notification.updatedObjects.union(notification.refreshedObjects)
for object in updated {
changeTypes.append(.update(object))
}
return changeTypes
}
private static func changeType(of object: NSManagedObject, in notification: ManagedObjectContextObjectsDidChangeNotification) -> ChangeType? {
let deleted = notification.deletedObjects.union(notification.invalidedObjects)
if notification.invalidatedAllObjects || deleted.contains(where: { $0 === object }) {
return .delete
return .delete(object)
}
let updated = notification.updatedObjects.union(notification.refreshedObjects)
@ -57,6 +94,16 @@ extension ManagedObjectObserver {
}
extension ManagedObjectObserver {
public struct Changes {
public let changeTypes: [ChangeType]
public let changeNotification: ManagedObjectContextObjectsDidChangeNotification
init(changeTypes: [ManagedObjectObserver.ChangeType], changeNotification: ManagedObjectContextObjectsDidChangeNotification) {
self.changeTypes = changeTypes
self.changeNotification = changeNotification
}
}
public struct Change {
public let changeType: ChangeType?
public let changeNotification: ManagedObjectContextObjectsDidChangeNotification
@ -65,10 +112,10 @@ extension ManagedObjectObserver {
self.changeType = changeType
self.changeNotification = changeNotification
}
}
public enum ChangeType {
case delete
case delete(NSManagedObject)
case update(NSManagedObject)
}

View File

@ -0,0 +1,14 @@
//
// AutoGenerateProperty.swift
// AutoGenerateProperty
//
// Created by Cirno MainasuK on 2021-8-18.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
// Stencil protocol
protocol AutoGenerateProperty { }
// - autoGenerateProperty

View File

@ -0,0 +1,14 @@
//
// AutoGenerateRelationship.swift
// AutoGenerateRelationship
//
// Created by Cirno MainasuK on 2021-8-19.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
// Stencil protocol
protocol AutoGenerateRelationship { }
// - autoGenerateRelationship

View File

@ -0,0 +1,14 @@
//
// AutoUpdatableObject.swift
// AutoUpdatableObject
//
// Created by Cirno MainasuK on 2021-8-18.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
// Stencil protocol
protocol AutoUpdatableObject { }
// - autoUpdatableObject

View File

@ -0,0 +1,45 @@
{% for type in types.implementing.AutoGenerateProperty %}
// sourcery:inline:{{type.name}}.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
{% for variable in type.variables|instance where
variable|annotated:"autoGenerateProperty"
%}
public let {{variable.name}}: {{variable.typeName}}
{% endfor %}
public init(
{% for variable in type.variables|instance where
variable|annotated:"autoGenerateProperty"
%}
{{variable.name}}: {{variable.typeName}}{% if not forloop.last %},{% endif %}
{% endfor %}
) {
{% for variable in type.variables|instance where
variable|annotated:"autoGenerateProperty"
%}
self.{{variable.name}} = {{variable.name}}
{% endfor %}
}
}
public func configure(property: Property) {
{% for variable in type.variables|instance where
variable|annotated:"autoGenerateProperty"
%}
self.{{variable.name}} = property.{{variable.name}}
{% endfor %}
}
public func update(property: Property) {
{% for variable in type.variables|instance where
variable|annotated:"autoUpdatableObject" and
variable|annotated:"autoGenerateProperty"
%}
update({{variable.name}}: property.{{variable.name}})
{% endfor %}
}
// sourcery:end
{% endfor %}

View File

@ -0,0 +1,29 @@
{% for type in types.implementing.AutoGenerateRelationship %}
// sourcery:inline:{{type.name}}.AutoGenerateRelationship
// Generated using Sourcery
// DO NOT EDIT
public struct Relationship {
{% for variable in type.storedVariables|annotated:"autoGenerateRelationship" %}
public let {{variable.name}}: {{variable.typeName}}
{% endfor %}
public init(
{% for variable in type.storedVariables|annotated:"autoGenerateRelationship" %}
{{variable.name}}: {{variable.typeName}}{% if not forloop.last %},{% endif %}
{% endfor %}
) {
{% for variable in type.storedVariables|annotated:"autoGenerateRelationship" %}
self.{{variable.name}} = {{variable.name}}
{% endfor %}
}
}
public func configure(relationship: Relationship) {
{% for variable in type.storedVariables|annotated:"autoGenerateRelationship" %}
self.{{variable.name}} = relationship.{{variable.name}}
{% endfor %}
}
// sourcery:end
{% endfor %}

View File

@ -0,0 +1,16 @@
{% for type in types.implementing.AutoUpdatableObject %}
// sourcery:inline:{{type.name}}.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
{% for variable in type.variables|instance where
variable|annotated:"autoUpdatableObject"
%}
public func update({{variable.name}}: {{variable.typeName}}) {
if self.{{variable.name}} != {{variable.name}} {
self.{{variable.name}} = {{variable.name}}
}
}
{% endfor %}
// sourcery:end
{% endfor %}

View File

@ -0,0 +1,32 @@
//
// ManagedObjectRecord.swift
// ManagedObjectRecord
//
// Created by Cirno MainasuK on 2021-8-25.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
import CoreData
public class ManagedObjectRecord<T: Managed>: Hashable {
public let objectID: NSManagedObjectID
public init(objectID: NSManagedObjectID) {
self.objectID = objectID
}
public func object(in managedObjectContext: NSManagedObjectContext) -> T? {
return managedObjectContext.object(with: objectID) as? T
}
public static func == (lhs: ManagedObjectRecord<T>, rhs: ManagedObjectRecord<T>) -> Bool {
return lhs.objectID == rhs.objectID
}
public func hash(into hasher: inout Hasher) {
hasher.combine(objectID)
}
}

View File

@ -17,6 +17,6 @@
<key>CFBundleShortVersionString</key>
<string>1.3.0</string>
<key>CFBundleVersion</key>
<string>90</string>
<string>91</string>
</dict>
</plist>

View File

@ -1,11 +1,6 @@
import os.log
import Foundation
let currentFileURL = URL(fileURLWithPath: "\(#file)", isDirectory: false)
let packageRootURL = currentFileURL.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
let inputDirectoryURL = packageRootURL.appendingPathComponent("input", isDirectory: true)
let outputDirectoryURL = packageRootURL.appendingPathComponent("output", isDirectory: true)
// conver i18n JSON templates to strings files
private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
do {
@ -17,7 +12,6 @@ private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
for inputLanguageDirectoryURL in inputLanguageDirectoryURLs {
let language = inputLanguageDirectoryURL.lastPathComponent
guard let mappedLanguage = map(language: language) else { continue }
let outputDirectoryURL = outputDirectory.appendingPathComponent(mappedLanguage + ".lproj", isDirectory: true)
os_log("%{public}s[%{public}ld], %{public}s: process %s -> %s", ((#file as NSString).lastPathComponent), #line, #function, language, mappedLanguage)
let fileURLs = try FileManager.default.contentsOfDirectory(
@ -29,9 +23,19 @@ private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
os_log("%{public}s[%{public}ld], %{public}s: process %s", ((#file as NSString).lastPathComponent), #line, #function, jsonURL.debugDescription)
let filename = jsonURL.deletingPathExtension().lastPathComponent
guard let (mappedFilename, keyStyle) = map(filename: filename) else { continue }
let outputFileURL = outputDirectoryURL.appendingPathComponent(mappedFilename).appendingPathExtension("strings")
guard let bundle = bundle(filename: filename) else { continue }
let outputDirectoryURL = outputDirectory
.appendingPathComponent(bundle, isDirectory: true)
.appendingPathComponent(mappedLanguage + ".lproj", isDirectory: true)
let outputFileURL = outputDirectoryURL
.appendingPathComponent(mappedFilename)
.appendingPathExtension("strings")
let strings = try process(url: jsonURL, keyStyle: keyStyle)
try? FileManager.default.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: nil)
try strings.write(to: outputFileURL, atomically: true, encoding: .utf8)
}
}
@ -44,6 +48,7 @@ private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
private func map(language: String) -> String? {
switch language {
case "ar_SA": return "ar" // Arabic (Saudi Arabia)
case "eu_ES": return "eu-ES" // Basque
case "ca_ES": return "ca" // Catalan
case "zh_CN": return "zh-Hans" // Chinese Simplified
case "nl_NL": return "nl" // Dutch
@ -56,6 +61,7 @@ private func map(language: String) -> String? {
case "gd_GB": return "gd-GB" // Scottish Gaelic
case "es_ES": return "es" // Spanish
case "es_AR": return "es-419" // Spanish, Argentina
case "sv_FI": return "sv_FI" // Swedish, Finland
case "th_TH": return "th" // Thai
default: return nil
}
@ -69,6 +75,14 @@ private func map(filename: String) -> (filename: String, keyStyle: Parser.KeySty
}
}
private func bundle(filename: String) -> String? {
switch filename {
case "app": return "module"
case "ios-infoPlist": return "main"
default: return nil
}
}
private func process(url: URL, keyStyle: Parser.KeyStyle) throws -> String {
do {
let data = try Data(contentsOf: url)
@ -115,9 +129,16 @@ private func move(from inputDirectoryURL: URL, to outputDirectoryURL: URL, pathE
}
}
// i18n from "input" to "output"
let currentFileURL = URL(fileURLWithPath: "\(#file)", isDirectory: false)
let packageRootURL = currentFileURL.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
let inputDirectoryURL = packageRootURL.appendingPathComponent("input", isDirectory: true)
let outputDirectoryURL = packageRootURL.appendingPathComponent("output", isDirectory: true)
convert(from: inputDirectoryURL, to: outputDirectoryURL)
move(from: inputDirectoryURL, to: outputDirectoryURL, pathExtension: "stringsdict")
let moduleDirectoryURL = outputDirectoryURL.appendingPathComponent("module", isDirectory: true)
move(from: inputDirectoryURL, to: moduleDirectoryURL, pathExtension: "stringsdict")
// i18n from "Intents/input" to "Intents/output"
let intentsDirectoryURL = packageRootURL.appendingPathComponent("Intents", isDirectory: true)

View File

@ -45,8 +45,8 @@
"message": "Please enable the photo library access permission to save the photo."
},
"delete_post": {
"title": "Are you sure you want to delete this post?",
"delete": "Delete"
"title": "Delete Post",
"message": "Are you sure you want to delete this post?"
},
"clean_cache": {
"title": "Clean Cache",
@ -412,14 +412,24 @@
"segmented_control": {
"posts": "Posts",
"replies": "Replies",
"media": "Media"
"posts_and_replies": "Posts and Replies",
"media": "Media",
"about": "About"
},
"relationship_action_alert": {
"confirm_mute_user": {
"title": "Mute Account",
"message": "Confirm to mute %s"
},
"confirm_unmute_user": {
"title": "Unmute Account",
"message": "Confirm to unmute %s"
},
"confirm_unblock_usre": {
"confirm_block_user": {
"title": "Block Account",
"message": "Confirm to block %s"
},
"confirm_unblock_user": {
"title": "Unblock Account",
"message": "Confirm to unblock %s"
}
@ -472,12 +482,14 @@
"Everything": "Everything",
"Mentions": "Mentions"
},
"user_followed_you": "%s followed you",
"user_favorited your post": "%s favorited your post",
"user_reblogged_your_post": "%s reblogged your post",
"user_mentioned_you": "%s mentioned you",
"user_requested_to_follow_you": "%s requested to follow you",
"user_your_poll_has_ended": "%s Your poll has ended",
"notification_description": {
"followed_you": "followd you",
"favorited_your_post": "favorited your post",
"reblogged_your_post": "reblogged your post",
"mentioned_you": "mentioned you",
"request_to_follow_you": "request to follow you",
"poll_has_ended": "poll has ended"
},
"keyobard": {
"show_everything": "Show Everything",
"show_mentions": "Show Mentions"
@ -564,4 +576,4 @@
"accessibility_hint": "Double tap to dismiss this wizard"
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,108 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DB427DD125BAA00100D1B89D"
BuildableName = "Mastodon.app"
BlueprintName = "Mastodon"
ReferencedContainer = "container:Mastodon.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "ASDK - Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DB427DE725BAA00100D1B89D"
BuildableName = "MastodonTests.xctest"
BlueprintName = "MastodonTests"
ReferencedContainer = "container:Mastodon.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DB427DF225BAA00100D1B89D"
BuildableName = "MastodonUITests.xctest"
BlueprintName = "MastodonUITests"
ReferencedContainer = "container:Mastodon.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DB89B9F525C10FD0008580ED"
BuildableName = "CoreDataStackTests.xctest"
BlueprintName = "CoreDataStackTests"
ReferencedContainer = "container:Mastodon.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "ASDK - Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DB427DD125BAA00100D1B89D"
BuildableName = "Mastodon.app"
BlueprintName = "Mastodon"
ReferencedContainer = "container:Mastodon.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "ASDK - Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DB427DD125BAA00100D1B89D"
BuildableName = "Mastodon.app"
BlueprintName = "Mastodon"
ReferencedContainer = "container:Mastodon.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "ASDK - Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "ASDK - Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -7,18 +7,13 @@
<key>AppShared.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>26</integer>
<integer>28</integer>
</dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>27</integer>
</dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
@ -102,7 +97,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>25</integer>
<integer>26</integer>
</dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict>
@ -117,15 +112,36 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
<integer>2</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>24</integer>
<integer>25</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict/>
<dict>
<key>DB427DD125BAA00100D1B89D</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>DB427DE725BAA00100D1B89D</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>DB427DF225BAA00100D1B89D</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>DB89B9F525C10FD0008580ED</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

View File

@ -55,15 +55,6 @@
"version": "1.2.0"
}
},
{
"package": "FLAnimatedImage",
"repositoryURL": "https://github.com/Flipboard/FLAnimatedImage",
"state": {
"branch": null,
"revision": "e7f9fd4681ae41bf6f3056db08af4f401d61da52",
"version": "1.0.16"
}
},
{
"package": "FPSIndicator",
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",

7
Mastodon/.sourcery.yml Normal file
View File

@ -0,0 +1,7 @@
sources:
- .
- ../MastodonSDK/Sources
templates:
- ./Template
output:
Generated

View File

@ -7,6 +7,8 @@
import UIKit
import SafariServices
import MastodonAsset
import MastodonLocalization
final class SafariActivity: UIActivity {
@ -55,8 +57,10 @@ final class SafariActivity: UIActivity {
return
}
sceneCoordinator?.present(scene: .safari(url: url as URL), from: nil, transition: .safariPresent(animated: true, completion: nil))
activityDidFinish(true)
Task {
await sceneCoordinator?.present(scene: .safari(url: url as URL), from: nil, transition: .safariPresent(animated: true, completion: nil))
activityDidFinish(true)
}
}
}

View File

@ -0,0 +1,13 @@
//
// ShareActivityProvider.swift
// Mastodon
//
// Created by MainasuK on 2022-1-25.
//
import UIKit
protocol ShareActivityProvider {
var activities: [Any] { get }
var applicationActivities: [UIActivity] { get }
}

View File

@ -10,6 +10,8 @@ import SafariServices
import CoreDataStack
import MastodonSDK
import PanModal
import MastodonAsset
import MastodonLocalization
final public class SceneCoordinator {
@ -194,10 +196,6 @@ extension SceneCoordinator {
case alertController(alertController: UIAlertController)
case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
#if DEBUG
case publicTimeline
#endif
var isOnboarding: Bool {
switch self {
case .welcome,
@ -211,7 +209,7 @@ extension SceneCoordinator {
return false
}
}
}
} // end enum Scene { }
}
extension SceneCoordinator {
@ -266,6 +264,7 @@ extension SceneCoordinator {
}
@discardableResult
@MainActor
func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? {
guard let viewController = get(scene: scene) else {
return nil
@ -481,12 +480,6 @@ private extension SceneCoordinator {
let _viewController = ReportViewController()
_viewController.viewModel = viewModel
viewController = _viewController
#if DEBUG
case .publicTimeline:
let _viewController = PublicTimelineViewController()
_viewController.viewModel = PublicTimelineViewModel(context: appContext)
viewController = _viewController
#endif
}
setupDependency(for: viewController as? NeedsDependency)

View File

@ -1,145 +0,0 @@
//
// PickServerCategoriesCell.swift
// Mastodon
//
// Created by BradGao on 2021/2/23.
//
//import os.log
//import UIKit
//import MastodonSDK
//
//protocol PickServerCategoriesCellDelegate: AnyObject {
// func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
//}
//
//final class PickServerCategoriesCell: UITableViewCell {
//
// weak var delegate: PickServerCategoriesCellDelegate?
//
// var diffableDataSource: UICollectionViewDiffableDataSource<CategoryPickerSection, CategoryPickerItem>?
//
// let metricView = UIView()
//
// let collectionView: UICollectionView = {
// let flowLayout = UICollectionViewFlowLayout()
// flowLayout.scrollDirection = .horizontal
// let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout)
// view.register(PickServerCategoryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self))
// view.backgroundColor = .clear
// view.showsHorizontalScrollIndicator = false
// view.showsVerticalScrollIndicator = false
// view.layer.masksToBounds = false
// view.translatesAutoresizingMaskIntoConstraints = false
// return view
// }()
//
// override func prepareForReuse() {
// super.prepareForReuse()
//
// delegate = nil
// }
//
// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
// super.init(style: style, reuseIdentifier: reuseIdentifier)
// _init()
// }
//
// required init?(coder: NSCoder) {
// super.init(coder: coder)
// _init()
// }
//}
//
//extension PickServerCategoriesCell {
//
// private func _init() {
// selectionStyle = .none
// backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
// configureMargin()
//
// metricView.translatesAutoresizingMaskIntoConstraints = false
// contentView.addSubview(metricView)
// NSLayoutConstraint.activate([
// metricView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
// metricView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
// metricView.topAnchor.constraint(equalTo: contentView.topAnchor),
// metricView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// metricView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh),
// ])
//
// contentView.addSubview(collectionView)
// NSLayoutConstraint.activate([
// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
// contentView.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 20),
// collectionView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh),
// ])
//
// collectionView.delegate = self
// }
//
// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
// super.traitCollectionDidChange(previousTraitCollection)
//
// configureMargin()
// }
//
// override func layoutSubviews() {
// super.layoutSubviews()
//
// collectionView.collectionViewLayout.invalidateLayout()
// }
//
//}
//
//extension PickServerCategoriesCell {
// private func configureMargin() {
// switch traitCollection.horizontalSizeClass {
// case .regular:
// let margin = MastodonPickServerViewController.viewEdgeMargin
// contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
// default:
// contentView.layoutMargins = .zero
// }
// }
//}
//
//// MARK: - UICollectionViewDelegateFlowLayout
//extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout {
//
// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
// collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
// delegate?.pickServerCategoriesCell(self, collectionView: collectionView, didSelectItemAt: indexPath)
// }
//
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
// layoutIfNeeded()
// return UIEdgeInsets(top: 0, left: metricView.frame.minX - collectionView.frame.minX, bottom: 0, right: collectionView.frame.maxX - metricView.frame.maxX)
// }
//
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
// return 16
// }
//
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// return CGSize(width: 60, height: 80)
// }
//
//}
//
//extension PickServerCategoriesCell {
//
// override func accessibilityElementCount() -> Int {
// guard let diffableDataSource = diffableDataSource else { return 0 }
// return diffableDataSource.snapshot().itemIdentifiers.count
// }
//
// override func accessibilityElement(at index: Int) -> Any? {
// guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil }
// return item
// }
//
//}

View File

@ -1,171 +0,0 @@
//
// PickServerSearchCell.swift
// Mastodon
//
// Created by BradGao on 2021/2/24.
//
import UIKit
//protocol PickServerSearchCellDelegate: AnyObject {
// func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?)
//}
//
//class PickServerSearchCell: UITableViewCell {
//
// weak var delegate: PickServerSearchCellDelegate?
//
// private var bgView: UIView = {
// let view = UIView()
// view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
// view.translatesAutoresizingMaskIntoConstraints = false
// view.layer.maskedCorners = [
// .layerMinXMinYCorner,
// .layerMaxXMinYCorner
// ]
// view.layer.cornerCurve = .continuous
// view.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
// return view
// }()
//
// private var textFieldBgView: UIView = {
// let view = UIView()
// view.backgroundColor = Asset.Colors.TextField.background.color
// view.translatesAutoresizingMaskIntoConstraints = false
// view.layer.masksToBounds = true
// view.layer.cornerRadius = 6
// view.layer.cornerCurve = .continuous
// return view
// }()
//
// let searchTextField: UITextField = {
// let textField = UITextField()
// textField.translatesAutoresizingMaskIntoConstraints = false
// textField.leftView = {
// let imageView = UIImageView(
// image: UIImage(
// systemName: "magnifyingglass",
// withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
// )
// )
// imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)
//
// let containerView = UIView()
// imageView.translatesAutoresizingMaskIntoConstraints = false
// containerView.addSubview(imageView)
// NSLayoutConstraint.activate([
// imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
// imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
// imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
// ])
//
// let paddingView = UIView()
// paddingView.translatesAutoresizingMaskIntoConstraints = false
// containerView.addSubview(paddingView)
// NSLayoutConstraint.activate([
// paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
// paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor),
// paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
// paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
// paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
// ])
// return containerView
// }()
// textField.leftViewMode = .always
// textField.font = .systemFont(ofSize: 15, weight: .regular)
// textField.tintColor = Asset.Colors.Label.primary.color
// textField.textColor = Asset.Colors.Label.primary.color
// textField.adjustsFontForContentSizeCategory = true
// textField.attributedPlaceholder =
// NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder,
// attributes: [.font: UIFont.systemFont(ofSize: 15, weight: .regular),
// .foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)])
// textField.clearButtonMode = .whileEditing
// textField.autocapitalizationType = .none
// textField.autocorrectionType = .no
// textField.returnKeyType = .done
// textField.keyboardType = .URL
// return textField
// }()
//
// override func prepareForReuse() {
// super.prepareForReuse()
//
// delegate = nil
// }
//
// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
// super.init(style: style, reuseIdentifier: reuseIdentifier)
// _init()
// }
//
// required init?(coder: NSCoder) {
// super.init(coder: coder)
// _init()
// }
//}
//
//extension PickServerSearchCell {
// private func _init() {
// selectionStyle = .none
// backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
// configureMargin()
//
// searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
// searchTextField.delegate = self
//
// contentView.addSubview(bgView)
// contentView.addSubview(textFieldBgView)
// contentView.addSubview(searchTextField)
//
// NSLayoutConstraint.activate([
// bgView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
// bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
// bgView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
// bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
//
// textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14),
// textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12),
// bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14),
// bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13),
//
// searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11),
// searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4),
// textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11),
// textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4),
// ])
// }
//
// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
// super.traitCollectionDidChange(previousTraitCollection)
//
// configureMargin()
// }
//}
//
//extension PickServerSearchCell {
// private func configureMargin() {
// switch traitCollection.horizontalSizeClass {
// case .regular:
// let margin = MastodonPickServerViewController.viewEdgeMargin
// contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
// default:
// contentView.layoutMargins = .zero
// }
// }
//}
//
//extension PickServerSearchCell {
// @objc private func textFieldDidChange(_ textField: UITextField) {
// delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text)
// }
//}
//
//// MARK: - UITextFieldDelegate
//extension PickServerSearchCell: UITextFieldDelegate {
//
// func textFieldShouldReturn(_ textField: UITextField) -> Bool {
// textField.resignFirstResponder()
// return false
// }
//}

View File

@ -8,6 +8,8 @@
import UIKit
import MastodonSDK
import MastodonMeta
import MastodonAsset
import MastodonLocalization
enum AutoCompleteSection: Equatable, Hashable {
case main
@ -80,7 +82,7 @@ extension AutoCompleteSection {
}
cell.subtitleLabel.text = "@" + account.acct
cell.avatarImageView.isHidden = false
cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: account.avatar)))
cell.avatarImageView.configure(configuration: .init(url: URL(string: account.avatar)))
}
private static func configureEmoji(cell: AutoCompleteTableViewCell, emoji: Mastodon.Entity.Emoji, isFirst: Bool) {
@ -90,7 +92,7 @@ extension AutoCompleteSection {
// cell.subtitleLabel.text = isFirst ? L10n.Scene.Compose.AutoComplete.spaceToAdd : " "
cell.subtitleLabel.text = " "
cell.avatarImageView.isHidden = false
cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: emoji.url)))
cell.avatarImageView.configure(configuration: .init(url: URL(string: emoji.url)))
}
}

View File

@ -9,11 +9,12 @@ import Foundation
import Combine
import CoreData
import MastodonMeta
import CoreDataStack
/// Note: update Equatable when change case
enum ComposeStatusItem {
case replyTo(statusObjectID: NSManagedObjectID)
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
case replyTo(record: ManagedObjectRecord<Status>)
case input(replyTo: ManagedObjectRecord<Status>?, attribute: ComposeStatusAttribute)
case attachment(attachmentAttribute: ComposeStatusAttachmentAttribute)
case pollOption(pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute], pollExpiresOptionAttribute: ComposeStatusPollItem.PollExpiresOptionAttribute)
}
@ -21,26 +22,21 @@ enum ComposeStatusItem {
extension ComposeStatusItem: Hashable { }
extension ComposeStatusItem {
final class ComposeStatusAttribute: Equatable, Hashable {
final class ComposeStatusAttribute: Hashable {
private let id = UUID()
let avatarURL = CurrentValueSubject<URL?, Never>(nil)
let displayName = CurrentValueSubject<String?, Never>(nil)
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
let username = CurrentValueSubject<String?, Never>(nil)
let composeContent = CurrentValueSubject<String?, Never>(nil)
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
let contentWarningContent = CurrentValueSubject<String, Never>("")
@Published var author: ManagedObjectRecord<MastodonUser>?
@Published var composeContent: String?
@Published var isContentWarningComposing = false
@Published var contentWarningContent = ""
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
return lhs.avatarURL.value == rhs.avatarURL.value &&
lhs.displayName.value == rhs.displayName.value &&
lhs.emojiMeta.value == rhs.emojiMeta.value &&
lhs.username.value == rhs.username.value &&
lhs.composeContent.value == rhs.composeContent.value &&
lhs.isContentWarningComposing.value == rhs.isContentWarningComposing.value &&
lhs.contentWarningContent.value == rhs.contentWarningContent.value
return lhs.author == rhs.author
&& lhs.composeContent == rhs.composeContent
&& lhs.isContentWarningComposing == rhs.isContentWarningComposing
&& lhs.contentWarningContent == rhs.contentWarningContent
}
func hash(into hasher: inout Hasher) {

View File

@ -7,6 +7,8 @@
import Foundation
import Combine
import MastodonAsset
import MastodonLocalization
enum ComposeStatusPollItem {
case pollOption(attribute: PollOptionAttribute)

View File

@ -14,7 +14,7 @@ import MastodonMeta
import AlamofireImage
enum ComposeStatusSection: Equatable, Hashable {
case repliedTo
case replyTo
case status
case attachment
case poll
@ -24,43 +24,44 @@ extension ComposeStatusSection {
enum ComposeKind {
case post
case hashtag(hashtag: String)
case mention(mastodonUserObjectID: NSManagedObjectID)
case reply(repliedToStatusObjectID: NSManagedObjectID)
case mention(user: ManagedObjectRecord<MastodonUser>)
case reply(status: ManagedObjectRecord<Status>)
}
}
extension ComposeStatusSection {
static func configureStatusContent(
static func configure(
cell: ComposeStatusContentTableViewCell,
attribute: ComposeStatusItem.ComposeStatusAttribute
) {
// set avatar
attribute.avatarURL
.receive(on: DispatchQueue.main)
.sink { avatarURL in
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL))
}
.store(in: &cell.disposeBag)
// set display name and username
Publishers.CombineLatest3(
attribute.displayName,
attribute.emojiMeta,
attribute.username
)
.receive(on: DispatchQueue.main)
.sink { displayName, emojiMeta, username in
do {
let mastodonContent = MastodonContent(content: displayName ?? " ", emojis: emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.statusView.nameLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: " ")
cell.statusView.nameLabel.configure(content: metaContent)
}
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
}
.store(in: &cell.disposeBag)
// cell.prepa
// // set avatar
// attribute.avatarURL
// .receive(on: DispatchQueue.main)
// .sink { avatarURL in
// cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL))
// }
// .store(in: &cell.disposeBag)
// // set display name and username
// Publishers.CombineLatest3(
// attribute.displayName,
// attribute.emojiMeta,
// attribute.username
// )
// .receive(on: DispatchQueue.main)
// .sink { displayName, emojiMeta, username in
// do {
// let mastodonContent = MastodonContent(content: displayName ?? " ", emojis: emojiMeta)
// let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
// cell.statusView.nameLabel.configure(content: metaContent)
// } catch {
// let metaContent = PlaintextMetaContent(string: " ")
// cell.statusView.nameLabel.configure(content: metaContent)
// }
// cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
// }
// .store(in: &cell.disposeBag)
}
}

View File

@ -0,0 +1,90 @@
//
// FeedFetchedResultsController.swift
// FeedFetchedResultsController
//
// Created by Cirno MainasuK on 2021-8-19.
// Copyright © 2021 Twidere. All rights reserved.
//
import os.log
import Foundation
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
final public class FeedFetchedResultsController: NSObject {
public let logger = Logger(subsystem: "FeedFetchedResultsController", category: "DB")
var disposeBag = Set<AnyCancellable>()
public let fetchedResultsController: NSFetchedResultsController<Feed>
// input
@Published public var predicate = Feed.predicate(kind: .none, acct: .none)
// output
private let _objectIDs = PassthroughSubject<[NSManagedObjectID], Never>()
@Published public var records: [ManagedObjectRecord<Feed>] = []
public init(managedObjectContext: NSManagedObjectContext) {
self.fetchedResultsController = {
let fetchRequest = Feed.sortedFetchRequest
// make sure initial query return empty results
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.shouldRefreshRefetchedObjects = true
fetchRequest.fetchBatchSize = 15
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)
return controller
}()
super.init()
// debounce output to prevent UI update issues
_objectIDs
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
.assign(to: &$records)
fetchedResultsController.delegate = self
$predicate
.removeDuplicates()
.receive(on: DispatchQueue.main)
.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)
}
deinit {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension FeedFetchedResultsController: NSFetchedResultsControllerDelegate {
public func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
self._objectIDs.send(snapshot.itemIdentifiers)
}
}

View File

@ -21,8 +21,9 @@ final class SearchHistoryFetchedResultController: NSObject {
let userID = CurrentValueSubject<Mastodon.Entity.Status.ID?, Never>(nil)
// output
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
@Published var records: [ManagedObjectRecord<SearchHistory>] = []
init(managedObjectContext: NSManagedObjectContext) {
self.fetchedResultsController = {
let fetchRequest = SearchHistory.sortedFetchRequest
@ -38,12 +39,18 @@ final class SearchHistoryFetchedResultController: NSObject {
return controller
}()
super.init()
// debounce output to prevent UI update issues
_objectIDs
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
.assign(to: &$records)
fetchedResultsController.delegate = self
Publishers.CombineLatest(
self.domain.removeDuplicates(),
self.userID.removeDuplicates()
self.domain,
self.userID
)
.receive(on: DispatchQueue.main)
.sink { [weak self] domain, userID in
@ -67,6 +74,6 @@ extension SearchHistoryFetchedResultController: NSFetchedResultsControllerDelega
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let objects = fetchedResultsController.fetchedObjects ?? []
self.objectIDs.value = objects.map { $0.objectID }
self._objectIDs.value = objects.map { $0.objectID }
}
}

View File

@ -23,7 +23,8 @@ final class StatusFetchedResultsController: NSObject {
let statusIDs = CurrentValueSubject<[Mastodon.Entity.Status.ID], Never>([])
// output
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
@Published var records: [ManagedObjectRecord<Status>] = []
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
self.domain.value = domain ?? ""
@ -43,11 +44,17 @@ final class StatusFetchedResultsController: NSObject {
}()
super.init()
// debounce output to prevent UI update issues
_objectIDs
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
.assign(to: &$records)
fetchedResultsController.delegate = self
Publishers.CombineLatest(
self.domain.removeDuplicates().eraseToAnyPublisher(),
self.statusIDs.removeDuplicates().eraseToAnyPublisher()
self.domain.removeDuplicates(),
self.statusIDs.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] domain, ids in
@ -68,6 +75,18 @@ final class StatusFetchedResultsController: NSObject {
}
extension StatusFetchedResultsController {
public func append(statusIDs: [Mastodon.Entity.Status.ID]) {
var result = self.statusIDs.value
for statusID in statusIDs where !result.contains(statusID) {
result.append(statusID)
}
self.statusIDs.value = result
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
@ -82,6 +101,6 @@ extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
}
.sorted { $0.0 < $1.0 }
.map { $0.1.objectID }
self.objectIDs.value = items
self._objectIDs.value = items
}
}

View File

@ -23,7 +23,8 @@ final class UserFetchedResultsController: NSObject {
let userIDs = CurrentValueSubject<[Mastodon.Entity.Account.ID], Never>([])
// output
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
@Published var records: [ManagedObjectRecord<MastodonUser>] = []
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
self.domain.value = domain ?? ""
@ -42,12 +43,18 @@ final class UserFetchedResultsController: NSObject {
return controller
}()
super.init()
// debounce output to prevent UI update issues
_objectIDs
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
.assign(to: &$records)
fetchedResultsController.delegate = self
Publishers.CombineLatest(
self.domain.removeDuplicates().eraseToAnyPublisher(),
self.userIDs.removeDuplicates().eraseToAnyPublisher()
self.domain.removeDuplicates(),
self.userIDs.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] domain, ids in
@ -68,6 +75,18 @@ final class UserFetchedResultsController: NSObject {
}
extension UserFetchedResultsController {
public func append(userIDs: [Mastodon.Entity.Account.ID]) {
var result = self.userIDs.value
for userID in userIDs where !result.contains(userID) {
result.append(userID)
}
self.userIDs.value = result
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
@ -82,6 +101,6 @@ extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
}
.sorted { $0.0 < $1.0 }
.map { $0.1.objectID }
self.objectIDs.value = items
self._objectIDs.value = items
}
}

View File

@ -7,50 +7,10 @@
import CoreData
import Foundation
import CoreDataStack
enum NotificationItem {
case notification(objectID: NSManagedObjectID, attribute: Item.StatusAttribute)
case notificationStatus(objectID: NSManagedObjectID, attribute: Item.StatusAttribute) // display notification status without card wrapper
enum NotificationItem: Hashable {
case feed(record: ManagedObjectRecord<Feed>)
case feedLoader(record: ManagedObjectRecord<Feed>)
case bottomLoader
}
extension NotificationItem: Equatable {
static func == (lhs: NotificationItem, rhs: NotificationItem) -> Bool {
switch (lhs, rhs) {
case (.notification(let idLeft, _), .notification(let idRight, _)):
return idLeft == idRight
case (.notificationStatus(let idLeft, _), .notificationStatus(let idRight, _)):
return idLeft == idRight
case (.bottomLoader, .bottomLoader):
return true
default:
return false
}
}
}
extension NotificationItem: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .notification(let id, _):
hasher.combine(id)
case .notificationStatus(let id, _):
hasher.combine(id)
case .bottomLoader:
hasher.combine(String(describing: NotificationItem.bottomLoader.self))
}
}
}
extension NotificationItem {
var statusObjectItem: StatusObjectItem? {
switch self {
case .notification(let objectID, _):
return .mastodonNotification(objectID: objectID)
case .notificationStatus(let objectID, _):
return .mastodonNotification(objectID: objectID)
case .bottomLoader:
return nil
}
}
}

View File

@ -13,234 +13,290 @@ import MastodonSDK
import UIKit
import MetaTextKit
import MastodonMeta
import MastodonAsset
import MastodonLocalization
enum NotificationSection: Equatable, Hashable {
case main
}
extension NotificationSection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
dependency: NeedsDependency,
managedObjectContext: NSManagedObjectContext,
delegate: NotificationTableViewCellDelegate,
statusTableViewCellDelegate: StatusTableViewCellDelegate
struct Configuration {
weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate?
}
static func diffableDataSource(
tableView: UITableView,
context: AppContext,
configuration: Configuration
) -> UITableViewDiffableDataSource<NotificationSection, NotificationItem> {
UITableViewDiffableDataSource(tableView: tableView) {
[weak delegate, weak dependency]
(tableView, indexPath, notificationItem) -> UITableViewCell? in
guard let dependency = dependency else { return nil }
switch notificationItem {
case .notification(let objectID, let attribute):
guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
!notification.isDeleted
else { return UITableViewCell() }
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
configure(
tableView: tableView,
cell: cell,
notification: notification,
dependency: dependency,
attribute: attribute
)
cell.delegate = delegate
cell.isAccessibilityElement = true
NotificationSection.configureStatusAccessibilityLabel(cell: cell)
tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self))
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
switch item {
case .feed(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell
context.managedObjectContext.performAndWait {
guard let feed = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)),
configuration: configuration
)
}
return cell
case .notificationStatus(objectID: let objectID, attribute: let attribute):
guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
!notification.isDeleted,
let status = notification.status,
let requestUserID = dependency.context.authenticationService.activeMastodonAuthenticationBox.value?.userID
else { return UITableViewCell() }
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
// configure cell
StatusSection.configureStatusTableViewCell(
cell: cell,
tableView: tableView,
timelineContext: .notifications,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
status: status,
requestUserID: requestUserID,
statusItemAttribute: attribute
)
cell.statusView.headerContainerView.isHidden = true // set header hide
cell.statusView.actionToolbarContainer.isHidden = true // set toolbar hide
cell.statusView.actionToolbarPlaceholderPaddingView.isHidden = false
cell.delegate = statusTableViewCellDelegate
cell.isAccessibilityElement = true
StatusSection.configureStatusAccessibilityLabel(cell: cell)
return cell
case .feedLoader(let record):
return UITableViewCell()
case .bottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
cell.startAnimating()
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
cell.activityIndicatorView.startAnimating()
return cell
}
// switch notificationItem {
// case .notification(let objectID, let attribute):
// guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
// !notification.isDeleted
// else { return UITableViewCell() }
//
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
// configure(
// tableView: tableView,
// cell: cell,
// notification: notification,
// dependency: dependency,
// attribute: attribute
// )
// cell.delegate = delegate
// cell.isAccessibilityElement = true
// NotificationSection.configureStatusAccessibilityLabel(cell: cell)
// return cell
//
// case .notificationStatus(objectID: let objectID, attribute: let attribute):
// guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
// !notification.isDeleted,
// let status = notification.status,
// let requestUserID = dependency.context.authenticationService.activeMastodonAuthenticationBox.value?.userID
// else { return UITableViewCell() }
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
//
// // configure cell
// StatusSection.configureStatusTableViewCell(
// cell: cell,
// tableView: tableView,
// timelineContext: .notifications,
// dependency: dependency,
// readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
// status: status,
// requestUserID: requestUserID,
// statusItemAttribute: attribute
// )
// cell.statusView.headerContainerView.isHidden = true // set header hide
// cell.statusView.actionToolbarContainer.isHidden = true // set toolbar hide
// cell.statusView.actionToolbarPlaceholderPaddingView.isHidden = false
// cell.delegate = statusTableViewCellDelegate
// cell.isAccessibilityElement = true
// StatusSection.configureStatusAccessibilityLabel(cell: cell)
// return cell
//
// case .bottomLoader:
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
// cell.startAnimating()
// return cell
// }
}
}
}
extension NotificationSection {
static func configure(
context: AppContext,
tableView: UITableView,
cell: NotificationStatusTableViewCell,
notification: MastodonNotification,
dependency: NeedsDependency,
attribute: Item.StatusAttribute
cell: NotificationTableViewCell,
viewModel: NotificationTableViewCell.ViewModel,
configuration: Configuration
) {
// configure author
cell.configure(
with: AvatarConfigurableViewConfiguration(
avatarImageURL: notification.account.avatarImageURL()
)
StatusSection.setupStatusPollDataSource(
context: context,
statusView: cell.notificationView.statusView
)
func createActionImage() -> UIImage? {
return UIImage(
systemName: notification.notificationType.actionImageName,
withConfiguration: UIImage.SymbolConfiguration(
pointSize: 12, weight: .semibold
)
)?
.withTintColor(.systemBackground)
.af.imageAspectScaled(toFit: CGSize(width: 14, height: 14))
}
StatusSection.setupStatusPollDataSource(
context: context,
statusView: cell.notificationView.quoteStatusView
)
cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color
cell.avatarButton.badgeImageView.image = createActionImage()
cell.traitCollectionDidChange
.receive(on: DispatchQueue.main)
.sink { [weak cell] in
guard let cell = cell else { return }
cell.avatarButton.badgeImageView.image = createActionImage()
}
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.notificationView.viewModel)
.store(in: &cell.disposeBag)
// configure author name, notification description, timestamp
let nameText = notification.account.displayNameWithFallback
let titleLabelText: String = {
switch notification.notificationType {
case .favourite: return L10n.Scene.Notification.userFavoritedYourPost(nameText)
case .follow: return L10n.Scene.Notification.userFollowedYou(nameText)
case .followRequest: return L10n.Scene.Notification.userRequestedToFollowYou(nameText)
case .mention: return L10n.Scene.Notification.userMentionedYou(nameText)
case .poll: return L10n.Scene.Notification.userYourPollHasEnded(nameText)
case .reblog: return L10n.Scene.Notification.userRebloggedYourPost(nameText)
default: return ""
}
}()
do {
let nameContent = MastodonContent(content: nameText, emojis: notification.account.emojiMeta)
let nameMetaContent = try MastodonMetaContent.convert(document: nameContent)
let mastodonContent = MastodonContent(content: titleLabelText, emojis: notification.account.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.titleLabel.configure(content: metaContent)
if let nameRange = metaContent.string.range(of: nameMetaContent.string) {
let nsRange = NSRange(nameRange, in: metaContent.string)
cell.titleLabel.textStorage.addAttributes([
.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20),
.foregroundColor: Asset.Colors.brandBlue.color,
], range: nsRange)
}
} catch {
let metaContent = PlaintextMetaContent(string: titleLabelText)
cell.titleLabel.configure(content: metaContent)
}
let createAt = notification.createAt
cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
AppContext.shared.timestampUpdatePublisher
.receive(on: DispatchQueue.main)
.sink { [weak cell] _ in
guard let cell = cell else { return }
cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
}
.store(in: &cell.disposeBag)
// configure follow request (if exist)
if case .followRequest = notification.notificationType {
cell.acceptButton.publisher(for: .touchUpInside)
.sink { [weak cell] _ in
guard let cell = cell else { return }
cell.delegate?.notificationTableViewCell(cell, notification: notification, acceptButtonDidPressed: cell.acceptButton)
}
.store(in: &cell.disposeBag)
cell.rejectButton.publisher(for: .touchUpInside)
.sink { [weak cell] _ in
guard let cell = cell else { return }
cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.rejectButton)
}
.store(in: &cell.disposeBag)
cell.buttonStackView.isHidden = false
} else {
cell.buttonStackView.isHidden = true
}
// configure status (if exist)
if let status = notification.status {
let frame = CGRect(
x: 0,
y: 0,
width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right,
height: tableView.readableContentGuide.layoutFrame.height
)
StatusSection.configure(
cell: cell,
tableView: tableView,
timelineContext: .notifications,
dependency: dependency,
readableLayoutFrame: frame,
status: status,
requestUserID: notification.userID,
statusItemAttribute: attribute
)
cell.statusContainerView.isHidden = false
cell.containerStackView.alignment = .top
cell.containerStackViewBottomLayoutConstraint.constant = 0
} else {
if case .followRequest = notification.notificationType {
cell.containerStackView.alignment = .top
} else {
cell.containerStackView.alignment = .center
}
cell.statusContainerView.isHidden = true
cell.containerStackViewBottomLayoutConstraint.constant = 5 // 5pt margin when no status view
}
cell.configure(
tableView: tableView,
viewModel: viewModel,
delegate: configuration.notificationTableViewCellDelegate
)
}
static func configureStatusAccessibilityLabel(cell: NotificationStatusTableViewCell) {
// FIXME:
cell.accessibilityLabel = {
var accessibilityViews: [UIView?] = []
accessibilityViews.append(contentsOf: [
cell.titleLabel,
cell.timestampLabel,
cell.statusView
])
if !cell.statusContainerView.isHidden {
if !cell.statusView.headerContainerView.isHidden {
accessibilityViews.append(cell.statusView.headerInfoLabel)
}
accessibilityViews.append(contentsOf: [
cell.statusView.nameMetaLabel,
cell.statusView.dateLabel,
cell.statusView.contentMetaText.textView,
])
}
return accessibilityViews
.compactMap { $0?.accessibilityLabel }
.joined(separator: " ")
}()
}
// static func configure(
// tableView: UITableView,
// cell: NotificationStatusTableViewCell,
// notification: MastodonNotification,
// dependency: NeedsDependency,
// attribute: Item.StatusAttribute
// ) {
// // configure author
// cell.configure(
// with: AvatarConfigurableViewConfiguration(
// avatarImageURL: notification.account.avatarImageURL()
// )
// )
//
// func createActionImage() -> UIImage? {
// return UIImage(
// systemName: notification.notificationType.actionImageName,
// withConfiguration: UIImage.SymbolConfiguration(
// pointSize: 12, weight: .semibold
// )
// )?
// .withTintColor(.systemBackground)
// .af.imageAspectScaled(toFit: CGSize(width: 14, height: 14))
// }
//
// cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color
// cell.avatarButton.badgeImageView.image = createActionImage()
// cell.traitCollectionDidChange
// .receive(on: DispatchQueue.main)
// .sink { [weak cell] in
// guard let cell = cell else { return }
// cell.avatarButton.badgeImageView.image = createActionImage()
// }
// .store(in: &cell.disposeBag)
//
// // configure author name, notification description, timestamp
// let nameText = notification.account.displayNameWithFallback
// let titleLabelText: String = {
// switch notification.notificationType {
// case .favourite: return L10n.Scene.Notification.userFavoritedYourPost(nameText)
// case .follow: return L10n.Scene.Notification.userFollowedYou(nameText)
// case .followRequest: return L10n.Scene.Notification.userRequestedToFollowYou(nameText)
// case .mention: return L10n.Scene.Notification.userMentionedYou(nameText)
// case .poll: return L10n.Scene.Notification.userYourPollHasEnded(nameText)
// case .reblog: return L10n.Scene.Notification.userRebloggedYourPost(nameText)
// default: return ""
// }
// }()
//
// do {
// let nameContent = MastodonContent(content: nameText, emojis: notification.account.emojiMeta)
// let nameMetaContent = try MastodonMetaContent.convert(document: nameContent)
//
// let mastodonContent = MastodonContent(content: titleLabelText, emojis: notification.account.emojiMeta)
// let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
//
// cell.titleLabel.configure(content: metaContent)
//
// if let nameRange = metaContent.string.range(of: nameMetaContent.string) {
// let nsRange = NSRange(nameRange, in: metaContent.string)
// cell.titleLabel.textStorage.addAttributes([
// .font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20),
// .foregroundColor: Asset.Colors.brandBlue.color,
// ], range: nsRange)
// }
//
// } catch {
// let metaContent = PlaintextMetaContent(string: titleLabelText)
// cell.titleLabel.configure(content: metaContent)
// }
//
// let createAt = notification.createAt
// cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
// AppContext.shared.timestampUpdatePublisher
// .receive(on: DispatchQueue.main)
// .sink { [weak cell] _ in
// guard let cell = cell else { return }
// cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
// }
// .store(in: &cell.disposeBag)
//
// // configure follow request (if exist)
// if case .followRequest = notification.notificationType {
// cell.acceptButton.publisher(for: .touchUpInside)
// .sink { [weak cell] _ in
// guard let cell = cell else { return }
// cell.delegate?.notificationTableViewCell(cell, notification: notification, acceptButtonDidPressed: cell.acceptButton)
// }
// .store(in: &cell.disposeBag)
// cell.rejectButton.publisher(for: .touchUpInside)
// .sink { [weak cell] _ in
// guard let cell = cell else { return }
// cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.rejectButton)
// }
// .store(in: &cell.disposeBag)
// cell.buttonStackView.isHidden = false
// } else {
// cell.buttonStackView.isHidden = true
// }
//
// // configure status (if exist)
// if let status = notification.status {
// let frame = CGRect(
// x: 0,
// y: 0,
// width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right,
// height: tableView.readableContentGuide.layoutFrame.height
// )
// StatusSection.configure(
// cell: cell,
// tableView: tableView,
// timelineContext: .notifications,
// dependency: dependency,
// readableLayoutFrame: frame,
// status: status,
// requestUserID: notification.userID,
// statusItemAttribute: attribute
// )
// cell.statusContainerView.isHidden = false
// cell.containerStackView.alignment = .top
// cell.containerStackViewBottomLayoutConstraint.constant = 0
// } else {
// if case .followRequest = notification.notificationType {
// cell.containerStackView.alignment = .top
// } else {
// cell.containerStackView.alignment = .center
// }
// cell.statusContainerView.isHidden = true
// cell.containerStackViewBottomLayoutConstraint.constant = 5 // 5pt margin when no status view
// }
// }
//
// static func configureStatusAccessibilityLabel(cell: NotificationStatusTableViewCell) {
// // FIXME:
// cell.accessibilityLabel = {
// var accessibilityViews: [UIView?] = []
// accessibilityViews.append(contentsOf: [
// cell.titleLabel,
// cell.timestampLabel,
// cell.statusView
// ])
// if !cell.statusContainerView.isHidden {
// if !cell.statusView.headerContainerView.isHidden {
// accessibilityViews.append(cell.statusView.headerInfoLabel)
// }
// accessibilityViews.append(contentsOf: [
// cell.statusView.nameMetaLabel,
// cell.statusView.dateLabel,
// cell.statusView.contentMetaText.textView,
// ])
// }
// return accessibilityViews
// .compactMap { $0?.accessibilityLabel }
// .joined(separator: " ")
// }()
// }
}

View File

@ -7,6 +7,8 @@
import Foundation
import MastodonSDK
import MastodonAsset
import MastodonLocalization
/// Note: update Equatable when change case
enum CategoryPickerItem {

View File

@ -6,6 +6,8 @@
//
import UIKit
import MastodonAsset
import MastodonLocalization
enum CategoryPickerSection: Equatable, Hashable {
case main

View File

@ -6,6 +6,8 @@
//
import UIKit
import MastodonAsset
import MastodonLocalization
enum ServerRuleSection: Hashable {
case header

View File

@ -1,68 +0,0 @@
//
// PollItem.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-2.
//
import Foundation
import CoreData
/// Note: update Equatable when change case
enum PollItem {
case option(objectID: NSManagedObjectID, attribute: Attribute)
}
extension PollItem {
class Attribute: Hashable {
enum SelectState: Equatable, Hashable {
case none
case off
case on
}
enum VoteState: Equatable, Hashable {
case hidden
case reveal(voted: Bool, percentage: Double, animated: Bool)
}
var selectState: SelectState
var voteState: VoteState
init(selectState: SelectState, voteState: VoteState) {
self.selectState = selectState
self.voteState = voteState
}
static func == (lhs: PollItem.Attribute, rhs: PollItem.Attribute) -> Bool {
return lhs.selectState == rhs.selectState &&
lhs.voteState == rhs.voteState
}
func hash(into hasher: inout Hasher) {
hasher.combine(selectState)
hasher.combine(voteState)
}
}
}
extension PollItem: Equatable {
static func == (lhs: PollItem, rhs: PollItem) -> Bool {
switch (lhs, rhs) {
case (.option(let objectIDLeft, _), .option(let objectIDRight, _)):
return objectIDLeft == objectIDRight
}
}
}
extension PollItem: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .option(let objectID, _):
hasher.combine(objectID)
}
}
}

View File

@ -1,120 +0,0 @@
//
// PollSection.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-2.
//
import UIKit
import CoreData
import CoreDataStack
import MastodonSDK
extension Mastodon.Entity.Attachment: Hashable {
public static func == (lhs: Mastodon.Entity.Attachment, rhs: Mastodon.Entity.Attachment) -> Bool {
return lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
enum PollSection: Equatable, Hashable {
case main
}
extension PollSection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
managedObjectContext: NSManagedObjectContext
) -> UITableViewDiffableDataSource<PollSection, PollItem> {
return UITableViewDiffableDataSource<PollSection, PollItem>(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
switch item {
case .option(let objectID, let attribute):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self), for: indexPath) as! PollOptionTableViewCell
managedObjectContext.performAndWait {
let option = managedObjectContext.object(with: objectID) as! PollOption
PollSection.configure(cell: cell, pollOption: option, pollItemAttribute: attribute)
cell.isAccessibilityElement = true
cell.accessibilityLabel = {
var labels: [String] = [option.title]
if let percentage = cell.pollOptionView.optionPercentageLabel.text {
labels.append(percentage)
}
return labels.joined(separator: ",")
}()
}
return cell
}
}
}
}
extension PollSection {
static func configure(
cell: PollOptionTableViewCell,
pollOption option: PollOption,
pollItemAttribute attribute: PollItem.Attribute
) {
cell.pollOptionView.optionTextField.text = option.title
configure(cell: cell, selectState: attribute.selectState)
configure(cell: cell, voteState: attribute.voteState)
cell.attribute = attribute
cell.layoutIfNeeded()
cell.updateTextAppearance()
}
}
extension PollSection {
static func configure(cell: PollOptionTableViewCell, selectState state: PollItem.Attribute.SelectState) {
switch state {
case .none:
cell.pollOptionView.checkmarkBackgroundView.isHidden = true
cell.pollOptionView.checkmarkImageView.isHidden = true
case .off:
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak cell] theme in
guard let cell = cell else { return }
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = theme.tertiarySystemBackgroundColor
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = theme.tableViewCellSelectionBackgroundColor.withAlphaComponent(0.3).cgColor
}
.store(in: &cell.disposeBag)
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
cell.pollOptionView.checkmarkImageView.isHidden = true
case .on:
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak cell] theme in
guard let cell = cell else { return }
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = theme.tertiarySystemBackgroundColor
}
.store(in: &cell.disposeBag)
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 0
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
cell.pollOptionView.checkmarkImageView.isHidden = false
}
}
static func configure(cell: PollOptionTableViewCell, voteState state: PollItem.Attribute.VoteState) {
switch state {
case .hidden:
cell.pollOptionView.optionPercentageLabel.isHidden = true
cell.pollOptionView.voteProgressStripView.isHidden = true
cell.pollOptionView.voteProgressStripView.setProgress(0.0, animated: false)
case .reveal(let voted, let percentage, let animated):
cell.pollOptionView.optionPercentageLabel.isHidden = false
cell.pollOptionView.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
cell.pollOptionView.voteProgressStripView.isHidden = false
cell.pollOptionView.voteProgressStripView.tintColor = voted ? Asset.Colors.brandBlue.color : Asset.Colors.Poll.disabled.color
cell.pollOptionView.voteProgressStripView.setProgress(CGFloat(percentage), animated: animated)
}
}
}

View File

@ -10,24 +10,10 @@ import Combine
import MastodonSDK
import MastodonMeta
enum ProfileFieldItem {
case field(field: FieldValue, attribute: FieldItemAttribute)
case addEntry(attribute: AddEntryItemAttribute)
}
protocol ProfileFieldListSeparatorLineConfigurable: AnyObject {
var isLast: Bool { get set }
}
extension ProfileFieldItem {
var listSeparatorLineConfigurable: ProfileFieldListSeparatorLineConfigurable? {
switch self {
case .field(_, let attribute):
return attribute
case .addEntry(let attribute):
return attribute
}
}
enum ProfileFieldItem: Hashable {
case field(field: FieldValue)
case editField(field: FieldValue)
case addEntry
}
extension ProfileFieldItem {
@ -36,17 +22,29 @@ extension ProfileFieldItem {
var name: CurrentValueSubject<String, Never>
var value: CurrentValueSubject<String, Never>
let emojiMeta: MastodonContent.Emojis
init(id: UUID = UUID(), name: String, value: String) {
init(
id: UUID = UUID(),
name: String,
value: String,
emojiMeta: MastodonContent.Emojis
) {
self.id = id
self.name = CurrentValueSubject(name)
self.value = CurrentValueSubject(value)
self.emojiMeta = emojiMeta
}
static func == (lhs: ProfileFieldItem.FieldValue, rhs: ProfileFieldItem.FieldValue) -> Bool {
static func == (
lhs: ProfileFieldItem.FieldValue,
rhs: ProfileFieldItem.FieldValue
) -> Bool {
return lhs.id == rhs.id
&& lhs.name.value == rhs.name.value
&& lhs.value.value == rhs.value.value
&& lhs.emojiMeta == rhs.emojiMeta
}
func hash(into hasher: inout Hasher) {
@ -54,50 +52,3 @@ extension ProfileFieldItem {
}
}
}
extension ProfileFieldItem {
class FieldItemAttribute: Equatable, ProfileFieldListSeparatorLineConfigurable {
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
var isEditing = false
var isLast = false
static func == (lhs: ProfileFieldItem.FieldItemAttribute, rhs: ProfileFieldItem.FieldItemAttribute) -> Bool {
return lhs.isEditing == rhs.isEditing
&& lhs.isLast == rhs.isLast
}
}
class AddEntryItemAttribute: Equatable, ProfileFieldListSeparatorLineConfigurable {
var isLast = false
static func == (lhs: ProfileFieldItem.AddEntryItemAttribute, rhs: ProfileFieldItem.AddEntryItemAttribute) -> Bool {
return lhs.isLast == rhs.isLast
}
}
}
extension ProfileFieldItem: Equatable {
static func == (lhs: ProfileFieldItem, rhs: ProfileFieldItem) -> Bool {
switch (lhs, rhs) {
case (.field(let fieldLeft, let attributeLeft), .field(let fieldRight, let attributeRight)):
return fieldLeft.id == fieldRight.id
&& attributeLeft == attributeRight
case (.addEntry(let attributeLeft), .addEntry(let attributeRight)):
return attributeLeft == attributeRight
default:
return false
}
}
}
extension ProfileFieldItem: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .field(let field, _):
hasher.combine(field.id)
case .addEntry:
hasher.combine(String(describing: ProfileFieldItem.addEntry.self))
}
}
}

View File

@ -9,125 +9,124 @@ import os
import UIKit
import Combine
import MastodonMeta
import MastodonLocalization
enum ProfileFieldSection: Equatable, Hashable {
case main
}
extension ProfileFieldSection {
static func collectionViewDiffableDataSource(
for collectionView: UICollectionView,
profileFieldCollectionViewCellDelegate: ProfileFieldCollectionViewCellDelegate,
profileFieldAddEntryCollectionViewCellDelegate: ProfileFieldAddEntryCollectionViewCellDelegate
struct Configuration {
weak var profileFieldCollectionViewCellDelegate: ProfileFieldCollectionViewCellDelegate?
weak var profileFieldEditCollectionViewCellDelegate: ProfileFieldEditCollectionViewCellDelegate?
}
static func diffableDataSource(
collectionView: UICollectionView,
context: AppContext,
configuration: Configuration
) -> UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem> {
let dataSource = UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>(collectionView: collectionView) {
[
weak profileFieldCollectionViewCellDelegate,
weak profileFieldAddEntryCollectionViewCellDelegate
] collectionView, indexPath, item in
collectionView.register(ProfileFieldCollectionViewHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer)
collectionView.register(ProfileFieldCollectionViewHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.footerReuseIdentifer)
let fieldCellRegistration = UICollectionView.CellRegistration<ProfileFieldCollectionViewCell, ProfileFieldItem> { cell, indexPath, item in
guard case let .field(field) = item else { return }
// set key
do {
let mastodonContent = MastodonContent(content: field.name.value, emojis: field.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.keyMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: field.name.value)
cell.keyMetaLabel.configure(content: content)
}
// set value
do {
let mastodonContent = MastodonContent(content: field.value.value, emojis: field.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.valueMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: field.value.value)
cell.valueMetaLabel.configure(content: content)
}
// set background
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell()
backgroundConfiguration.backgroundColor = UIColor.secondarySystemBackground
cell.backgroundConfiguration = backgroundConfiguration
cell.delegate = configuration.profileFieldCollectionViewCellDelegate
}
let editFieldCellRegistration = UICollectionView.CellRegistration<ProfileFieldEditCollectionViewCell, ProfileFieldItem> { cell, indexPath, item in
guard case let .editField(field) = item else { return }
cell.keyTextField.text = field.name.value
cell.valueTextField.text = field.value.value
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.keyTextField)
.compactMap { $0.object as? UITextField }
.map { $0.text ?? "" }
.removeDuplicates()
.assign(to: \.value, on: field.name)
.store(in: &cell.disposeBag)
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.valueTextField)
.compactMap { $0.object as? UITextField }
.map { $0.text ?? "" }
.removeDuplicates()
.assign(to: \.value, on: field.value)
.store(in: &cell.disposeBag)
// set background
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell()
backgroundConfiguration.backgroundColor = UIColor.secondarySystemBackground
cell.backgroundConfiguration = backgroundConfiguration
cell.delegate = configuration.profileFieldEditCollectionViewCellDelegate
}
let addEntryCellRegistration = UICollectionView.CellRegistration<ProfileFieldAddEntryCollectionViewCell, ProfileFieldItem> { cell, indexPath, item in
guard case .addEntry = item else { return }
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell()
backgroundConfiguration.backgroundColorTransformer = .init { [weak cell] _ in
guard let cell = cell else {
return .secondarySystemBackground
}
let state = cell.configurationState
if state.isHighlighted || state.isSelected {
return .secondarySystemBackground.withAlphaComponent(0.5)
} else {
return .secondarySystemBackground
}
}
cell.backgroundConfiguration = backgroundConfiguration
}
let dataSource = UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>(collectionView: collectionView) { collectionView, indexPath, item in
switch item {
case .field(let field, let attribute):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell
// set key
do {
let mastodonContent = MastodonContent(content: field.name.value, emojis: attribute.emojiMeta.value)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.fieldView.titleMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: field.name.value)
cell.fieldView.titleMetaLabel.configure(content: content)
}
cell.fieldView.titleTextField.text = field.name.value
Publishers.CombineLatest(
field.name.removeDuplicates(),
attribute.emojiMeta.removeDuplicates()
case .field:
return collectionView.dequeueConfiguredReusableCell(
using: fieldCellRegistration,
for: indexPath,
item: item
)
.receive(on: RunLoop.main)
.sink { [weak cell] name, emojiMeta in
guard let cell = cell else { return }
do {
let mastodonContent = MastodonContent(content: name, emojis: emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.fieldView.titleMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: name)
cell.fieldView.titleMetaLabel.configure(content: content)
}
// only bind label. The text field should only set once
}
.store(in: &cell.disposeBag)
// set value
do {
let mastodonContent = MastodonContent(content: field.value.value, emojis: attribute.emojiMeta.value)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.fieldView.valueMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: field.value.value)
cell.fieldView.valueMetaLabel.configure(content: content)
}
cell.fieldView.valueTextField.text = field.value.value
Publishers.CombineLatest(
field.value.removeDuplicates(),
attribute.emojiMeta.removeDuplicates()
case .editField:
return collectionView.dequeueConfiguredReusableCell(
using: editFieldCellRegistration,
for: indexPath,
item: item
)
case .addEntry:
return collectionView.dequeueConfiguredReusableCell(
using: addEntryCellRegistration,
for: indexPath,
item: item
)
.receive(on: RunLoop.main)
.sink { [weak cell] value, emojiMeta in
guard let cell = cell else { return }
do {
let mastodonContent = MastodonContent(content: value, emojis: emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.fieldView.valueMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: value)
cell.fieldView.valueMetaLabel.configure(content: content)
}
// only bind label. The text field should only set once
}
.store(in: &cell.disposeBag)
// bind editing
if attribute.isEditing {
cell.fieldView.name
.removeDuplicates()
.receive(on: RunLoop.main)
.assign(to: \.value, on: field.name)
.store(in: &cell.disposeBag)
cell.fieldView.value
.removeDuplicates()
.receive(on: RunLoop.main)
.assign(to: \.value, on: field.value)
.store(in: &cell.disposeBag)
}
// setup editing state
cell.fieldView.titleTextField.isHidden = !attribute.isEditing
cell.fieldView.valueTextField.isHidden = !attribute.isEditing
cell.fieldView.titleMetaLabel.isHidden = attribute.isEditing
cell.fieldView.valueMetaLabel.isHidden = attribute.isEditing
// set control hidden
let isHidden = !attribute.isEditing
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update editing state: %s", ((#file as NSString).lastPathComponent), #line, #function, isHidden ? "true" : "false")
cell.editButton.isHidden = isHidden
cell.reorderBarImageView.isHidden = isHidden
// update separator line
cell.bottomSeparatorLine.isHidden = attribute.isLast
cell.delegate = profileFieldCollectionViewCellDelegate
return cell
case .addEntry(let attribute):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldAddEntryCollectionViewCell.self), for: indexPath) as! ProfileFieldAddEntryCollectionViewCell
cell.bottomSeparatorLine.isHidden = attribute.isLast
cell.delegate = profileFieldAddEntryCollectionViewCellDelegate
return cell
}
}
@ -135,6 +134,7 @@ extension ProfileFieldSection {
switch kind {
case UICollectionView.elementKindSectionHeader:
let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer, for: indexPath) as! ProfileFieldCollectionViewHeaderFooterView
reusableView.frame.size.height = 20
return reusableView
case UICollectionView.elementKindSectionFooter:
let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.footerReuseIdentifer, for: indexPath) as! ProfileFieldCollectionViewHeaderFooterView

View File

@ -0,0 +1,150 @@
//
// RecommendAccountSection.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/1.
//
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
import MetaTextKit
import MastodonMeta
import Combine
enum RecommendAccountSection: Equatable, Hashable {
case main
}
//extension RecommendAccountSection {
// static func collectionViewDiffableDataSource(
// for collectionView: UICollectionView,
// dependency: NeedsDependency,
// delegate: SearchRecommendAccountsCollectionViewCellDelegate,
// managedObjectContext: NSManagedObjectContext
// ) -> UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
// UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in
// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell
// managedObjectContext.performAndWait {
// let user = managedObjectContext.object(with: objectID) as! MastodonUser
// configure(cell: cell, user: user, dependency: dependency)
// }
// cell.delegate = delegate
// return cell
// }
// }
//
// static func configure(
// cell: SearchRecommendAccountsCollectionViewCell,
// user: MastodonUser,
// dependency: NeedsDependency
// ) {
// configureContent(cell: cell, user: user)
//
// if let currentMastodonUser = dependency.context.authenticationService.activeMastodonAuthentication.value?.user {
// configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
// }
//
// Publishers.CombineLatest(
// ManagedObjectObserver.observe(object: user).eraseToAnyPublisher().mapError { $0 as Error },
// dependency.context.authenticationService.activeMastodonAuthentication.setFailureType(to: Error.self)
// )
// .receive(on: DispatchQueue.main)
// .sink { _ in
// // do nothing
// } receiveValue: { [weak cell] change, authentication in
// guard let cell = cell else { return }
// guard case .update(let object) = change.changeType,
// let user = object as? MastodonUser else { return }
// guard let currentMastodonUser = authentication?.user else { return }
//
// configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
// }
// .store(in: &cell.disposeBag)
//
// }
//
// static func configureContent(
// cell: SearchRecommendAccountsCollectionViewCell,
// user: MastodonUser
// ) {
// do {
// let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary)
// let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
// cell.displayNameLabel.configure(content: metaContent)
// } catch {
// let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
// cell.displayNameLabel.configure(content: metaContent)
// }
// cell.acctLabel.text = "@" + user.acct
// cell.avatarImageView.af.setImage(
// withURL: user.avatarImageURLWithFallback(domain: user.domain),
// placeholderImage: UIImage.placeholder(color: .systemFill),
// imageTransition: .crossDissolve(0.2)
// )
// cell.headerImageView.af.setImage(
// withURL: URL(string: user.header)!,
// placeholderImage: UIImage.placeholder(color: .systemFill),
// imageTransition: .crossDissolve(0.2)
// )
// }
//
// static func configureFollowButton(
// with mastodonUser: MastodonUser,
// currentMastodonUser: MastodonUser,
// followButton: HighlightDimmableButton
// ) {
// let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
// followButton.setTitle(relationshipActionSet.title, for: .normal)
// }
//
// static func relationShipActionSet(
// mastodonUser: MastodonUser,
// currentMastodonUser: MastodonUser
// ) -> ProfileViewModel.RelationshipActionOptionSet {
// var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow])
// let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
// if isFollowing {
// relationshipActionSet.insert(.following)
// }
//
// let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
// if isPending {
// relationshipActionSet.insert(.pending)
// }
//
// let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
// if isBlocking {
// relationshipActionSet.insert(.blocking)
// }
//
// let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
// if isBlockedBy {
// relationshipActionSet.insert(.blocked)
// }
// return relationshipActionSet
// }
//
//}
//
//extension RecommendAccountSection {
//
// static func tableViewDiffableDataSource(
// for tableView: UITableView,
// managedObjectContext: NSManagedObjectContext,
// viewModel: SuggestionAccountViewModel,
// delegate: SuggestionAccountTableViewCellDelegate
// ) -> UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
// UITableViewDiffableDataSource(tableView: tableView) { [weak viewModel, weak delegate] (tableView, indexPath, objectID) -> UITableViewCell? in
// guard let viewModel = viewModel else { return nil }
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
// let user = managedObjectContext.object(with: objectID) as! MastodonUser
// let isSelected = viewModel.selectedAccounts.value.contains(objectID)
// cell.delegate = delegate
// cell.config(with: user, isSelected: isSelected)
// return cell
// }
// }
//}

View File

@ -0,0 +1,12 @@
//
// ReportItem.swift
// Mastodon
//
// Created by MainasuK on 2022-1-27.
//
import Foundation
enum ReportItem: Hashable {
}

View File

@ -0,0 +1,70 @@
//
// ReportSection.swift
// Mastodon
//
// Created by ihugo on 2021/4/20.
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
import AVKit
import os.log
import MastodonAsset
import MastodonLocalization
enum ReportSection: Equatable, Hashable {
case main
}
extension ReportSection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
dependency: ReportViewController,
managedObjectContext: NSManagedObjectContext,
timestampUpdatePublisher: AnyPublisher<Date, Never>
) -> UITableViewDiffableDataSource<ReportSection, ReportItem> {
UITableViewDiffableDataSource(tableView: tableView) {[
weak dependency
] tableView, indexPath, item -> UITableViewCell? in
return UITableViewCell()
guard let dependency = dependency else { return UITableViewCell() }
// switch item {
// case .reportStatus(let objectID, let attribute):
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportedStatusTableViewCell.self), for: indexPath) as! ReportedStatusTableViewCell
// cell.dependency = dependency
// let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
// let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
// managedObjectContext.performAndWait { [weak dependency] in
// guard let dependency = dependency else { return }
// let status = managedObjectContext.object(with: objectID) as! Status
// StatusSection.configure(
// cell: cell,
// tableView: tableView,
// timelineContext: .report,
// dependency: dependency,
// readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
// status: status,
// requestUserID: requestUserID,
// statusItemAttribute: attribute
// )
// }
//
// // defalut to select the report status
// if attribute.isSelected {
// tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
// } else {
// tableView.deselectRow(at: indexPath, animated: false)
// }
//
// return cell
// default:
// return nil
// }
}
}
}

View File

@ -1,150 +0,0 @@
//
// RecommendAccountSection.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/1.
//
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
import MetaTextKit
import MastodonMeta
import Combine
enum RecommendAccountSection: Equatable, Hashable {
case main
}
extension RecommendAccountSection {
static func collectionViewDiffableDataSource(
for collectionView: UICollectionView,
dependency: NeedsDependency,
delegate: SearchRecommendAccountsCollectionViewCellDelegate,
managedObjectContext: NSManagedObjectContext
) -> UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell
managedObjectContext.performAndWait {
let user = managedObjectContext.object(with: objectID) as! MastodonUser
configure(cell: cell, user: user, dependency: dependency)
}
cell.delegate = delegate
return cell
}
}
static func configure(
cell: SearchRecommendAccountsCollectionViewCell,
user: MastodonUser,
dependency: NeedsDependency
) {
configureContent(cell: cell, user: user)
if let currentMastodonUser = dependency.context.authenticationService.activeMastodonAuthentication.value?.user {
configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
}
Publishers.CombineLatest(
ManagedObjectObserver.observe(object: user).eraseToAnyPublisher().mapError { $0 as Error },
dependency.context.authenticationService.activeMastodonAuthentication.setFailureType(to: Error.self)
)
.receive(on: DispatchQueue.main)
.sink { _ in
// do nothing
} receiveValue: { [weak cell] change, authentication in
guard let cell = cell else { return }
guard case .update(let object) = change.changeType,
let user = object as? MastodonUser else { return }
guard let currentMastodonUser = authentication?.user else { return }
configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
}
.store(in: &cell.disposeBag)
}
static func configureContent(
cell: SearchRecommendAccountsCollectionViewCell,
user: MastodonUser
) {
do {
let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.displayNameLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
cell.displayNameLabel.configure(content: metaContent)
}
cell.acctLabel.text = "@" + user.acct
cell.avatarImageView.af.setImage(
withURL: user.avatarImageURLWithFallback(domain: user.domain),
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
cell.headerImageView.af.setImage(
withURL: URL(string: user.header)!,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
}
static func configureFollowButton(
with mastodonUser: MastodonUser,
currentMastodonUser: MastodonUser,
followButton: HighlightDimmableButton
) {
let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
followButton.setTitle(relationshipActionSet.title, for: .normal)
}
static func relationShipActionSet(
mastodonUser: MastodonUser,
currentMastodonUser: MastodonUser
) -> ProfileViewModel.RelationshipActionOptionSet {
var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow])
let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
if isFollowing {
relationshipActionSet.insert(.following)
}
let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
if isPending {
relationshipActionSet.insert(.pending)
}
let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
if isBlocking {
relationshipActionSet.insert(.blocking)
}
let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
if isBlockedBy {
relationshipActionSet.insert(.blocked)
}
return relationshipActionSet
}
}
extension RecommendAccountSection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
managedObjectContext: NSManagedObjectContext,
viewModel: SuggestionAccountViewModel,
delegate: SuggestionAccountTableViewCellDelegate
) -> UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
UITableViewDiffableDataSource(tableView: tableView) { [weak viewModel, weak delegate] (tableView, indexPath, objectID) -> UITableViewCell? in
guard let viewModel = viewModel else { return nil }
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
let user = managedObjectContext.object(with: objectID) as! MastodonUser
let isSelected = viewModel.selectedAccounts.value.contains(objectID)
cell.delegate = delegate
cell.config(with: user, isSelected: isSelected)
return cell
}
}
}

View File

@ -1,26 +0,0 @@
//
// RecommendHashTagSection.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/1.
//
import Foundation
import MastodonSDK
import UIKit
enum RecommendHashTagSection: Equatable, Hashable {
case main
}
extension RecommendHashTagSection {
static func collectionViewDiffableDataSource(
for collectionView: UICollectionView
) -> UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag> {
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, tag -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self), for: indexPath) as! SearchRecommendTagsCollectionViewCell
cell.config(with: tag)
return cell
}
}
}

View File

@ -7,35 +7,9 @@
import Foundation
import CoreData
import CoreDataStack
enum SearchHistoryItem {
case account(objectID: NSManagedObjectID)
case hashtag(objectID: NSManagedObjectID)
case status(objectID: NSManagedObjectID, attribute: Item.StatusAttribute)
}
extension SearchHistoryItem: Hashable {
static func == (lhs: SearchHistoryItem, rhs: SearchHistoryItem) -> Bool {
switch (lhs, rhs) {
case (.account(let objectIDLeft), account(let objectIDRight)):
return objectIDLeft == objectIDRight
case (.hashtag(let objectIDLeft), hashtag(let objectIDRight)):
return objectIDLeft == objectIDRight
case (.status(let objectIDLeft, _), status(let objectIDRight, _)):
return objectIDLeft == objectIDRight
default:
return false
}
}
func hash(into hasher: inout Hasher) {
switch self {
case .account(let objectID):
hasher.combine(objectID)
case .hashtag(let objectID):
hasher.combine(objectID)
case .status(let objectID, _):
hasher.combine(objectID)
}
}
enum SearchHistoryItem: Hashable {
case hashtag(ManagedObjectRecord<Tag>)
case user(ManagedObjectRecord<MastodonUser>)
}

View File

@ -13,28 +13,80 @@ enum SearchHistorySection: Hashable {
}
extension SearchHistorySection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
dependency: NeedsDependency
) -> UITableViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> {
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
struct Configuration {
weak var searchHistorySectionHeaderCollectionReusableViewDelegate: SearchHistorySectionHeaderCollectionReusableViewDelegate?
}
static func diffableDataSource(
collectionView: UICollectionView,
context: AppContext,
configuration: Configuration
) -> UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> {
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, ManagedObjectRecord<MastodonUser>> { cell, indexPath, item in
context.managedObjectContext.performAndWait {
guard let user = item.object(in: context.managedObjectContext) else { return }
cell.configure(viewModel: .init(value: user))
}
}
let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, ManagedObjectRecord<Tag>> { cell, indexPath, item in
context.managedObjectContext.performAndWait {
guard let hashtag = item.object(in: context.managedObjectContext) else { return }
var contentConfiguration = cell.defaultContentConfiguration()
contentConfiguration.text = "#" + hashtag.name
cell.contentConfiguration = contentConfiguration
}
var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
backgroundConfiguration.backgroundColorTransformer = .init { [weak cell] _ in
guard let state = cell?.configurationState else {
return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
}
if state.isHighlighted || state.isSelected {
return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor
}
return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
}
cell.backgroundConfiguration = backgroundConfiguration
}
let dataSource = UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>(collectionView: collectionView) { collectionView, indexPath, item in
switch item {
case .account(let objectID):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
if let user = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? MastodonUser {
cell.config(with: user)
}
return cell
case .hashtag(let objectID):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
if let hashtag = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? Tag {
cell.config(with: hashtag)
}
return cell
case .status:
// Should not show status in the history list
return UITableViewCell()
} // end switch
} // end UITableViewDiffableDataSource
case .user(let record):
return collectionView.dequeueConfiguredReusableCell(
using: userCellRegister,
for: indexPath, item: record)
case .hashtag(let record):
return collectionView.dequeueConfiguredReusableCell(
using: hashtagCellRegister,
for: indexPath, item: record)
}
}
let trendHeaderRegister = UICollectionView.SupplementaryRegistration<SearchHistorySectionHeaderCollectionReusableView>(elementKind: UICollectionView.elementKindSectionHeader) { [weak dataSource] supplementaryView, elementKind, indexPath in
supplementaryView.delegate = configuration.searchHistorySectionHeaderCollectionReusableViewDelegate
guard let dataSource = dataSource else { return }
let sections = dataSource.snapshot().sectionIdentifiers
guard indexPath.section < sections.count else { return }
let section = sections[indexPath.section]
}
dataSource.supplementaryViewProvider = { (collectionView: UICollectionView, elementKind: String, indexPath: IndexPath) in
let fallback = UICollectionReusableView()
switch elementKind {
case UICollectionView.elementKindSectionHeader:
return collectionView.dequeueConfiguredReusableSupplementary(using: trendHeaderRegister, for: indexPath)
default:
assertionFailure()
return fallback
}
}
return dataSource
} // end func
}

View File

@ -0,0 +1,13 @@
//
// SearchItem.swift
// Mastodon
//
// Created by MainasuK on 2022-1-18.
//
import Foundation
import MastodonSDK
enum SearchItem: Hashable {
case trend(Mastodon.Entity.Tag)
}

View File

@ -5,14 +5,15 @@
// Created by sxiaojian on 2021/4/6.
//
import CoreData
import Foundation
import CoreData
import CoreDataStack
import MastodonSDK
enum SearchResultItem {
enum SearchResultItem: Hashable {
case user(ManagedObjectRecord<MastodonUser>)
case status(ManagedObjectRecord<Status>)
case hashtag(tag: Mastodon.Entity.Tag)
case account(account: Mastodon.Entity.Account)
case status(statusObjectID: NSManagedObjectID, attribute: Item.StatusAttribute)
case bottomLoader(attribute: BottomLoaderAttribute)
}
@ -26,7 +27,10 @@ extension SearchResultItem {
self.isNoResult = isEmptyResult
}
static func == (lhs: SearchResultItem.BottomLoaderAttribute, rhs: SearchResultItem.BottomLoaderAttribute) -> Bool {
static func == (
lhs: SearchResultItem.BottomLoaderAttribute,
rhs: SearchResultItem.BottomLoaderAttribute
) -> Bool {
return lhs.id == rhs.id
}
@ -35,60 +39,3 @@ extension SearchResultItem {
}
}
}
extension SearchResultItem: Equatable {
static func == (lhs: SearchResultItem, rhs: SearchResultItem) -> Bool {
switch (lhs, rhs) {
case (.hashtag(let tagLeft), .hashtag(let tagRight)):
return tagLeft == tagRight
case (.account(let accountLeft), .account(let accountRight)):
return accountLeft == accountRight
case (.status(let idLeft, _), .status(let idRight, _)):
return idLeft == idRight
case (.bottomLoader(let attributeLeft), .bottomLoader(let attributeRight)):
return attributeLeft == attributeRight
default:
return false
}
}
}
extension SearchResultItem: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .account(let account):
hasher.combine(String(describing: SearchResultItem.account.self))
hasher.combine(account.id)
case .hashtag(let tag):
hasher.combine(String(describing: SearchResultItem.hashtag.self))
hasher.combine(tag.name)
case .status(let id, _):
hasher.combine(id)
case .bottomLoader(let attribute):
hasher.combine(attribute)
}
}
}
extension SearchResultItem {
var sortKey: String? {
switch self {
case .account(let account): return account.displayName.lowercased()
case .hashtag(let hashtag): return hashtag.name.lowercased()
default: return nil
}
}
}
extension SearchResultItem {
var statusObjectItem: StatusObjectItem? {
switch self {
case .status(let objectID, _):
return .status(objectID: objectID)
case .hashtag,
.account,
.bottomLoader:
return nil
}
}
}

View File

@ -5,51 +5,70 @@
// Created by sxiaojian on 2021/4/6.
//
import os.log
import Foundation
import MastodonSDK
import UIKit
import CoreData
import CoreDataStack
import MastodonAsset
import MastodonLocalization
import MastodonUI
enum SearchResultSection: Equatable, Hashable {
enum SearchResultSection: Hashable {
case main
}
extension SearchResultSection {
static let logger = Logger(subsystem: "SearchResultSection", category: "logic")
struct Configuration {
weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate?
weak var userTableViewCellDelegate: UserTableViewCellDelegate?
}
static func tableViewDiffableDataSource(
for tableView: UITableView,
dependency: NeedsDependency,
statusTableViewCellDelegate: StatusTableViewCellDelegate
tableView: UITableView,
context: AppContext,
configuration: Configuration
) -> UITableViewDiffableDataSource<SearchResultSection, SearchResultItem> {
UITableViewDiffableDataSource(tableView: tableView) { [
weak statusTableViewCellDelegate
] tableView, indexPath, item -> UITableViewCell? in
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
tableView.register(HashtagTableViewCell.self, forCellReuseIdentifier: String(describing: HashtagTableViewCell.self))
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
switch item {
case .account(let account):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
cell.config(with: account)
return cell
case .hashtag(let tag):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
cell.config(with: tag)
return cell
case .status(let statusObjectID, let attribute):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
if let status = try? dependency.context.managedObjectContext.existingObject(with: statusObjectID) as? Status {
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
StatusSection.configure(
cell: cell,
case .user(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
tableView: tableView,
timelineContext: .search,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
status: status,
requestUserID: requestUserID,
statusItemAttribute: attribute
cell: cell,
viewModel: .init(value: .user(user)),
configuration: configuration
)
}
cell.delegate = statusTableViewCellDelegate
return cell
case .status(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
context.managedObjectContext.performAndWait {
guard let status = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusTableViewCell.ViewModel(value: .status(status)),
configuration: configuration
)
}
return cell
case .hashtag(let tag):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: HashtagTableViewCell.self)) as! HashtagTableViewCell
cell.primaryLabel.configure(content: PlaintextMetaContent(string: "#" + tag.name))
return cell
case .bottomLoader(let attribute):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell
@ -63,7 +82,49 @@ extension SearchResultSection {
cell.loadMoreLabel.isHidden = true
}
return cell
} // end switch
}
} // end UITableViewDiffableDataSource
} // end func
}
extension SearchResultSection {
static func configure(
context: AppContext,
tableView: UITableView,
cell: StatusTableViewCell,
viewModel: StatusTableViewCell.ViewModel,
configuration: Configuration
) {
StatusSection.setupStatusPollDataSource(
context: context,
statusView: cell.statusView
)
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
cell.configure(
tableView: tableView,
viewModel: viewModel,
delegate: configuration.statusViewTableViewCellDelegate
)
}
static func configure(
context: AppContext,
tableView: UITableView,
cell: UserTableViewCell,
viewModel: UserTableViewCell.ViewModel,
configuration: Configuration
) {
cell.configure(
tableView: tableView,
viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate
)
}
}

View File

@ -0,0 +1,76 @@
//
// SearchSection.swift
// Mastodon
//
// Created by MainasuK on 2022-1-18.
//
import UIKit
import MastodonSDK
import MastodonLocalization
enum SearchSection: Hashable {
case trend
}
extension SearchSection {
static func diffableDataSource(
collectionView: UICollectionView,
context: AppContext
) -> UICollectionViewDiffableDataSource<SearchSection, SearchItem> {
let trendCellRegister = UICollectionView.CellRegistration<TrendCollectionViewCell, Mastodon.Entity.Tag> { cell, indexPath, item in
cell.primaryLabel.text = "#" + item.name
cell.secondaryLabel.text = L10n.Scene.Search.Recommend.HashTag.peopleTalking(item.talkingPeopleCount ?? 0)
cell.lineChartView.data = (item.history ?? [])
.sorted(by: { $0.day < $1.day }) // latest last
.map { entry in
guard let point = Int(entry.accounts) else {
return .zero
}
return CGFloat(point)
}
}
let dataSource = UICollectionViewDiffableDataSource<SearchSection, SearchItem>(
collectionView: collectionView
) { collectionView, indexPath, item in
switch item {
case .trend(let hashtag):
let cell = collectionView.dequeueConfiguredReusableCell(
using: trendCellRegister,
for: indexPath,
item: hashtag
)
return cell
}
}
let trendHeaderRegister = UICollectionView.SupplementaryRegistration<TrendSectionHeaderCollectionReusableView>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
// do nothing
}
dataSource.supplementaryViewProvider = { [weak dataSource] (collectionView: UICollectionView, elementKind: String, indexPath: IndexPath) in
let fallback = UICollectionReusableView()
guard let dataSource = dataSource else { return fallback }
let sections = dataSource.snapshot().sectionIdentifiers
guard indexPath.section < sections.count else { return fallback }
let section = sections[indexPath.section]
switch elementKind {
case UICollectionView.elementKindSectionHeader:
switch section {
case .trend:
return collectionView.dequeueConfiguredReusableSupplementary(using: trendHeaderRegister, for: indexPath)
}
default:
assertionFailure()
return fallback
}
}
return dataSource
} // end func
}

View File

@ -7,6 +7,8 @@
import UIKit
import CoreData
import MastodonAsset
import MastodonLocalization
enum SettingsItem {
case appearance(settingObjectID: NSManagedObjectID)

View File

@ -8,6 +8,8 @@
import UIKit
import CoreData
import CoreDataStack
import MastodonAsset
import MastodonLocalization
enum SettingsSection: Hashable {
case appearance

View File

@ -1,198 +0,0 @@
//
// Item.swift
// Mastodon
//
// Created by sxiaojian on 2021/1/27.
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import DifferenceKit
/// Note: update Equatable when change case
enum Item {
// timeline
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusAttribute)
// thread
case root(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
case reply(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
case leaf(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
case leafBottomLoader(statusObjectID: NSManagedObjectID)
// normal list
case status(objectID: NSManagedObjectID, attribute: StatusAttribute)
// loader
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
case publicMiddleLoader(statusID: String)
case topLoader
case bottomLoader
case emptyBottomLoader
case emptyStateHeader(attribute: EmptyStateHeaderAttribute)
// reports
case reportStatus(objectID: NSManagedObjectID, attribute: ReportStatusAttribute)
}
extension Item {
class StatusAttribute {
var isSeparatorLineHidden: Bool
/// is media loaded or not
let isImageLoaded = CurrentValueSubject<Bool, Never>(false)
/// flag for current sensitive content reveal state
///
/// - true: displaying sensitive content
/// - false: displaying content warning overlay
let isRevealing = CurrentValueSubject<Bool, Never>(false)
init(isSeparatorLineHidden: Bool = false) {
self.isSeparatorLineHidden = isSeparatorLineHidden
}
}
class EmptyStateHeaderAttribute: Hashable {
let id = UUID()
let reason: Reason
enum Reason: Equatable {
case noStatusFound
case blocking(name: String?)
case blocked(name: String?)
case suspended(name: String?)
static func == (lhs: Item.EmptyStateHeaderAttribute.Reason, rhs: Item.EmptyStateHeaderAttribute.Reason) -> Bool {
switch (lhs, rhs) {
case (.noStatusFound, noStatusFound): return true
case (.blocking(let nameLeft), blocking(let nameRight)): return nameLeft == nameRight
case (.blocked(let nameLeft), blocked(let nameRight)): return nameLeft == nameRight
case (.suspended(let nameLeft), .suspended(let nameRight)): return nameLeft == nameRight
default: return false
}
}
}
init(reason: Reason) {
self.reason = reason
}
static func == (lhs: Item.EmptyStateHeaderAttribute, rhs: Item.EmptyStateHeaderAttribute) -> Bool {
return lhs.reason == rhs.reason
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
class ReportStatusAttribute: StatusAttribute {
var isSelected: Bool
init(isSeparatorLineHidden: Bool = false, isSelected: Bool = false) {
self.isSelected = isSelected
super.init(isSeparatorLineHidden: isSeparatorLineHidden)
}
}
}
extension Item: Equatable {
static func == (lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) {
case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)):
return objectIDLeft == objectIDRight
case (.root(let objectIDLeft, _), .root(let objectIDRight, _)):
return objectIDLeft == objectIDRight
case (.reply(let objectIDLeft, _), .reply(let objectIDRight, _)):
return objectIDLeft == objectIDRight
case (.leaf(let objectIDLeft, _), .leaf(let objectIDRight, _)):
return objectIDLeft == objectIDRight
case (.leafBottomLoader(let objectIDLeft), .leafBottomLoader(let objectIDRight)):
return objectIDLeft == objectIDRight
case (.status(let objectIDLeft, _), .status(let objectIDRight, _)):
return objectIDLeft == objectIDRight
case (.homeMiddleLoader(let upperLeft), .homeMiddleLoader(let upperRight)):
return upperLeft == upperRight
case (.publicMiddleLoader(let upperLeft), .publicMiddleLoader(let upperRight)):
return upperLeft == upperRight
case (.topLoader, .topLoader):
return true
case (.bottomLoader, .bottomLoader):
return true
case (.emptyBottomLoader, .emptyBottomLoader):
return true
case (.emptyStateHeader(let attributeLeft), .emptyStateHeader(let attributeRight)):
return attributeLeft == attributeRight
case (.reportStatus(let objectIDLeft, _), .reportStatus(let objectIDRight, _)):
return objectIDLeft == objectIDRight
default:
return false
}
}
}
extension Item: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .homeTimelineIndex(let objectID, _):
hasher.combine(objectID)
case .root(let objectID, _):
hasher.combine(objectID)
case .reply(let objectID, _):
hasher.combine(objectID)
case .leaf(let objectID, _):
hasher.combine(objectID)
case .leafBottomLoader(let objectID):
hasher.combine(objectID)
case .status(let objectID, _):
hasher.combine(objectID)
case .homeMiddleLoader(upperTimelineIndexAnchorObjectID: let upper):
hasher.combine(String(describing: Item.homeMiddleLoader.self))
hasher.combine(upper)
case .publicMiddleLoader(let upper):
hasher.combine(String(describing: Item.publicMiddleLoader.self))
hasher.combine(upper)
case .topLoader:
hasher.combine(String(describing: Item.topLoader.self))
case .bottomLoader:
hasher.combine(String(describing: Item.bottomLoader.self))
case .emptyBottomLoader:
hasher.combine(String(describing: Item.emptyBottomLoader.self))
case .emptyStateHeader(let attribute):
hasher.combine(attribute)
case .reportStatus(let objectID, _):
hasher.combine(objectID)
}
}
}
extension Item: Differentiable { }
extension Item {
var statusObjectItem: StatusObjectItem? {
switch self {
case .homeTimelineIndex(let objectID, _):
return .homeTimelineIndex(objectID: objectID)
case .root(let objectID, _),
.reply(let objectID, _),
.leaf(let objectID, _),
.status(let objectID, _),
.reportStatus(let objectID, _):
return .status(objectID: objectID)
case .leafBottomLoader,
.homeMiddleLoader,
.publicMiddleLoader,
.topLoader,
.bottomLoader,
.emptyBottomLoader,
.emptyStateHeader:
return nil
}
}
}

View File

@ -1,67 +0,0 @@
//
// ReportSection.swift
// Mastodon
//
// Created by ihugo on 2021/4/20.
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
import AVKit
import os.log
enum ReportSection: Equatable, Hashable {
case main
}
extension ReportSection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
dependency: ReportViewController,
managedObjectContext: NSManagedObjectContext,
timestampUpdatePublisher: AnyPublisher<Date, Never>
) -> UITableViewDiffableDataSource<ReportSection, Item> {
UITableViewDiffableDataSource(tableView: tableView) {[
weak dependency
] tableView, indexPath, item -> UITableViewCell? in
guard let dependency = dependency else { return UITableViewCell() }
switch item {
case .reportStatus(let objectID, let attribute):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportedStatusTableViewCell.self), for: indexPath) as! ReportedStatusTableViewCell
cell.dependency = dependency
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
managedObjectContext.performAndWait { [weak dependency] in
guard let dependency = dependency else { return }
let status = managedObjectContext.object(with: objectID) as! Status
StatusSection.configure(
cell: cell,
tableView: tableView,
timelineContext: .report,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
status: status,
requestUserID: requestUserID,
statusItemAttribute: attribute
)
}
// defalut to select the report status
if attribute.isSelected {
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
} else {
tableView.deselectRow(at: indexPath, animated: false)
}
return cell
default:
return nil
}
}
}
}

View File

@ -0,0 +1,84 @@
//
// StatusItem.swift
// Mastodon
//
// Created by MainasuK on 2022-1-11.
//
import Foundation
import CoreDataStack
enum StatusItem: Hashable {
case feed(record: ManagedObjectRecord<Feed>)
case feedLoader(record: ManagedObjectRecord<Feed>)
case status(record: ManagedObjectRecord<Status>)
// case statusLoader(record: ManagedObjectRecord<Status>, context: StatusLoaderContext)
case thread(Thread)
case topLoader
case bottomLoader
}
//extension StatusItem {
// final class StatusLoaderContext: Hashable {
// let id = UUID()
// @Published var isFetching = false
//
// static func == (
// lhs: StatusItem.StatusLoaderContext,
// rhs: StatusItem.StatusLoaderContext
// ) -> Bool {
// return lhs.id == rhs.id
// }
//
// func hash(into hasher: inout Hasher) {
// hasher.combine(id)
// }
// }
//}
extension StatusItem {
enum Thread: Hashable {
case root(context: Context)
case reply(context: Context)
case leaf(context: Context)
public var record: ManagedObjectRecord<Status> {
switch self {
case .root(let threadContext),
.reply(let threadContext),
.leaf(let threadContext):
return threadContext.status
}
}
}
}
extension StatusItem.Thread {
class Context: Hashable {
let status: ManagedObjectRecord<Status>
var displayUpperConversationLink: Bool
var displayBottomConversationLink: Bool
init(
status: ManagedObjectRecord<Status>,
displayUpperConversationLink: Bool = false,
displayBottomConversationLink: Bool = false
) {
self.status = status
self.displayUpperConversationLink = displayUpperConversationLink
self.displayBottomConversationLink = displayBottomConversationLink
}
static func == (lhs: StatusItem.Thread.Context, rhs: StatusItem.Thread.Context) -> Bool {
return lhs.status == rhs.status
&& lhs.displayUpperConversationLink == rhs.displayUpperConversationLink
&& lhs.displayBottomConversationLink == rhs.displayBottomConversationLink
}
func hash(into hasher: inout Hasher) {
hasher.combine(status)
hasher.combine(displayUpperConversationLink)
hasher.combine(displayBottomConversationLink)
}
}
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More