feat: add recent search history in sidebar

This commit is contained in:
CMK 2021-09-26 18:29:08 +08:00
parent d35f163623
commit 1da803fb97
23 changed files with 674 additions and 67 deletions

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>CoreData 2.xcdatamodel</string>
</dict>
</plist>

View File

@ -0,0 +1,298 @@
<?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="">
<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="Attachment" representedClassName=".Attachment" syncable="YES">
<attribute name="blurhash" optional="YES" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="descriptionString" optional="YES" attributeType="String"/>
<attribute name="domain" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="metaData" optional="YES" attributeType="Binary"/>
<attribute name="previewRemoteURL" optional="YES" attributeType="String"/>
<attribute name="previewURL" optional="YES" attributeType="String"/>
<attribute name="remoteURL" optional="YES" attributeType="String"/>
<attribute name="textURL" optional="YES" attributeType="String"/>
<attribute name="typeRaw" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" optional="YES" attributeType="String"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="mediaAttachments" 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="History" representedClassName=".History" syncable="YES">
<attribute name="accounts" optional="YES" attributeType="String"/>
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="day" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="uses" optional="YES" attributeType="String"/>
<relationship name="tag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="histories" inverseEntity="Tag"/>
</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" inverseName="homeTimelineIndexes" inverseEntity="Status"/>
</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="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
</entity>
<entity name="MastodonNotification" representedClassName=".MastodonNotification" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="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="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="inNotifications" inverseEntity="Status"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</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="emojisData" optional="YES" attributeType="Binary"/>
<attribute name="fieldsData" 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" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blockingBy" inverseEntity="MastodonUser"/>
<relationship name="blockingBy" optional="YES" 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" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlocking" inverseEntity="MastodonUser"/>
<relationship name="endorsed" optional="YES" 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" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="favouritedBy" inverseEntity="Status"/>
<relationship name="following" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followingBy" inverseEntity="MastodonUser"/>
<relationship name="followingBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="following" inverseEntity="MastodonUser"/>
<relationship name="followRequested" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequestedBy" inverseEntity="MastodonUser"/>
<relationship name="followRequestedBy" optional="YES" 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" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="mutedBy" inverseEntity="Status"/>
<relationship name="muting" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mutingBy" inverseEntity="MastodonUser"/>
<relationship name="mutingBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muting" inverseEntity="MastodonUser"/>
<relationship name="notifications" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonNotification" inverseName="account" inverseEntity="MastodonNotification"/>
<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" optional="YES" 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" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
<relationship name="votePolls" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
</entity>
<entity name="Mention" representedClassName=".Mention" syncable="YES">
<attribute name="acct" attributeType="String"/>
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="url" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="mentions" inverseEntity="Status"/>
</entity>
<entity name="Poll" representedClassName=".Poll" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="expired" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="String"/>
<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="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="appearanceRaw" attributeType="String"/>
<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="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="emojisData" 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="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" attributeType="String"/>
<attribute name="visibility" optional="YES" attributeType="String"/>
<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" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
<relationship name="favouritedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="HomeTimelineIndex" inverseName="status" inverseEntity="HomeTimelineIndex"/>
<relationship name="inNotifications" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MastodonNotification" inverseName="status" inverseEntity="MastodonNotification"/>
<relationship name="mediaAttachments" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Attachment" inverseName="status" inverseEntity="Attachment"/>
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Mention" inverseName="status" inverseEntity="Mention"/>
<relationship name="mutedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
<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" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
<relationship name="rebloggedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
<relationship name="replyFrom" optional="YES" 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="Nullify" 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="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="histories" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="History" inverseName="tag" inverseEntity="History"/>
<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="Attachment" positionX="0" positionY="0" width="128" height="254"/>
<element name="DomainBlock" positionX="45" positionY="162" width="128" height="89"/>
<element name="Emoji" positionX="0" positionY="0" width="128" height="134"/>
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="734"/>
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
<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="179"/>
<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="134"/>
</elements>
</model>

View File

@ -43,11 +43,11 @@ final public class MastodonUser: NSManagedObject {
// one-to-one relationship
@NSManaged public private(set) var pinnedStatus: Status?
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
@NSManaged public private(set) var searchHistory: SearchHistory?
// 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>?
@ -274,6 +274,15 @@ extension MastodonUser {
}
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

View File

@ -16,7 +16,7 @@ public final class SearchHistory: NSManagedObject {
@NSManaged public private(set) var createAt: Date
@NSManaged public private(set) var updatedAt: Date
// one-to-one relationship
// 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?
@ -31,10 +31,10 @@ extension SearchHistory {
setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.updatedAt))
}
public override func willSave() {
super.willSave()
setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.updatedAt))
}
// public override func willSave() {
// super.willSave()
// setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.updatedAt))
// }
@discardableResult
public static func insert(

View File

@ -52,18 +52,18 @@ public final class Status: NSManagedObject {
// one-to-one relationship
@NSManaged public private(set) var pinnedBy: MastodonUser?
@NSManaged public private(set) var poll: Poll?
@NSManaged public private(set) var searchHistory: SearchHistory?
// 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 tags: Set<Tag>?
@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?
@ -81,7 +81,6 @@ extension Status {
replyTo: Status?,
poll: Poll?,
mentions: [Mention]?,
tags: [Tag]?,
mediaAttachments: [Attachment]?,
favouritedBy: MastodonUser?,
rebloggedBy: MastodonUser?,
@ -126,9 +125,6 @@ extension Status {
if let mentions = mentions {
status.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions)
}
if let tags = tags {
status.mutableSetValue(forKey: #keyPath(Status.tags)).addObjects(from: tags)
}
if let mediaAttachments = mediaAttachments {
status.mutableSetValue(forKey: #keyPath(Status.mediaAttachments)).addObjects(from: mediaAttachments)
}

View File

@ -18,13 +18,12 @@ public final class Tag: NSManagedObject {
@NSManaged public private(set) var url: String
// one-to-one relationship
@NSManaged public private(set) var searchHistory: SearchHistory?
// many-to-many relationship
@NSManaged public private(set) var statuses: Set<Status>?
// one-to-many relationship
@NSManaged public private(set) var histories: Set<History>?
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
}
public extension Tag {
@ -55,6 +54,15 @@ public extension 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

View File

@ -1352,6 +1352,7 @@
DBE3CE0C261D767100430CC6 /* FavoriteViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteViewController+Provider.swift"; sourceTree = "<group>"; };
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewControllerAspect.swift; sourceTree = "<group>"; };
DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPreference.swift; sourceTree = "<group>"; };
DBF156DD27006F5D00EC00B7 /* CoreData 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "CoreData 2.xcdatamodel"; sourceTree = "<group>"; };
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDetailViewController.swift; sourceTree = "<group>"; };
DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewController.swift; sourceTree = "<group>"; };
DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDetailViewModel.swift; sourceTree = "<group>"; };
@ -6129,9 +6130,10 @@
DB89BA3525C1145C008580ED /* CoreData.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DBF156DD27006F5D00EC00B7 /* CoreData 2.xcdatamodel */,
DB89BA3625C1145C008580ED /* CoreData.xcdatamodel */,
);
currentVersion = DB89BA3625C1145C008580ED /* CoreData.xcdatamodel */;
currentVersion = DBF156DD27006F5D00EC00B7 /* CoreData 2.xcdatamodel */;
path = CoreData.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View File

@ -7,12 +7,12 @@
<key>AppShared.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>42</integer>
<integer>38</integer>
</dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>43</integer>
<integer>35</integer>
</dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict>
@ -97,7 +97,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>44</integer>
<integer>37</integer>
</dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict>
@ -117,7 +117,7 @@
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>41</integer>
<integer>36</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -2,8 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDuration</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>

View File

@ -109,6 +109,9 @@ extension AccountListTableViewCell {
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
])
badgeButton.setBadge(number: 0)
checkmarkImageView.isHidden = true
}
}

View File

@ -85,16 +85,20 @@ final class HashtagTimelineViewModel: NSObject {
return
}
let query = Mastodon.API.V2.Search.Query(q: hashtag, type: .hashtags)
context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
.sink { _ in
} receiveValue: { [weak self] response in
let matchedTag = response.value.hashtags.first { tag -> Bool in
return tag.name == self?.hashtag
}
self?.hashtagEntity.send(matchedTag)
context.apiService.search(
domain: activeMastodonAuthenticationBox.domain,
query: query,
mastodonAuthenticationBox: activeMastodonAuthenticationBox
)
.sink { _ in
} receiveValue: { [weak self] response in
let matchedTag = response.value.hashtags.first { tag -> Bool in
return tag.name == self?.hashtag
}
.store(in: &disposeBag)
self?.hashtagEntity.send(matchedTag)
}
.store(in: &disposeBag)
}

View File

@ -99,7 +99,17 @@ extension HomeTimelineViewController {
.receive(on: DispatchQueue.main)
.sink { [weak self] displaySettingBarButtonItem in
guard let self = self else { return }
#if DEBUG
// display debug menu
self.navigationItem.leftBarButtonItem = {
let barButtonItem = UIBarButtonItem()
barButtonItem.image = UIImage(systemName: "ellipsis.circle")
barButtonItem.menu = self.debugMenu
return barButtonItem
}()
#else
self.navigationItem.leftBarButtonItem = displaySettingBarButtonItem ? self.settingBarButtonItem : nil
#endif
}
.store(in: &disposeBag)
navigationItem.titleView = titleView

View File

@ -8,6 +8,7 @@
import os.log
import UIKit
import Combine
import CoreDataStack
final class RootSplitViewController: UISplitViewController, NeedsDependency {
@ -117,6 +118,7 @@ extension RootSplitViewController {
// MARK: - SidebarViewControllerDelegate
extension RootSplitViewController: SidebarViewControllerDelegate {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) {
guard let index = MainTabBarController.Tab.allCases.firstIndex(of: tab) else {
@ -126,6 +128,30 @@ extension RootSplitViewController: SidebarViewControllerDelegate {
currentSupplementaryTab = tab
setViewController(supplementaryViewControllers[index], for: .supplementary)
}
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectSearchHistory searchHistoryViewModel: SidebarViewModel.SearchHistoryViewModel) {
// self.sidebarViewController(sidebarViewController, didSelectTab: .search)
let supplementaryViewController = viewController(for: .supplementary)
let managedObjectContext = context.managedObjectContext
managedObjectContext.perform {
let searchHistory = managedObjectContext.object(with: searchHistoryViewModel.searchHistoryObjectID) as! SearchHistory
if let account = searchHistory.account {
DispatchQueue.main.async {
let profileViewModel = CachedProfileViewModel(context: self.context, mastodonUser: account)
self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: supplementaryViewController, transition: .show)
}
} else if let hashtag = searchHistory.hashtag {
DispatchQueue.main.async {
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: self.context, hashtag: hashtag.name)
self.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: supplementaryViewController, transition: .show)
}
} else {
assertionFailure()
}
}
}
}
// MARK: - UISplitViewControllerDelegate

View File

@ -12,6 +12,7 @@ import CoreDataStack
protocol SidebarViewControllerDelegate: AnyObject {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab)
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectSearchHistory searchHistoryViewModel: SidebarViewModel.SearchHistoryViewModel)
}
final class SidebarViewController: UIViewController, NeedsDependency {
@ -34,6 +35,13 @@ final class SidebarViewController: UIViewController, NeedsDependency {
static func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
if sectionIndex == SidebarViewModel.Section.tab.rawValue {
// with indentation
configuration.headerMode = .none
} else {
// remove indentation
configuration.headerMode = .firstItemInSection
}
configuration.showsSeparators = false
let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
return section
@ -59,6 +67,11 @@ extension SidebarViewController {
.sink { [weak self] activeMastodonAuthenticationBox in
guard let self = self else { return }
let domain = activeMastodonAuthenticationBox?.domain
self.navigationItem.backBarButtonItem = {
let barButtonItem = UIBarButtonItem()
barButtonItem.image = UIImage(systemName: "sidebar.leading")
return barButtonItem
}()
self.navigationItem.title = domain
}
.store(in: &disposeBag)
@ -121,6 +134,10 @@ extension SidebarViewController: UICollectionViewDelegate {
switch item {
case .tab(let tab):
delegate?.sidebarViewController(self, didSelectTab: tab)
case .searchHistory(let viewModel):
delegate?.sidebarViewController(self, didSelectSearchHistory: viewModel)
case .header:
break
case .account(let viewModel):
assert(Thread.isMainThread)
let authentication = context.managedObjectContext.object(with: viewModel.authenticationObjectID) as! MastodonAuthentication
@ -133,9 +150,6 @@ extension SidebarViewController: UICollectionViewDelegate {
.store(in: &disposeBag)
case .addAccount:
coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))
default:
// TODO:
break
}
}
}

View File

@ -18,30 +18,51 @@ final class SidebarViewModel {
// input
let context: AppContext
let searchHistoryFetchedResultController: SearchHistoryFetchedResultController
// output
var diffableDataSource: UICollectionViewDiffableDataSource<Section, Item>!
let activeMastodonAuthenticationObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil)
init(context: AppContext) {
self.context = context
self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext)
context.authenticationService.activeMastodonAuthentication
.sink { [weak self] authentication in
guard let self = self else { return }
// bind search history
self.searchHistoryFetchedResultController.domain.value = authentication?.domain
self.searchHistoryFetchedResultController.userID.value = authentication?.userID
// bind objectID
self.activeMastodonAuthenticationObjectID.value = authentication?.objectID
}
.store(in: &disposeBag)
try? searchHistoryFetchedResultController.fetchedResultsController.performFetch()
}
}
extension SidebarViewModel {
enum Section: Hashable, CaseIterable {
enum Section: Int, Hashable, CaseIterable {
case tab
case account
}
enum Item: Hashable {
case tab(MainTabBarController.Tab)
case searchHistory(SearchHistoryViewModel)
case header(HeaderViewModel)
case account(AccountViewModel)
case addAccount
}
struct SearchHistoryViewModel: Hashable {
let searchHistoryObjectID: NSManagedObjectID
}
struct HeaderViewModel: Hashable {
let title: String
}
@ -59,7 +80,9 @@ extension SidebarViewModel {
func setupDiffableDataSource(
collectionView: UICollectionView
) {
let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, MainTabBarController.Tab> { (cell, indexPath, item) in
let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, MainTabBarController.Tab> { [weak self] cell, indexPath, item in
guard let self = self else { return }
let imageURL: URL? = {
switch item {
case .me:
@ -79,13 +102,76 @@ extension SidebarViewModel {
return PlaintextMetaContent(string: item.title)
}
}()
let needsOutlineDisclosure = item == .search
cell.item = SidebarListContentView.Item(
image: item.sidebarImage,
imageURL: imageURL,
headline: headline,
subheadline: nil
subheadline: nil,
needsOutlineDisclosure: needsOutlineDisclosure
)
cell.setNeedsUpdateConfiguration()
switch item {
case .notification:
Publishers.CombineLatest(
self.context.authenticationService.activeMastodonAuthentication,
self.context.notificationService.unreadNotificationCountDidUpdate
)
.receive(on: DispatchQueue.main)
.sink { [weak cell] authentication, _ in
guard let cell = cell else { return }
let hasUnreadPushNotification: Bool = authentication.flatMap { authentication in
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.userAccessToken)
return count > 0
} ?? false
let image = hasUnreadPushNotification ? UIImage(systemName: "bell.badge")! : UIImage(systemName: "bell")!
cell._contentView?.imageView.image = image
}
.store(in: &cell.disposeBag)
default:
break
}
}
let searchHistoryCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, SearchHistoryViewModel> { [weak self] cell, indexPath, item in
guard let self = self else { return }
let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext
guard let searchHistory = try? managedObjectContext.existingObject(with: item.searchHistoryObjectID) as? SearchHistory else { return }
if let account = searchHistory.account {
let headline: MetaContent = {
do {
let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta)
return try MastodonMetaContent.convert(document: content)
} catch {
return PlaintextMetaContent(string: account.displayNameWithFallback)
}
}()
cell.item = SidebarListContentView.Item(
image: .placeholder(color: .systemFill),
imageURL: account.avatarImageURL(),
headline: headline,
subheadline: PlaintextMetaContent(string: "@" + account.acctWithDomain),
needsOutlineDisclosure: false
)
} else if let hashtag = searchHistory.hashtag {
let image = UIImage(systemName: "number.square.fill")!.withRenderingMode(.alwaysTemplate)
let headline = PlaintextMetaContent(string: "#" + hashtag.name)
cell.item = SidebarListContentView.Item(
image: image,
imageURL: nil,
headline: headline,
subheadline: nil,
needsOutlineDisclosure: false
)
} else {
assertionFailure()
}
cell.setNeedsUpdateConfiguration()
}
let headerRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, HeaderViewModel> { (cell, indexPath, item) in
@ -95,7 +181,9 @@ extension SidebarViewModel {
cell.accessories = [.outlineDisclosure()]
}
let accountRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, AccountViewModel> { (cell, indexPath, item) in
let accountRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, AccountViewModel> { [weak self] (cell, indexPath, item) in
guard let self = self else { return }
let authentication = AppContext.shared.managedObjectContext.object(with: item.authenticationObjectID) as! MastodonAuthentication
let user = authentication.user
let imageURL = user.avatarImageURL()
@ -111,15 +199,37 @@ extension SidebarViewModel {
image: .placeholder(color: .systemFill),
imageURL: imageURL,
headline: headline,
subheadline: PlaintextMetaContent(string: "@" + user.acctWithDomain)
subheadline: PlaintextMetaContent(string: "@" + user.acctWithDomain),
needsOutlineDisclosure: false
)
cell.setNeedsUpdateConfiguration()
// FIXME: use notification, not timer
let accessToken = authentication.userAccessToken
AppContext.shared.timestampUpdatePublisher
.map { _ in UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) }
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak cell] count in
guard let cell = cell else { return }
cell._contentView?.badgeButton.setBadge(number: count)
}
.store(in: &cell.disposeBag)
let authenticationObjectID = item.authenticationObjectID
self.activeMastodonAuthenticationObjectID
.receive(on: DispatchQueue.main)
.sink { [weak cell] objectID in
guard let cell = cell else { return }
cell._contentView?.checkmarkImageView.isHidden = authenticationObjectID != objectID
}
.store(in: &cell.disposeBag)
}
let addAccountRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, AddAccountViewModel> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = L10n.Scene.AccountList.addAccount
content.image = nil
content.image = UIImage(systemName: "plus.square.fill")!
cell.contentConfiguration = content
cell.accessories = []
}
@ -128,6 +238,8 @@ extension SidebarViewModel {
switch item {
case .tab(let tab):
return collectionView.dequeueConfiguredReusableCell(using: tabCellRegistration, for: indexPath, item: tab)
case .searchHistory(let viewModel):
return collectionView.dequeueConfiguredReusableCell(using: searchHistoryCellRegistration, for: indexPath, item: viewModel)
case .header(let viewModel):
return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: viewModel)
case .account(let viewModel):
@ -164,6 +276,38 @@ extension SidebarViewModel {
}
}
// update .search tab
searchHistoryFetchedResultController.objectIDs
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] objectIDs in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
// update .search tab
var sectionSnapshot = diffableDataSource.snapshot(for: .tab)
// remove children
let searchHistorySnapshot = sectionSnapshot.snapshot(of: .tab(.search))
sectionSnapshot.delete(searchHistorySnapshot.items)
// append children
let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext
let items: [Item] = objectIDs.compactMap { objectID -> Item? in
guard let searchHistory = try? managedObjectContext.existingObject(with: objectID) as? SearchHistory else { return nil }
guard searchHistory.account != nil || searchHistory.hashtag != nil else { return nil }
let viewModel = SearchHistoryViewModel(searchHistoryObjectID: objectID)
return Item.searchHistory(viewModel)
}
sectionSnapshot.append(Array(items.prefix(5)), to: .tab(.search))
sectionSnapshot.expand([.tab(.search)])
// apply snapshot
diffableDataSource.apply(sectionSnapshot, to: .tab, animatingDifferences: false)
}
.store(in: &disposeBag)
// update .me tab and .account section
context.authenticationService.mastodonAuthentications
.receive(on: DispatchQueue.main)
.sink { [weak self] authentications in

View File

@ -6,11 +6,29 @@
//
import UIKit
import Combine
final class SidebarListCollectionViewCell: UICollectionViewListCell {
var disposeBag = Set<AnyCancellable>()
var item: SidebarListContentView.Item?
var _contentView: SidebarListContentView? {
guard let view = contentView as? SidebarListContentView else {
assertionFailure()
return nil
}
return view
}
override func prepareForReuse() {
super.prepareForReuse()
disposeBag.removeAll()
}
override init(frame: CGRect) {
super.init(frame: frame)
_init()
@ -44,5 +62,19 @@ extension SidebarListCollectionViewCell {
backgroundConfiguration = newBackgroundConfiguration
let needsOutlineDisclosure = item?.needsOutlineDisclosure ?? false
if !needsOutlineDisclosure {
accessories = []
} else {
let tintColor: UIColor = state.isHighlighted || state.isSelected ? .white : Asset.Colors.brandBlue.color
accessories = [
UICellAccessory.outlineDisclosure(
displayed: .always,
options: UICellAccessory.OutlineDisclosureOptions(tintColor: tintColor),
actionHandler: nil
)
]
}
}
}

View File

@ -18,6 +18,13 @@ final class SidebarListContentView: UIView, UIContentView {
let animationImageView = FLAnimatedImageView() // for animation image
let headlineLabel = MetaLabel(style: .sidebarHeadline(isSelected: false))
let subheadlineLabel = MetaLabel(style: .sidebarSubheadline(isSelected: false))
let badgeButton = BadgeButton()
let checkmarkImageView: UIImageView = {
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .semibold))
let imageView = UIImageView(image: image)
imageView.tintColor = .label
return imageView
}()
private var currentConfiguration: ContentConfiguration!
var configuration: UIContentConfiguration {
@ -85,7 +92,7 @@ extension SidebarListContentView {
NSLayoutConstraint.activate([
textContainer.topAnchor.constraint(equalTo: topAnchor, constant: 10),
textContainer.leadingAnchor.constraint(equalTo: imageViewContainer.trailingAnchor, constant: 10),
textContainer.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
// textContainer.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
bottomAnchor.constraint(equalTo: textContainer.bottomAnchor, constant: 12),
])
@ -96,11 +103,32 @@ extension SidebarListContentView {
subheadlineLabel.setContentHuggingPriority(.required - 10, for: .vertical)
subheadlineLabel.setContentCompressionResistancePriority(.required - 10, for: .vertical)
badgeButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(badgeButton)
NSLayoutConstraint.activate([
badgeButton.leadingAnchor.constraint(equalTo: textContainer.trailingAnchor, constant: 4),
badgeButton.centerYAnchor.constraint(equalTo: centerYAnchor),
badgeButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 16).priority(.required - 1),
badgeButton.widthAnchor.constraint(equalTo: badgeButton.heightAnchor, multiplier: 1.0).priority(.required - 1),
])
badgeButton.setContentHuggingPriority(.required - 10, for: .horizontal)
badgeButton.setContentCompressionResistancePriority(.required - 10, for: .horizontal)
NSLayoutConstraint.activate([
imageViewContainer.heightAnchor.constraint(equalTo: headlineLabel.heightAnchor, multiplier: 1.0).priority(.required - 1),
imageViewContainer.widthAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1),
])
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(checkmarkImageView)
NSLayoutConstraint.activate([
checkmarkImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
checkmarkImageView.leadingAnchor.constraint(equalTo: badgeButton.trailingAnchor, constant: 16),
checkmarkImageView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
])
checkmarkImageView.setContentHuggingPriority(.required - 9, for: .horizontal)
checkmarkImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal)
animationImageView.isUserInteractionEnabled = false
headlineLabel.isUserInteractionEnabled = false
subheadlineLabel.isUserInteractionEnabled = false
@ -109,6 +137,9 @@ extension SidebarListContentView {
animationImageView.contentMode = .scaleAspectFit
imageView.tintColor = Asset.Colors.brandBlue.color
animationImageView.tintColor = Asset.Colors.brandBlue.color
badgeButton.setBadge(number: 0)
checkmarkImageView.isHidden = true
}
private func apply(configuration: ContentConfiguration) {
@ -160,6 +191,8 @@ extension SidebarListContentView {
let headline: MetaContent
let subheadline: MetaContent?
let needsOutlineDisclosure: Bool
static func == (lhs: SidebarListContentView.Item, rhs: SidebarListContentView.Item) -> Bool {
return lhs.isSelected == rhs.isSelected
&& lhs.image == rhs.image

View File

@ -98,14 +98,19 @@ extension SearchHistoryViewModel {
let managedObjectContext = context.backgroundManagedObjectContext
managedObjectContext.performChanges {
guard let user = try? managedObjectContext.existingObject(with: objectID) as? MastodonUser else { return }
if let searchHistory = user.searchHistory {
if let searchHistory = user.findSearchHistory(domain: box.domain, userID: box.userID) {
searchHistory.update(updatedAt: Date())
} else {
SearchHistory.insert(into: managedObjectContext, property: property, account: user)
}
}
.sink { result in
// do nothing
switch result {
case .failure(let error):
assertionFailure(error.localizedDescription)
case .success:
break
}
}
.store(in: &context.disposeBag)
@ -113,14 +118,19 @@ extension SearchHistoryViewModel {
let managedObjectContext = context.backgroundManagedObjectContext
managedObjectContext.performChanges {
guard let hashtag = try? managedObjectContext.existingObject(with: objectID) as? Tag else { return }
if let searchHistory = hashtag.searchHistory {
if let searchHistory = hashtag.findSearchHistory(domain: box.domain, userID: box.userID) {
searchHistory.update(updatedAt: Date())
} else {
SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag)
_ = SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag)
}
}
.sink { result in
// do nothing
switch result {
case .failure(let error):
assertionFailure(error.localizedDescription)
case .success:
break
}
}
.store(in: &context.disposeBag)

View File

@ -146,44 +146,57 @@ extension SearchResultViewModel {
let domain = box.domain
switch item {
case .account(let account):
case .account(let entity):
let managedObjectContext = context.backgroundManagedObjectContext
managedObjectContext.performChanges {
let (user, _) = APIService.CoreData.createOrMergeMastodonUser(
into: managedObjectContext,
for: nil,
in: domain,
entity: account,
entity: entity,
userCache: nil,
networkDate: Date(),
log: OSLog.api
)
if let searchHistory = user.searchHistory {
if let searchHistory = user.findSearchHistory(domain: box.domain, userID: box.userID) {
searchHistory.update(updatedAt: Date())
} else {
SearchHistory.insert(into: managedObjectContext, property: property, account: user)
}
}
.sink { result in
// do nothing
switch result {
case .failure(let error):
assertionFailure(error.localizedDescription)
case .success:
break
}
}
.store(in: &context.disposeBag)
case .hashtag(let hashtag):
case .hashtag(let entity):
let managedObjectContext = context.backgroundManagedObjectContext
var tag: Tag?
managedObjectContext.performChanges {
let (hashtag, _) = APIService.CoreData.createOrMergeTag(
into: managedObjectContext,
entity: hashtag
entity: entity
)
if let searchHistory = hashtag.searchHistory {
tag = hashtag
if let searchHistory = hashtag.findSearchHistory(domain: box.domain, userID: box.userID) {
searchHistory.update(updatedAt: Date())
} else {
SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag)
_ = SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag)
}
}
.sink { result in
// do nothing
switch result {
case .failure(let error):
assertionFailure(error.localizedDescription)
case .success:
print(tag?.searchHistories)
break
}
}
.store(in: &context.disposeBag)

View File

@ -104,6 +104,10 @@ extension SearchResultTableViewCell {
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
])
resetSeparatorLineLayout()
_titleLabel.isUserInteractionEnabled = false
_subTitleLabel.isUserInteractionEnabled = false
_imageView.isUserInteractionEnabled = false
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

View File

@ -545,7 +545,7 @@ extension SettingsViewController: ASWebAuthenticationPresentationContextProvidin
// MARK: - UIAdaptivePresentationControllerDelegate
extension SettingsViewController: UIAdaptivePresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .formSheet
return .pageSheet
}
}

View File

@ -86,15 +86,9 @@ extension APIService.CoreData {
let object = Poll.insert(into: managedObjectContext, property: Poll.Property(id: poll.id, expiresAt: poll.expiresAt, expired: poll.expired, multiple: poll.multiple, votesCount: poll.votesCount, votersCount: poll.votersCount, networkDate: networkDate), votedBy: votedBy, options: options)
return object
}
let metions = entity.mentions?.enumerated().compactMap { index, mention -> Mention in
let mentions = entity.mentions?.enumerated().compactMap { index, mention -> Mention in
Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url), index: index)
}
let tags = entity.tags?.compactMap { tag -> Tag in
let histories = tag.history?.compactMap { history -> History in
History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts))
}
return Tag.insert(into: managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories))
}
let mediaAttachments: [Attachment]? = {
let encoder = JSONEncoder()
var attachments: [Attachment] = []
@ -117,8 +111,7 @@ extension APIService.CoreData {
application: application,
replyTo: replyTo,
poll: poll,
mentions: metions,
tags: tags,
mentions: mentions,
mediaAttachments: mediaAttachments,
favouritedBy: (entity.favourited ?? false) ? requestMastodonUser : nil,
rebloggedBy: (entity.reblogged ?? false) ? requestMastodonUser : nil,

View File

@ -15,7 +15,7 @@ extension APIService.CoreData {
into managedObjectContext: NSManagedObjectContext,
entity: Mastodon.Entity.Tag
) -> (Tag: Tag, isCreated: Bool) {
// fetch old mastodon user
// fetch old hashtag 
let oldTag: Tag? = {
let request = Tag.sortedFetchRequest
request.predicate = Tag.predicate(name: entity.name)