Merge branch 'feat/hastagTimeline' of https://github.com/tootsuite/mastodon-ios into feat/hastagTimeline
This commit is contained in:
commit
412a7dc508
|
@ -1,11 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D75" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Application" representedClassName=".Application" syncable="YES">
|
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="name" attributeType="String"/>
|
<attribute name="name" attributeType="String"/>
|
||||||
<attribute name="vapidKey" optional="YES" attributeType="String"/>
|
<attribute name="vapidKey" optional="YES" attributeType="String"/>
|
||||||
<attribute name="website" optional="YES" attributeType="String"/>
|
<attribute name="website" optional="YES" attributeType="String"/>
|
||||||
<relationship name="toots" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="application" inverseEntity="Toot"/>
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="application" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Attachment" representedClassName=".Attachment" syncable="YES">
|
<entity name="Attachment" representedClassName=".Attachment" syncable="YES">
|
||||||
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
<attribute name="typeRaw" attributeType="String"/>
|
<attribute name="typeRaw" attributeType="String"/>
|
||||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="url" optional="YES" attributeType="String"/>
|
<attribute name="url" optional="YES" attributeType="String"/>
|
||||||
<relationship name="toot" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="mediaAttachments" inverseEntity="Toot"/>
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="mediaAttachments" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Emoji" representedClassName=".Emoji" syncable="YES">
|
<entity name="Emoji" representedClassName=".Emoji" syncable="YES">
|
||||||
<attribute name="category" optional="YES" attributeType="String"/>
|
<attribute name="category" optional="YES" attributeType="String"/>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<attribute name="staticURL" attributeType="String"/>
|
<attribute name="staticURL" attributeType="String"/>
|
||||||
<attribute name="url" attributeType="String"/>
|
<attribute name="url" attributeType="String"/>
|
||||||
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<relationship name="toot" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="emojis" inverseEntity="Toot"/>
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="emojis" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="History" representedClassName=".History" syncable="YES">
|
<entity name="History" representedClassName=".History" syncable="YES">
|
||||||
<attribute name="accounts" optional="YES" attributeType="String"/>
|
<attribute name="accounts" optional="YES" attributeType="String"/>
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
<attribute name="hasMore" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="hasMore" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="identifier" attributeType="String"/>
|
<attribute name="identifier" attributeType="String"/>
|
||||||
<attribute name="userID" attributeType="String"/>
|
<attribute name="userID" attributeType="String"/>
|
||||||
<relationship name="toot" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="homeTimelineIndexes" inverseEntity="Toot"/>
|
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="homeTimelineIndexes" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="MastodonAuthentication" representedClassName=".MastodonAuthentication" syncable="YES">
|
<entity name="MastodonAuthentication" representedClassName=".MastodonAuthentication" syncable="YES">
|
||||||
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
@ -72,17 +72,38 @@
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="displayName" attributeType="String"/>
|
<attribute name="displayName" attributeType="String"/>
|
||||||
<attribute name="domain" attributeType="String"/>
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<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="id" attributeType="String"/>
|
||||||
<attribute name="identifier" attributeType="String"/>
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="note" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="url" optional="YES" attributeType="String"/>
|
||||||
<attribute name="username" attributeType="String"/>
|
<attribute name="username" attributeType="String"/>
|
||||||
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="bookmarkedBy" inverseEntity="Toot"/>
|
<relationship name="blocking" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blockingBy" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="favourite" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="favouritedBy" inverseEntity="Toot"/>
|
<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="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
|
||||||
<relationship name="muted" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="mutedBy" inverseEntity="Toot"/>
|
<relationship name="muted" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="mutedBy" inverseEntity="Status"/>
|
||||||
<relationship name="pinnedToot" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="pinnedBy" inverseEntity="Toot"/>
|
<relationship name="muting" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mutingBy" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="reblogged" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="rebloggedBy" inverseEntity="Toot"/>
|
<relationship name="mutingBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muting" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="toots" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="author" inverseEntity="Toot"/>
|
<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="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="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"/>
|
<relationship name="votePolls" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
@ -93,7 +114,7 @@
|
||||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="url" attributeType="String"/>
|
<attribute name="url" attributeType="String"/>
|
||||||
<attribute name="username" attributeType="String"/>
|
<attribute name="username" attributeType="String"/>
|
||||||
<relationship name="toot" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="mentions" inverseEntity="Toot"/>
|
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="mentions" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Poll" representedClassName=".Poll" syncable="YES">
|
<entity name="Poll" representedClassName=".Poll" syncable="YES">
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
@ -105,7 +126,7 @@
|
||||||
<attribute name="votersCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="votersCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<relationship name="options" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
|
<relationship name="options" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
|
||||||
<relationship name="toot" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="poll" inverseEntity="Toot"/>
|
<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"/>
|
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePolls" inverseEntity="MastodonUser"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="PollOption" representedClassName=".PollOption" syncable="YES">
|
<entity name="PollOption" representedClassName=".PollOption" syncable="YES">
|
||||||
|
@ -117,15 +138,13 @@
|
||||||
<relationship name="poll" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
|
<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"/>
|
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Tag" representedClassName=".Tag" syncable="YES">
|
<entity name="PrivateNote" representedClassName="PrivateNote" syncable="YES">
|
||||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
<attribute name="note" optional="YES" attributeType="String"/>
|
||||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="name" attributeType="String"/>
|
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/>
|
||||||
<attribute name="url" attributeType="String"/>
|
<relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="histories" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="History" inverseName="tag" inverseEntity="History"/>
|
|
||||||
<relationship name="toot" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="tags" inverseEntity="Toot"/>
|
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Toot" representedClassName=".Toot" syncable="YES">
|
<entity name="Status" representedClassName=".Status" syncable="YES">
|
||||||
<attribute name="content" attributeType="String"/>
|
<attribute name="content" attributeType="String"/>
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
@ -145,23 +164,31 @@
|
||||||
<attribute name="uri" attributeType="String"/>
|
<attribute name="uri" attributeType="String"/>
|
||||||
<attribute name="url" attributeType="String"/>
|
<attribute name="url" attributeType="String"/>
|
||||||
<attribute name="visibility" optional="YES" attributeType="String"/>
|
<attribute name="visibility" optional="YES" attributeType="String"/>
|
||||||
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="toots" inverseEntity="Application"/>
|
<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="toots" inverseEntity="MastodonUser"/>
|
<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="bookmarkedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="emojis" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Emoji" inverseName="toot" inverseEntity="Emoji"/>
|
<relationship name="emojis" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Emoji" inverseName="status" inverseEntity="Emoji"/>
|
||||||
<relationship name="favouritedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" 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="Nullify" destinationEntity="HomeTimelineIndex" inverseName="toot" inverseEntity="HomeTimelineIndex"/>
|
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="HomeTimelineIndex" inverseName="status" inverseEntity="HomeTimelineIndex"/>
|
||||||
<relationship name="mediaAttachments" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Attachment" inverseName="toot" inverseEntity="Attachment"/>
|
<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="toot" inverseEntity="Mention"/>
|
<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="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="pinnedToot" 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="toot" inverseEntity="Poll"/>
|
<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="Toot" inverseName="reblogFrom" inverseEntity="Toot"/>
|
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogFrom" inverseEntity="Status"/>
|
||||||
<relationship name="reblogFrom" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="reblog" inverseEntity="Toot"/>
|
<relationship name="reblogFrom" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
|
||||||
<relationship name="rebloggedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
|
<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="Toot" inverseName="replyTo" inverseEntity="Toot"/>
|
<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="Toot" inverseName="replyFrom" inverseEntity="Toot"/>
|
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
|
||||||
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="toot" inverseEntity="Tag"/>
|
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="statuses" inverseEntity="Tag"/>
|
||||||
|
</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="url" attributeType="String"/>
|
||||||
|
<relationship name="histories" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="History" inverseName="tag" inverseEntity="History"/>
|
||||||
|
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="tags" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Application" positionX="160" positionY="192" width="128" height="104"/>
|
<element name="Application" positionX="160" positionY="192" width="128" height="104"/>
|
||||||
|
@ -170,11 +197,12 @@
|
||||||
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
||||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="314"/>
|
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="629"/>
|
||||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||||
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
|
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
|
||||||
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
||||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||||
<element name="Toot" positionX="0" positionY="0" width="128" height="14"/>
|
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
||||||
|
<element name="PrivateNote" positionX="72" positionY="153" width="128" height="89"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -38,7 +38,7 @@ public final class CoreDataStack {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static func persistentContainer() -> NSPersistentContainer {
|
static func persistentContainer() -> NSPersistentContainer {
|
||||||
let bundles = [Bundle(for: Toot.self)]
|
let bundles = [Bundle(for: Status.self)]
|
||||||
guard let managedObjectModel = NSManagedObjectModel.mergedModel(from: bundles) else {
|
guard let managedObjectModel = NSManagedObjectModel.mergedModel(from: bundles) else {
|
||||||
fatalError("cannot locate bundles")
|
fatalError("cannot locate bundles")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@ public final class Application: NSManagedObject {
|
||||||
@NSManaged public private(set) var website: String?
|
@NSManaged public private(set) var website: String?
|
||||||
@NSManaged public private(set) var vapidKey: String?
|
@NSManaged public private(set) var vapidKey: String?
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-one relationship
|
||||||
@NSManaged public private(set) var toots: Set<Toot>
|
@NSManaged public private(set) var status: Status
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Application {
|
public extension Application {
|
||||||
|
|
|
@ -28,7 +28,7 @@ public final class Attachment: NSManagedObject {
|
||||||
@NSManaged public private(set) var index: NSNumber
|
@NSManaged public private(set) var index: NSNumber
|
||||||
|
|
||||||
// many-to-one relastionship
|
// many-to-one relastionship
|
||||||
@NSManaged public private(set) var toot: Toot?
|
@NSManaged public private(set) var status: Status?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ public final class Emoji: NSManagedObject {
|
||||||
@NSManaged public private(set) var category: String?
|
@NSManaged public private(set) var category: String?
|
||||||
|
|
||||||
// many-to-one relationship
|
// many-to-one relationship
|
||||||
@NSManaged public private(set) var toot: Toot?
|
@NSManaged public private(set) var status: Status?
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Emoji {
|
public extension Emoji {
|
||||||
|
|
|
@ -22,7 +22,7 @@ final public class HomeTimelineIndex: NSManagedObject {
|
||||||
|
|
||||||
|
|
||||||
// many-to-one relationship
|
// many-to-one relationship
|
||||||
@NSManaged public private(set) var toot: Toot
|
@NSManaged public private(set) var status: Status
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,16 +32,16 @@ extension HomeTimelineIndex {
|
||||||
public static func insert(
|
public static func insert(
|
||||||
into context: NSManagedObjectContext,
|
into context: NSManagedObjectContext,
|
||||||
property: Property,
|
property: Property,
|
||||||
toot: Toot
|
status: Status
|
||||||
) -> HomeTimelineIndex {
|
) -> HomeTimelineIndex {
|
||||||
let index: HomeTimelineIndex = context.insertObject()
|
let index: HomeTimelineIndex = context.insertObject()
|
||||||
|
|
||||||
index.identifier = property.identifier
|
index.identifier = property.identifier
|
||||||
index.domain = property.domain
|
index.domain = property.domain
|
||||||
index.userID = property.userID
|
index.userID = property.userID
|
||||||
index.createdAt = toot.createdAt
|
index.createdAt = status.createdAt
|
||||||
|
|
||||||
index.toot = toot
|
index.status = status
|
||||||
|
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,24 +21,44 @@ final public class MastodonUser: NSManagedObject {
|
||||||
@NSManaged public private(set) var displayName: String
|
@NSManaged public private(set) var displayName: String
|
||||||
@NSManaged public private(set) var avatar: String
|
@NSManaged public private(set) var avatar: String
|
||||||
@NSManaged public private(set) var avatarStatic: 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 statusesCount: NSNumber
|
||||||
|
@NSManaged public private(set) var followingCount: NSNumber
|
||||||
|
@NSManaged public private(set) var followersCount: NSNumber
|
||||||
|
|
||||||
@NSManaged public private(set) var createdAt: Date
|
@NSManaged public private(set) var createdAt: Date
|
||||||
@NSManaged public private(set) var updatedAt: Date
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
|
||||||
// one-to-one relationship
|
// one-to-one relationship
|
||||||
@NSManaged public private(set) var pinnedToot: Toot?
|
@NSManaged public private(set) var pinnedStatus: Status?
|
||||||
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
|
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var toots: Set<Toot>?
|
@NSManaged public private(set) var statuses: Set<Status>?
|
||||||
|
|
||||||
// many-to-many relationship
|
// many-to-many relationship
|
||||||
@NSManaged public private(set) var favourite: Set<Toot>?
|
@NSManaged public private(set) var favourite: Set<Status>?
|
||||||
@NSManaged public private(set) var reblogged: Set<Toot>?
|
@NSManaged public private(set) var reblogged: Set<Status>?
|
||||||
@NSManaged public private(set) var muted: Set<Toot>?
|
@NSManaged public private(set) var muted: Set<Status>?
|
||||||
@NSManaged public private(set) var bookmarked: Set<Toot>?
|
@NSManaged public private(set) var bookmarked: Set<Status>?
|
||||||
@NSManaged public private(set) var votePollOptions: Set<PollOption>?
|
@NSManaged public private(set) var votePollOptions: Set<PollOption>?
|
||||||
@NSManaged public private(set) var votePolls: Set<Poll>?
|
@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>?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +80,16 @@ extension MastodonUser {
|
||||||
user.displayName = property.displayName
|
user.displayName = property.displayName
|
||||||
user.avatar = property.avatar
|
user.avatar = property.avatar
|
||||||
user.avatarStatic = property.avatarStatic
|
user.avatarStatic = property.avatarStatic
|
||||||
|
user.header = property.header
|
||||||
|
user.headerStatic = property.headerStatic
|
||||||
|
user.note = property.note
|
||||||
|
user.url = property.url
|
||||||
|
user.statusesCount = NSNumber(value: property.statusesCount)
|
||||||
|
user.followingCount = NSNumber(value: property.followingCount)
|
||||||
|
user.followersCount = NSNumber(value: property.followersCount)
|
||||||
|
|
||||||
|
// Mastodon do not provide relationship on the `Account`
|
||||||
|
// Update relationship via attribute updating interface
|
||||||
|
|
||||||
user.createdAt = property.createdAt
|
user.createdAt = property.createdAt
|
||||||
user.updatedAt = property.networkDate
|
user.updatedAt = property.networkDate
|
||||||
|
@ -93,6 +123,107 @@ extension MastodonUser {
|
||||||
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: 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(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) {
|
public func didUpdate(at networkDate: Date) {
|
||||||
self.updatedAt = networkDate
|
self.updatedAt = networkDate
|
||||||
|
@ -100,8 +231,8 @@ extension MastodonUser {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension MastodonUser {
|
extension MastodonUser {
|
||||||
struct Property {
|
public struct Property {
|
||||||
public let identifier: String
|
public let identifier: String
|
||||||
public let domain: String
|
public let domain: String
|
||||||
|
|
||||||
|
@ -111,6 +242,13 @@ public extension MastodonUser {
|
||||||
public let displayName: String
|
public let displayName: String
|
||||||
public let avatar: String
|
public let avatar: String
|
||||||
public let avatarStatic: String?
|
public let avatarStatic: String?
|
||||||
|
public let header: String
|
||||||
|
public let headerStatic: String?
|
||||||
|
public let note: String?
|
||||||
|
public let url: String?
|
||||||
|
public let statusesCount: Int
|
||||||
|
public let followingCount: Int
|
||||||
|
public let followersCount: Int
|
||||||
|
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
public let networkDate: Date
|
public let networkDate: Date
|
||||||
|
@ -123,6 +261,13 @@ public extension MastodonUser {
|
||||||
displayName: String,
|
displayName: String,
|
||||||
avatar: String,
|
avatar: String,
|
||||||
avatarStatic: String?,
|
avatarStatic: String?,
|
||||||
|
header: String,
|
||||||
|
headerStatic: String?,
|
||||||
|
note: String?,
|
||||||
|
url: String?,
|
||||||
|
statusesCount: Int,
|
||||||
|
followingCount: Int,
|
||||||
|
followersCount: Int,
|
||||||
createdAt: Date,
|
createdAt: Date,
|
||||||
networkDate: Date
|
networkDate: Date
|
||||||
) {
|
) {
|
||||||
|
@ -134,6 +279,13 @@ public extension MastodonUser {
|
||||||
self.displayName = displayName
|
self.displayName = displayName
|
||||||
self.avatar = avatar
|
self.avatar = avatar
|
||||||
self.avatarStatic = avatarStatic
|
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.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
self.networkDate = networkDate
|
self.networkDate = networkDate
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ public final class Mention: NSManagedObject {
|
||||||
@NSManaged public private(set) var url: String
|
@NSManaged public private(set) var url: String
|
||||||
|
|
||||||
// many-to-one relationship
|
// many-to-one relationship
|
||||||
@NSManaged public private(set) var toot: Toot
|
@NSManaged public private(set) var status: Status
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Mention {
|
public extension Mention {
|
||||||
|
|
|
@ -22,7 +22,7 @@ public final class Poll: NSManagedObject {
|
||||||
@NSManaged public private(set) var updatedAt: Date
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
|
||||||
// one-to-one relationship
|
// one-to-one relationship
|
||||||
@NSManaged public private(set) var toot: Toot
|
@NSManaged public private(set) var status: Status
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var options: Set<PollOption>
|
@NSManaged public private(set) var options: Set<PollOption>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
//
|
||||||
|
// PrivateNote.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-4-1.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final public class PrivateNote: NSManagedObject {
|
||||||
|
|
||||||
|
@NSManaged public private(set) var note: String?
|
||||||
|
|
||||||
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
|
||||||
|
// many-to-one relationship
|
||||||
|
@NSManaged public private(set) var to: MastodonUser?
|
||||||
|
@NSManaged public private(set) var from: MastodonUser
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PrivateNote {
|
||||||
|
public override func awakeFromInsert() {
|
||||||
|
super.awakeFromInsert()
|
||||||
|
setPrimitiveValue(Date(), forKey: #keyPath(PrivateNote.updatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public static func insert(
|
||||||
|
into context: NSManagedObjectContext,
|
||||||
|
property: Property
|
||||||
|
) -> PrivateNote {
|
||||||
|
let privateNode: PrivateNote = context.insertObject()
|
||||||
|
privateNode.note = property.note
|
||||||
|
return privateNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PrivateNote {
|
||||||
|
public struct Property {
|
||||||
|
public let note: String?
|
||||||
|
|
||||||
|
init(note: String) {
|
||||||
|
self.note = note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PrivateNote: Managed {
|
||||||
|
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||||
|
return [NSSortDescriptor(keyPath: \PrivateNote.updatedAt, ascending: false)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import CoreData
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public final class Toot: NSManagedObject {
|
public final class Status: NSManagedObject {
|
||||||
public typealias ID = String
|
public typealias ID = String
|
||||||
|
|
||||||
@NSManaged public private(set) var identifier: ID
|
@NSManaged public private(set) var identifier: ID
|
||||||
|
@ -30,7 +30,7 @@ public final class Toot: NSManagedObject {
|
||||||
@NSManaged public private(set) var repliesCount: NSNumber?
|
@NSManaged public private(set) var repliesCount: NSNumber?
|
||||||
|
|
||||||
@NSManaged public private(set) var url: String?
|
@NSManaged public private(set) var url: String?
|
||||||
@NSManaged public private(set) var inReplyToID: Toot.ID?
|
@NSManaged public private(set) var inReplyToID: Status.ID?
|
||||||
@NSManaged public private(set) var inReplyToAccountID: MastodonUser.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 language: String? // (ISO 639 Part 1 two-letter language code)
|
||||||
|
@ -38,8 +38,8 @@ public final class Toot: NSManagedObject {
|
||||||
|
|
||||||
// many-to-one relastionship
|
// many-to-one relastionship
|
||||||
@NSManaged public private(set) var author: MastodonUser
|
@NSManaged public private(set) var author: MastodonUser
|
||||||
@NSManaged public private(set) var reblog: Toot?
|
@NSManaged public private(set) var reblog: Status?
|
||||||
@NSManaged public private(set) var replyTo: Toot?
|
@NSManaged public private(set) var replyTo: Status?
|
||||||
|
|
||||||
// many-to-many relastionship
|
// many-to-many relastionship
|
||||||
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>?
|
||||||
|
@ -52,27 +52,27 @@ public final class Toot: NSManagedObject {
|
||||||
@NSManaged public private(set) var poll: Poll?
|
@NSManaged public private(set) var poll: Poll?
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var reblogFrom: Set<Toot>?
|
@NSManaged public private(set) var reblogFrom: Set<Status>?
|
||||||
@NSManaged public private(set) var mentions: Set<Mention>?
|
@NSManaged public private(set) var mentions: Set<Mention>?
|
||||||
@NSManaged public private(set) var emojis: Set<Emoji>?
|
@NSManaged public private(set) var emojis: Set<Emoji>?
|
||||||
@NSManaged public private(set) var tags: Set<Tag>?
|
@NSManaged public private(set) var tags: Set<Tag>?
|
||||||
@NSManaged public private(set) var homeTimelineIndexes: Set<HomeTimelineIndex>?
|
@NSManaged public private(set) var homeTimelineIndexes: Set<HomeTimelineIndex>?
|
||||||
@NSManaged public private(set) var mediaAttachments: Set<Attachment>?
|
@NSManaged public private(set) var mediaAttachments: Set<Attachment>?
|
||||||
@NSManaged public private(set) var replyFrom: Set<Toot>?
|
@NSManaged public private(set) var replyFrom: Set<Status>?
|
||||||
|
|
||||||
@NSManaged public private(set) var updatedAt: Date
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
@NSManaged public private(set) var deletedAt: Date?
|
@NSManaged public private(set) var deletedAt: Date?
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Toot {
|
public extension Status {
|
||||||
@discardableResult
|
@discardableResult
|
||||||
static func insert(
|
static func insert(
|
||||||
into context: NSManagedObjectContext,
|
into context: NSManagedObjectContext,
|
||||||
property: Property,
|
property: Property,
|
||||||
author: MastodonUser,
|
author: MastodonUser,
|
||||||
reblog: Toot?,
|
reblog: Status?,
|
||||||
application: Application?,
|
application: Application?,
|
||||||
replyTo: Toot?,
|
replyTo: Status?,
|
||||||
poll: Poll?,
|
poll: Poll?,
|
||||||
mentions: [Mention]?,
|
mentions: [Mention]?,
|
||||||
emojis: [Emoji]?,
|
emojis: [Emoji]?,
|
||||||
|
@ -83,8 +83,8 @@ public extension Toot {
|
||||||
mutedBy: MastodonUser?,
|
mutedBy: MastodonUser?,
|
||||||
bookmarkedBy: MastodonUser?,
|
bookmarkedBy: MastodonUser?,
|
||||||
pinnedBy: MastodonUser?
|
pinnedBy: MastodonUser?
|
||||||
) -> Toot {
|
) -> Status {
|
||||||
let toot: Toot = context.insertObject()
|
let toot: Status = context.insertObject()
|
||||||
|
|
||||||
toot.identifier = property.identifier
|
toot.identifier = property.identifier
|
||||||
toot.domain = property.domain
|
toot.domain = property.domain
|
||||||
|
@ -117,28 +117,28 @@ public extension Toot {
|
||||||
toot.poll = poll
|
toot.poll = poll
|
||||||
|
|
||||||
if let mentions = mentions {
|
if let mentions = mentions {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions)
|
toot.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions)
|
||||||
}
|
}
|
||||||
if let emojis = emojis {
|
if let emojis = emojis {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.emojis)).addObjects(from: emojis)
|
toot.mutableSetValue(forKey: #keyPath(Status.emojis)).addObjects(from: emojis)
|
||||||
}
|
}
|
||||||
if let tags = tags {
|
if let tags = tags {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.tags)).addObjects(from: tags)
|
toot.mutableSetValue(forKey: #keyPath(Status.tags)).addObjects(from: tags)
|
||||||
}
|
}
|
||||||
if let mediaAttachments = mediaAttachments {
|
if let mediaAttachments = mediaAttachments {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.mediaAttachments)).addObjects(from: mediaAttachments)
|
toot.mutableSetValue(forKey: #keyPath(Status.mediaAttachments)).addObjects(from: mediaAttachments)
|
||||||
}
|
}
|
||||||
if let favouritedBy = favouritedBy {
|
if let favouritedBy = favouritedBy {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(favouritedBy)
|
toot.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(favouritedBy)
|
||||||
}
|
}
|
||||||
if let rebloggedBy = rebloggedBy {
|
if let rebloggedBy = rebloggedBy {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).add(rebloggedBy)
|
toot.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(rebloggedBy)
|
||||||
}
|
}
|
||||||
if let mutedBy = mutedBy {
|
if let mutedBy = mutedBy {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).add(mutedBy)
|
toot.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mutedBy)
|
||||||
}
|
}
|
||||||
if let bookmarkedBy = bookmarkedBy {
|
if let bookmarkedBy = bookmarkedBy {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(bookmarkedBy)
|
toot.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(bookmarkedBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
toot.updatedAt = property.networkDate
|
toot.updatedAt = property.networkDate
|
||||||
|
@ -167,56 +167,56 @@ public extension Toot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(replyTo: Toot?) {
|
func update(replyTo: Status?) {
|
||||||
if self.replyTo != replyTo {
|
if self.replyTo != replyTo {
|
||||||
self.replyTo = replyTo
|
self.replyTo = replyTo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(liked: Bool, mastodonUser: MastodonUser) {
|
func update(liked: Bool, by mastodonUser: MastodonUser) {
|
||||||
if liked {
|
if liked {
|
||||||
if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
|
if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(mastodonUser)
|
self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(mastodonUser)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (self.favouritedBy ?? Set()).contains(mastodonUser) {
|
if (self.favouritedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).remove(mastodonUser)
|
self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).remove(mastodonUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(reblogged: Bool, mastodonUser: MastodonUser) {
|
func update(reblogged: Bool, by mastodonUser: MastodonUser) {
|
||||||
if reblogged {
|
if reblogged {
|
||||||
if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).add(mastodonUser)
|
self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(mastodonUser)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
if (self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).remove(mastodonUser)
|
self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).remove(mastodonUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(muted: Bool, mastodonUser: MastodonUser) {
|
func update(muted: Bool, by mastodonUser: MastodonUser) {
|
||||||
if muted {
|
if muted {
|
||||||
if !(self.mutedBy ?? Set()).contains(mastodonUser) {
|
if !(self.mutedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).add(mastodonUser)
|
self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mastodonUser)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (self.mutedBy ?? Set()).contains(mastodonUser) {
|
if (self.mutedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).remove(mastodonUser)
|
self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).remove(mastodonUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(bookmarked: Bool, mastodonUser: MastodonUser) {
|
func update(bookmarked: Bool, by mastodonUser: MastodonUser) {
|
||||||
if bookmarked {
|
if bookmarked {
|
||||||
if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(mastodonUser)
|
self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(mastodonUser)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
if (self.bookmarkedBy ?? Set()).contains(mastodonUser) {
|
||||||
self.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).remove(mastodonUser)
|
self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).remove(mastodonUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,7 +227,7 @@ public extension Toot {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Toot {
|
public extension Status {
|
||||||
struct Property {
|
struct Property {
|
||||||
|
|
||||||
public let identifier: ID
|
public let identifier: ID
|
||||||
|
@ -247,7 +247,7 @@ public extension Toot {
|
||||||
public let repliesCount: NSNumber?
|
public let repliesCount: NSNumber?
|
||||||
|
|
||||||
public let url: String?
|
public let url: String?
|
||||||
public let inReplyToID: Toot.ID?
|
public let inReplyToID: Status.ID?
|
||||||
public let inReplyToAccountID: MastodonUser.ID?
|
public let inReplyToAccountID: MastodonUser.ID?
|
||||||
public let language: String? // (ISO 639 Part @1 two-letter language code)
|
public let language: String? // (ISO 639 Part @1 two-letter language code)
|
||||||
public let text: String?
|
public let text: String?
|
||||||
|
@ -267,7 +267,7 @@ public extension Toot {
|
||||||
favouritesCount: NSNumber,
|
favouritesCount: NSNumber,
|
||||||
repliesCount: NSNumber?,
|
repliesCount: NSNumber?,
|
||||||
url: String?,
|
url: String?,
|
||||||
inReplyToID: Toot.ID?,
|
inReplyToID: Status.ID?,
|
||||||
inReplyToAccountID: MastodonUser.ID?,
|
inReplyToAccountID: MastodonUser.ID?,
|
||||||
language: String?,
|
language: String?,
|
||||||
text: String?,
|
text: String?,
|
||||||
|
@ -296,20 +296,20 @@ public extension Toot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Toot: Managed {
|
extension Status: Managed {
|
||||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||||
return [NSSortDescriptor(keyPath: \Toot.createdAt, ascending: false)]
|
return [NSSortDescriptor(keyPath: \Status.createdAt, ascending: false)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Toot {
|
extension Status {
|
||||||
|
|
||||||
static func predicate(domain: String) -> NSPredicate {
|
static func predicate(domain: String) -> NSPredicate {
|
||||||
return NSPredicate(format: "%K == %@", #keyPath(Toot.domain), domain)
|
return NSPredicate(format: "%K == %@", #keyPath(Status.domain), domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func predicate(id: String) -> NSPredicate {
|
static func predicate(id: String) -> NSPredicate {
|
||||||
return NSPredicate(format: "%K == %@", #keyPath(Toot.id), id)
|
return NSPredicate(format: "%K == %@", #keyPath(Status.id), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func predicate(domain: String, id: String) -> NSPredicate {
|
public static func predicate(domain: String, id: String) -> NSPredicate {
|
||||||
|
@ -320,7 +320,7 @@ extension Toot {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func predicate(ids: [String]) -> NSPredicate {
|
static func predicate(ids: [String]) -> NSPredicate {
|
||||||
return NSPredicate(format: "%K IN %@", #keyPath(Toot.id), ids)
|
return NSPredicate(format: "%K IN %@", #keyPath(Status.id), ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
|
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
|
||||||
|
@ -331,10 +331,10 @@ extension Toot {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func notDeleted() -> NSPredicate {
|
public static func notDeleted() -> NSPredicate {
|
||||||
return NSPredicate(format: "%K == nil", #keyPath(Toot.deletedAt))
|
return NSPredicate(format: "%K == nil", #keyPath(Status.deletedAt))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func deleted() -> NSPredicate {
|
public static func deleted() -> NSPredicate {
|
||||||
return NSPredicate(format: "%K != nil", #keyPath(Toot.deletedAt))
|
return NSPredicate(format: "%K != nil", #keyPath(Status.deletedAt))
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -17,7 +17,7 @@ public final class Tag: NSManagedObject {
|
||||||
@NSManaged public private(set) var url: String
|
@NSManaged public private(set) var url: String
|
||||||
|
|
||||||
// many-to-many relationship
|
// many-to-many relationship
|
||||||
@NSManaged public private(set) var toot: Toot
|
@NSManaged public private(set) var statuses: Set<Status>?
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var histories: Set<History>?
|
@NSManaged public private(set) var histories: Set<History>?
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
|
"done": "Done",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
@ -65,6 +66,15 @@
|
||||||
"closed": "Closed"
|
"closed": "Closed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"firendship": {
|
||||||
|
"follow": "Follow",
|
||||||
|
"following": "Following",
|
||||||
|
"block": "Block",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"mute": "Mute",
|
||||||
|
"muted": "Muted",
|
||||||
|
"edit_info": "Edit info"
|
||||||
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"loader": {
|
"loader": {
|
||||||
"load_missing_posts": "Load missing posts",
|
"load_missing_posts": "Load missing posts",
|
||||||
|
@ -162,6 +172,8 @@
|
||||||
"title": "Some ground rules.",
|
"title": "Some ground rules.",
|
||||||
"subtitle": "These rules are set by the admins of %s.",
|
"subtitle": "These rules are set by the admins of %s.",
|
||||||
"prompt": "By continuing, you're subject to the terms of service and privacy policy for %s.",
|
"prompt": "By continuing, you're subject to the terms of service and privacy policy for %s.",
|
||||||
|
"terms_of_service": "terms of service",
|
||||||
|
"privacy_policy": "privacy policy",
|
||||||
"button": {
|
"button": {
|
||||||
"confirm": "I Agree"
|
"confirm": "I Agree"
|
||||||
}
|
}
|
||||||
|
@ -235,6 +247,18 @@
|
||||||
"direct": "Only people I mention"
|
"direct": "Only people I mention"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"dashboard": {
|
||||||
|
"posts": "posts",
|
||||||
|
"following": "following",
|
||||||
|
"followers": "followers"
|
||||||
|
},
|
||||||
|
"segmented_control": {
|
||||||
|
"posts": "Posts",
|
||||||
|
"replies": "Replies",
|
||||||
|
"media": "Media"
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"searchBar": {
|
"searchBar": {
|
||||||
"placeholder": "Search hashtags and users",
|
"placeholder": "Search hashtags and users",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"NSCameraUsageDescription": "Used to take photo for toot",
|
"NSCameraUsageDescription": "Used to take photo for post status",
|
||||||
"NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library"
|
"NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library"
|
||||||
}
|
}
|
|
@ -57,7 +57,7 @@
|
||||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; };
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; };
|
||||||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; };
|
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; };
|
||||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
|
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
|
||||||
2D42FF6B25C817D2004A627A /* TootContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* TootContent.swift */; };
|
2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */; };
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
||||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; };
|
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; };
|
||||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
|
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
|
||||||
|
@ -111,6 +111,8 @@
|
||||||
2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; };
|
2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; };
|
||||||
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
||||||
2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */; };
|
2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */; };
|
||||||
|
5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; };
|
||||||
|
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; };
|
||||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
||||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; };
|
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; };
|
||||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; };
|
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; };
|
||||||
|
@ -126,7 +128,7 @@
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; };
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; };
|
||||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
|
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||||
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Toot.swift */; };
|
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||||
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
||||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
|
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
|
||||||
|
@ -142,6 +144,9 @@
|
||||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
||||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
||||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; };
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; };
|
||||||
|
DB35FC1F2612F1D9006193C9 /* ProfileFriendshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileFriendshipActionButton.swift */; };
|
||||||
|
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; };
|
||||||
|
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; };
|
||||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
||||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
||||||
|
@ -172,6 +177,9 @@
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
|
||||||
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; };
|
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; };
|
||||||
|
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; };
|
||||||
|
DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */; };
|
||||||
|
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; };
|
||||||
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; };
|
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; };
|
||||||
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
|
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
|
||||||
|
@ -190,7 +198,7 @@
|
||||||
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
|
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
|
||||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
|
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
|
||||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
|
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
|
||||||
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
|
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; };
|
||||||
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
||||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
||||||
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
|
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
|
||||||
|
@ -207,7 +215,7 @@
|
||||||
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
||||||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
|
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
|
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
|
||||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */; };
|
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */; };
|
||||||
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
|
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
|
||||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; };
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; };
|
||||||
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D44A2609C11900D12C0D /* PollOptionView.swift */; };
|
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D44A2609C11900D12C0D /* PollOptionView.swift */; };
|
||||||
|
@ -222,7 +230,7 @@
|
||||||
DB89BA1B25C1107F008580ED /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1825C1107F008580ED /* Collection.swift */; };
|
DB89BA1B25C1107F008580ED /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1825C1107F008580ED /* Collection.swift */; };
|
||||||
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */; };
|
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */; };
|
||||||
DB89BA1D25C1107F008580ED /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1A25C1107F008580ED /* URL.swift */; };
|
DB89BA1D25C1107F008580ED /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1A25C1107F008580ED /* URL.swift */; };
|
||||||
DB89BA2725C110B4008580ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA2625C110B4008580ED /* Toot.swift */; };
|
DB89BA2725C110B4008580ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA2625C110B4008580ED /* Status.swift */; };
|
||||||
DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA3525C1145C008580ED /* CoreData.xcdatamodeld */; };
|
DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA3525C1145C008580ED /* CoreData.xcdatamodeld */; };
|
||||||
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA4125C1165F008580ED /* NetworkUpdatable.swift */; };
|
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA4125C1165F008580ED /* NetworkUpdatable.swift */; };
|
||||||
DB89BA4425C1165F008580ED /* Managed.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA4225C1165F008580ED /* Managed.swift */; };
|
DB89BA4425C1165F008580ED /* Managed.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA4225C1165F008580ED /* Managed.swift */; };
|
||||||
|
@ -261,9 +269,30 @@
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
|
||||||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
|
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
|
||||||
|
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
||||||
|
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
||||||
|
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
|
||||||
|
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; };
|
||||||
|
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */; };
|
||||||
|
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525402611ED54002F1F29 /* ProfileHeaderViewController.swift */; };
|
||||||
|
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */; };
|
||||||
|
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */; };
|
||||||
|
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */; };
|
||||||
|
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; };
|
||||||
|
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */; };
|
||||||
|
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */; };
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
|
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
|
||||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; };
|
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; };
|
||||||
|
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */; };
|
||||||
|
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */; };
|
||||||
|
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; };
|
||||||
|
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B35261440BA0045B23D /* UINavigationController.swift */; };
|
||||||
|
DBCC3B7B261443AD0045B23D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B7A261443AD0045B23D /* ViewController.swift */; };
|
||||||
|
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B88261454BA0045B23D /* CGImage.swift */; };
|
||||||
|
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; };
|
||||||
|
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */; };
|
||||||
|
DBCC3B9B261584A00045B23D /* PrivateNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9A2615849F0045B23D /* PrivateNote.swift */; };
|
||||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
||||||
|
@ -374,7 +403,7 @@
|
||||||
2D38F1FD25CD481700561493 /* StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProvider.swift; sourceTree = "<group>"; };
|
2D38F1FD25CD481700561493 /* StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProvider.swift; sourceTree = "<group>"; };
|
||||||
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = "<group>"; };
|
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = "<group>"; };
|
||||||
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = "<group>"; };
|
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
2D42FF6A25C817D2004A627A /* TootContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TootContent.swift; sourceTree = "<group>"; };
|
2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonStatusContent.swift; sourceTree = "<group>"; };
|
||||||
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
|
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
|
||||||
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
||||||
2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
|
2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
|
||||||
|
@ -430,6 +459,8 @@
|
||||||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
||||||
|
5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
|
||||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = "<group>"; };
|
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = "<group>"; };
|
||||||
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
|
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
|
||||||
|
@ -445,7 +476,7 @@
|
||||||
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
||||||
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = "<group>"; };
|
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||||
DB084B5625CBC56C00F898ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = "<group>"; };
|
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
||||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
|
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
|
||||||
|
@ -462,6 +493,9 @@
|
||||||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
||||||
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB35FC1E2612F1D9006193C9 /* ProfileFriendshipActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFriendshipActionButton.swift; sourceTree = "<group>"; };
|
||||||
|
DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldView.swift; sourceTree = "<group>"; };
|
||||||
|
DB35FC2E26130172006193C9 /* MastodonField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = "<group>"; };
|
||||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
||||||
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -498,6 +532,9 @@
|
||||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
||||||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
|
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
|
||||||
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = "<group>"; };
|
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = "<group>"; };
|
||||||
|
DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+State.swift"; sourceTree = "<group>"; };
|
||||||
|
DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||||
|
DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+UserTimeline.swift"; sourceTree = "<group>"; };
|
||||||
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = "<group>"; };
|
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = "<group>"; };
|
||||||
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
|
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
|
||||||
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
|
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -515,7 +552,7 @@
|
||||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = "<group>"; };
|
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = "<group>"; };
|
||||||
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = "<group>"; };
|
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = "<group>"; };
|
||||||
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
|
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
|
||||||
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
|
DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
|
||||||
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||||
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
||||||
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; };
|
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; };
|
||||||
|
@ -532,7 +569,7 @@
|
||||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
||||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
|
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
||||||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB87D44A2609C11900D12C0D /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
|
DB87D44A2609C11900D12C0D /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -549,7 +586,7 @@
|
||||||
DB89BA1825C1107F008580ED /* Collection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = "<group>"; };
|
DB89BA1825C1107F008580ED /* Collection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = "<group>"; };
|
||||||
DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = "<group>"; };
|
DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = "<group>"; };
|
||||||
DB89BA1A25C1107F008580ED /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
|
DB89BA1A25C1107F008580ED /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
|
||||||
DB89BA2625C110B4008580ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = "<group>"; };
|
DB89BA2625C110B4008580ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
||||||
DB89BA3625C1145C008580ED /* CoreData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreData.xcdatamodel; sourceTree = "<group>"; };
|
DB89BA3625C1145C008580ED /* CoreData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreData.xcdatamodel; sourceTree = "<group>"; };
|
||||||
DB89BA4125C1165F008580ED /* NetworkUpdatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkUpdatable.swift; sourceTree = "<group>"; };
|
DB89BA4125C1165F008580ED /* NetworkUpdatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkUpdatable.swift; sourceTree = "<group>"; };
|
||||||
DB89BA4225C1165F008580ED /* Managed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Managed.swift; sourceTree = "<group>"; };
|
DB89BA4225C1165F008580ED /* Managed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Managed.swift; sourceTree = "<group>"; };
|
||||||
|
@ -587,9 +624,29 @@
|
||||||
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
|
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
|
||||||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
||||||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
||||||
|
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DBB525402611ED54002F1F29 /* ProfileHeaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
||||||
|
DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardView.swift; sourceTree = "<group>"; };
|
||||||
|
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardMeterView.swift; sourceTree = "<group>"; };
|
||||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||||
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
|
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
|
||||||
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = "<group>"; };
|
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = "<group>"; };
|
||||||
|
DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
|
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetchedResultsController.swift; sourceTree = "<group>"; };
|
||||||
|
DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = "<group>"; };
|
||||||
|
DBCC3B35261440BA0045B23D /* UINavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationController.swift; sourceTree = "<group>"; };
|
||||||
|
DBCC3B7A261443AD0045B23D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DBCC3B88261454BA0045B23D /* CGImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImage.swift; sourceTree = "<group>"; };
|
||||||
|
DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Relationship.swift"; sourceTree = "<group>"; };
|
||||||
|
DBCC3B9A2615849F0045B23D /* PrivateNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateNote.swift; sourceTree = "<group>"; };
|
||||||
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
|
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
|
||||||
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
||||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -609,6 +666,7 @@
|
||||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
||||||
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */,
|
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */,
|
||||||
2D939AC825EE14620076FA61 /* CropViewController in Frameworks */,
|
2D939AC825EE14620076FA61 /* CropViewController in Frameworks */,
|
||||||
|
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */,
|
||||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
|
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
|
||||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
||||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||||
|
@ -899,6 +957,7 @@
|
||||||
children = (
|
children = (
|
||||||
2D76319D25C151F600929FB9 /* Section */,
|
2D76319D25C151F600929FB9 /* Section */,
|
||||||
2D7631B125C159E700929FB9 /* Item */,
|
2D7631B125C159E700929FB9 /* Item */,
|
||||||
|
DBCBED2226132E1D00B49291 /* FetchedResultsController */,
|
||||||
);
|
);
|
||||||
path = Diffiable;
|
path = Diffiable;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -994,6 +1053,15 @@
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
5D03938E2612D200007FE196 /* Webview */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5D03938F2612D259007FE196 /* WebViewController.swift */,
|
||||||
|
5D0393952612D266007FE196 /* WebViewModel.swift */,
|
||||||
|
);
|
||||||
|
path = Webview;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB01409B25C40BB600F9F3CF /* Onboarding */ = {
|
DB01409B25C40BB600F9F3CF /* Onboarding */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1023,7 +1091,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
|
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
|
||||||
DB084B5625CBC56C00F898ED /* Toot.swift */,
|
DB084B5625CBC56C00F898ED /* Status.swift */,
|
||||||
DB9D6C3725E508BE0051B173 /* Attachment.swift */,
|
DB9D6C3725E508BE0051B173 /* Attachment.swift */,
|
||||||
);
|
);
|
||||||
path = CoreDataStack;
|
path = CoreDataStack;
|
||||||
|
@ -1104,6 +1172,7 @@
|
||||||
children = (
|
children = (
|
||||||
DB427DE325BAA00100D1B89D /* Info.plist */,
|
DB427DE325BAA00100D1B89D /* Info.plist */,
|
||||||
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
||||||
|
DBCC3B7A261443AD0045B23D /* ViewController.swift */,
|
||||||
2D76319C25C151DE00929FB9 /* Diffiable */,
|
2D76319C25C151DE00929FB9 /* Diffiable */,
|
||||||
DB8AF52A25C13561002E6C99 /* State */,
|
DB8AF52A25C13561002E6C99 /* State */,
|
||||||
2D61335525C1886800CAE157 /* Service */,
|
2D61335525C1886800CAE157 /* Service */,
|
||||||
|
@ -1154,6 +1223,7 @@
|
||||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
|
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
|
||||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
|
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
|
||||||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
|
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
|
||||||
|
DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */,
|
||||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
||||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
||||||
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
|
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
|
||||||
|
@ -1163,6 +1233,7 @@
|
||||||
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */,
|
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */,
|
||||||
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
||||||
0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */,
|
0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */,
|
||||||
|
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */,
|
||||||
);
|
);
|
||||||
path = APIService;
|
path = APIService;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1220,7 +1291,7 @@
|
||||||
DB68A04F25E9028800CFDF14 /* NavigationController */ = {
|
DB68A04F25E9028800CFDF14 /* NavigationController */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */,
|
DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */,
|
||||||
);
|
);
|
||||||
path = NavigationController;
|
path = NavigationController;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1259,7 +1330,7 @@
|
||||||
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */ = {
|
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */,
|
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */,
|
||||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
|
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
|
||||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
|
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
|
||||||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
||||||
|
@ -1317,7 +1388,7 @@
|
||||||
DB89BA2C25C110B7008580ED /* Entity */ = {
|
DB89BA2C25C110B7008580ED /* Entity */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB89BA2625C110B4008580ED /* Toot.swift */,
|
DB89BA2625C110B4008580ED /* Status.swift */,
|
||||||
DB8AF52425C131D1002E6C99 /* MastodonUser.swift */,
|
DB8AF52425C131D1002E6C99 /* MastodonUser.swift */,
|
||||||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */,
|
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */,
|
||||||
2D927F0125C7E4F2004F19B8 /* Mention.swift */,
|
2D927F0125C7E4F2004F19B8 /* Mention.swift */,
|
||||||
|
@ -1329,6 +1400,7 @@
|
||||||
DB9D6C2D25E504AC0051B173 /* Attachment.swift */,
|
DB9D6C2D25E504AC0051B173 /* Attachment.swift */,
|
||||||
DB4481AC25EE155900BEFB67 /* Poll.swift */,
|
DB4481AC25EE155900BEFB67 /* Poll.swift */,
|
||||||
DB4481B225EE16D000BEFB67 /* PollOption.swift */,
|
DB4481B225EE16D000BEFB67 /* PollOption.swift */,
|
||||||
|
DBCC3B9A2615849F0045B23D /* PrivateNote.swift */,
|
||||||
);
|
);
|
||||||
path = Entity;
|
path = Entity;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1373,6 +1445,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
0F2021F5261325ED000C64BF /* HashtagTimeline */,
|
0F2021F5261325ED000C64BF /* HashtagTimeline */,
|
||||||
|
5D03938E2612D200007FE196 /* Webview */,
|
||||||
2D7631A425C1532200929FB9 /* Share */,
|
2D7631A425C1532200929FB9 /* Share */,
|
||||||
DB8AF54E25C13703002E6C99 /* MainTab */,
|
DB8AF54E25C13703002E6C99 /* MainTab */,
|
||||||
DB01409B25C40BB600F9F3CF /* Onboarding */,
|
DB01409B25C40BB600F9F3CF /* Onboarding */,
|
||||||
|
@ -1404,6 +1477,7 @@
|
||||||
0FAA101B25E10E760017CCDE /* UIFont.swift */,
|
0FAA101B25E10E760017CCDE /* UIFont.swift */,
|
||||||
2D939AB425EDD8A90076FA61 /* String.swift */,
|
2D939AB425EDD8A90076FA61 /* String.swift */,
|
||||||
2D206B7F25F5F45E00143C56 /* UIImage.swift */,
|
2D206B7F25F5F45E00143C56 /* UIImage.swift */,
|
||||||
|
DBCC3B88261454BA0045B23D /* CGImage.swift */,
|
||||||
2D206B8525F5FB0900143C56 /* Double.swift */,
|
2D206B8525F5FB0900143C56 /* Double.swift */,
|
||||||
2D206B9125F60EA700143C56 /* UIControl.swift */,
|
2D206B9125F60EA700143C56 /* UIControl.swift */,
|
||||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
|
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
|
||||||
|
@ -1415,6 +1489,8 @@
|
||||||
2D84350425FF858100EECE90 /* UIScrollView.swift */,
|
2D84350425FF858100EECE90 /* UIScrollView.swift */,
|
||||||
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
|
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
|
||||||
0F20223826146553000C64BF /* Array+removeDuplicates.swift */,
|
0F20223826146553000C64BF /* Array+removeDuplicates.swift */,
|
||||||
|
DBCC3B2F261440A50045B23D /* UITabBarController.swift */,
|
||||||
|
DBCC3B35261440BA0045B23D /* UINavigationController.swift */,
|
||||||
);
|
);
|
||||||
path = Extension;
|
path = Extension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1469,7 +1545,13 @@
|
||||||
DB9D6C0825E4F5A60051B173 /* Profile */ = {
|
DB9D6C0825E4F5A60051B173 /* Profile */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DBB525132611EBB1002F1F29 /* Segmented */,
|
||||||
|
DBB525462611ED57002F1F29 /* Header */,
|
||||||
|
DBB5253B2611ECF5002F1F29 /* Timeline */,
|
||||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
|
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
|
||||||
|
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
|
||||||
|
DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */,
|
||||||
|
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = Profile;
|
path = Profile;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1499,7 +1581,8 @@
|
||||||
DB9E0D6925EDFFE500CFDD76 /* Helper */ = {
|
DB9E0D6925EDFFE500CFDD76 /* Helper */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2D42FF6A25C817D2004A627A /* TootContent.swift */,
|
2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */,
|
||||||
|
DB35FC2E26130172006193C9 /* MastodonField.swift */,
|
||||||
);
|
);
|
||||||
path = Helper;
|
path = Helper;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1521,6 +1604,65 @@
|
||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DBB525132611EBB1002F1F29 /* Segmented */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBB525262611EBDA002F1F29 /* Paging */,
|
||||||
|
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Segmented;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DBB525262611EBDA002F1F29 /* Paging */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */,
|
||||||
|
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */,
|
||||||
|
);
|
||||||
|
path = Paging;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DBB5253B2611ECF5002F1F29 /* Timeline */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */,
|
||||||
|
DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */,
|
||||||
|
DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */,
|
||||||
|
DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */,
|
||||||
|
DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */,
|
||||||
|
);
|
||||||
|
path = Timeline;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DBB525462611ED57002F1F29 /* Header */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBB525732612D5A5002F1F29 /* View */,
|
||||||
|
DBB525402611ED54002F1F29 /* ProfileHeaderViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Header;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DBB525732612D5A5002F1F29 /* View */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */,
|
||||||
|
DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */,
|
||||||
|
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */,
|
||||||
|
DB35FC1E2612F1D9006193C9 /* ProfileFriendshipActionButton.swift */,
|
||||||
|
DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */,
|
||||||
|
);
|
||||||
|
path = View;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DBCBED2226132E1D00B49291 /* FetchedResultsController */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
|
||||||
|
);
|
||||||
|
path = FetchedResultsController;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DBE0821A25CD382900FD6BBD /* Register */ = {
|
DBE0821A25CD382900FD6BBD /* Register */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1574,6 +1716,7 @@
|
||||||
2D939AC725EE14620076FA61 /* CropViewController */,
|
2D939AC725EE14620076FA61 /* CropViewController */,
|
||||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
||||||
DBE64A8A260C49D200E6359A /* TwitterTextEditor */,
|
DBE64A8A260C49D200E6359A /* TwitterTextEditor */,
|
||||||
|
DBB525072611EAC0002F1F29 /* Tabman */,
|
||||||
);
|
);
|
||||||
productName = Mastodon;
|
productName = Mastodon;
|
||||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||||
|
@ -1705,6 +1848,7 @@
|
||||||
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
|
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
|
||||||
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
|
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
|
||||||
DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||||
|
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */,
|
||||||
);
|
);
|
||||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -1889,6 +2033,8 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */,
|
||||||
|
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */,
|
||||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
|
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
|
||||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
||||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||||
|
@ -1896,16 +2042,20 @@
|
||||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
||||||
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
||||||
|
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
|
||||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
||||||
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */,
|
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */,
|
||||||
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||||
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
|
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
|
||||||
|
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */,
|
||||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||||
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
|
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
|
||||||
|
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
||||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
||||||
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
|
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
|
||||||
|
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */,
|
||||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
||||||
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */,
|
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */,
|
||||||
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
|
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
|
||||||
|
@ -1917,6 +2067,7 @@
|
||||||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
||||||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||||
|
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
|
||||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
|
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
|
||||||
0F1E2D0B2615C39400C38565 /* HashtagTimelineTitleView.swift in Sources */,
|
0F1E2D0B2615C39400C38565 /* HashtagTimelineTitleView.swift in Sources */,
|
||||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
||||||
|
@ -1930,6 +2081,7 @@
|
||||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
||||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
||||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
||||||
|
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
|
||||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||||
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */,
|
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */,
|
||||||
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
||||||
|
@ -1937,14 +2089,17 @@
|
||||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */,
|
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */,
|
||||||
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
||||||
|
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
|
||||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
||||||
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */,
|
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */,
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
|
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
|
||||||
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
||||||
|
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
|
||||||
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
||||||
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
||||||
|
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
||||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||||
|
@ -1974,12 +2129,15 @@
|
||||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
||||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
||||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||||
|
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */,
|
||||||
|
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */,
|
||||||
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
||||||
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
|
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
|
||||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||||
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
|
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
|
||||||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
||||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||||
|
DBCC3B7B261443AD0045B23D /* ViewController.swift in Sources */,
|
||||||
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */,
|
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */,
|
||||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
||||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
||||||
|
@ -1988,6 +2146,7 @@
|
||||||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
||||||
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
||||||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
||||||
|
5D0393902612D259007FE196 /* WebViewController.swift in Sources */,
|
||||||
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
|
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
|
||||||
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */,
|
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */,
|
||||||
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */,
|
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||||
|
@ -1997,10 +2156,12 @@
|
||||||
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
||||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
||||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||||
|
DB35FC1F2612F1D9006193C9 /* ProfileFriendshipActionButton.swift in Sources */,
|
||||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
||||||
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
||||||
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */,
|
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */,
|
||||||
|
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||||
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
|
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
|
||||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||||
|
@ -2016,11 +2177,12 @@
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||||
|
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
||||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||||
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */,
|
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */,
|
||||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
|
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
|
||||||
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
|
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
|
||||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||||
|
@ -2029,16 +2191,19 @@
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
||||||
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
|
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
||||||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
||||||
2D34D9CB261489930081BFC0 /* SearchViewController+recomendView.swift in Sources */,
|
2D34D9CB261489930081BFC0 /* SearchViewController+recomendView.swift in Sources */,
|
||||||
|
DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */,
|
||||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
||||||
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */,
|
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */,
|
||||||
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
|
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
|
||||||
|
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */,
|
||||||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
||||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
||||||
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
|
DB084B5725CBC56C00F898ED /* Status.swift in Sources */,
|
||||||
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
|
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
|
||||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
||||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||||
|
@ -2069,7 +2234,7 @@
|
||||||
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */,
|
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */,
|
||||||
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
|
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
|
||||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
||||||
2D42FF6B25C817D2004A627A /* TootContent.swift in Sources */,
|
2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */,
|
||||||
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
|
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
|
||||||
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */,
|
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */,
|
||||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||||
|
@ -2079,8 +2244,10 @@
|
||||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
||||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||||
|
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||||
0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */,
|
0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */,
|
||||||
|
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */,
|
||||||
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */,
|
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */,
|
||||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||||
|
@ -2088,19 +2255,23 @@
|
||||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
||||||
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
||||||
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
||||||
|
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||||
|
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
|
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
|
||||||
|
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
||||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
||||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
|
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
|
||||||
|
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
|
||||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */,
|
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -2131,11 +2302,12 @@
|
||||||
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
||||||
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */,
|
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */,
|
||||||
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
|
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
|
||||||
|
DBCC3B9B261584A00045B23D /* PrivateNote.swift in Sources */,
|
||||||
DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */,
|
DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */,
|
||||||
DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */,
|
DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */,
|
||||||
DB89BA1B25C1107F008580ED /* Collection.swift in Sources */,
|
DB89BA1B25C1107F008580ED /* Collection.swift in Sources */,
|
||||||
DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */,
|
DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */,
|
||||||
DB89BA2725C110B4008580ED /* Toot.swift in Sources */,
|
DB89BA2725C110B4008580ED /* Status.swift in Sources */,
|
||||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */,
|
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */,
|
||||||
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */,
|
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */,
|
||||||
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
||||||
|
@ -2695,6 +2867,14 @@
|
||||||
minimumVersion = 1.4.1;
|
minimumVersion = 1.4.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/uias/Tabman";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.11.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
|
DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/MainasuK/TwitterTextEditor";
|
repositoryURL = "https://github.com/MainasuK/TwitterTextEditor";
|
||||||
|
@ -2750,6 +2930,11 @@
|
||||||
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||||
productName = "UITextView+Placeholder";
|
productName = "UITextView+Placeholder";
|
||||||
};
|
};
|
||||||
|
DBB525072611EAC0002F1F29 /* Tabman */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */;
|
||||||
|
productName = Tabman;
|
||||||
|
};
|
||||||
DBE64A8A260C49D200E6359A /* TwitterTextEditor */ = {
|
DBE64A8A260C49D200E6359A /* TwitterTextEditor */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
package = DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
||||||
|
|
|
@ -55,6 +55,15 @@
|
||||||
"version": "6.1.0"
|
"version": "6.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "Pageboy",
|
||||||
|
"repositoryURL": "https://github.com/uias/Pageboy",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6",
|
||||||
|
"version": "3.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "swift-nio",
|
"package": "swift-nio",
|
||||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||||
|
@ -82,6 +91,15 @@
|
||||||
"version": "5.0.0"
|
"version": "5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "Tabman",
|
||||||
|
"repositoryURL": "https://github.com/uias/Tabman",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "bce2c87659c0ed868e6ef0aa1e05a330e202533f",
|
||||||
|
"version": "2.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "ThirdPartyMailer",
|
"package": "ThirdPartyMailer",
|
||||||
"repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git",
|
"repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git",
|
||||||
|
|
|
@ -46,6 +46,7 @@ extension SceneCoordinator {
|
||||||
case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
|
case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
|
||||||
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
|
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
|
||||||
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
|
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
|
||||||
|
case mastodonWebView(viewModel:WebViewModel)
|
||||||
|
|
||||||
// compose
|
// compose
|
||||||
case compose(viewModel: ComposeViewModel)
|
case compose(viewModel: ComposeViewModel)
|
||||||
|
@ -53,6 +54,9 @@ extension SceneCoordinator {
|
||||||
// Hashtag Timeline
|
// Hashtag Timeline
|
||||||
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
|
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
|
||||||
|
|
||||||
|
// profile
|
||||||
|
case profile(viewModel: ProfileViewModel)
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
case alertController(alertController: UIAlertController)
|
case alertController(alertController: UIAlertController)
|
||||||
|
|
||||||
|
@ -122,17 +126,18 @@ extension SceneCoordinator {
|
||||||
presentingViewController.show(viewController, sender: sender)
|
presentingViewController.show(viewController, sender: sender)
|
||||||
|
|
||||||
case .showDetail:
|
case .showDetail:
|
||||||
let navigationController = UINavigationController(rootViewController: viewController)
|
let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
|
||||||
presentingViewController.showDetailViewController(navigationController, sender: sender)
|
presentingViewController.showDetailViewController(navigationController, sender: sender)
|
||||||
|
|
||||||
case .modal(let animated, let completion):
|
case .modal(let animated, let completion):
|
||||||
let modalNavigationController: UINavigationController = {
|
let modalNavigationController: UINavigationController = {
|
||||||
if scene.isOnboarding {
|
if scene.isOnboarding {
|
||||||
return DarkContentStatusBarStyleNavigationController(rootViewController: viewController)
|
return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
|
||||||
} else {
|
} else {
|
||||||
return UINavigationController(rootViewController: viewController)
|
return UINavigationController(rootViewController: viewController)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
modalNavigationController.modalPresentationCapturesStatusBarAppearance = true
|
||||||
if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate {
|
if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate {
|
||||||
modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate
|
modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate
|
||||||
}
|
}
|
||||||
|
@ -149,12 +154,15 @@ extension SceneCoordinator {
|
||||||
sender?.navigationController?.pushViewController(viewController, animated: true)
|
sender?.navigationController?.pushViewController(viewController, animated: true)
|
||||||
|
|
||||||
case .safariPresent(let animated, let completion):
|
case .safariPresent(let animated, let completion):
|
||||||
|
viewController.modalPresentationCapturesStatusBarAppearance = true
|
||||||
presentingViewController.present(viewController, animated: animated, completion: completion)
|
presentingViewController.present(viewController, animated: animated, completion: completion)
|
||||||
|
|
||||||
case .activityViewControllerPresent(let animated, let completion):
|
case .activityViewControllerPresent(let animated, let completion):
|
||||||
|
viewController.modalPresentationCapturesStatusBarAppearance = true
|
||||||
presentingViewController.present(viewController, animated: animated, completion: completion)
|
presentingViewController.present(viewController, animated: animated, completion: completion)
|
||||||
|
|
||||||
case .alertController(let animated, let completion):
|
case .alertController(let animated, let completion):
|
||||||
|
viewController.modalPresentationCapturesStatusBarAppearance = true
|
||||||
presentingViewController.present(viewController, animated: animated, completion: completion)
|
presentingViewController.present(viewController, animated: animated, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,10 +204,18 @@ private extension SceneCoordinator {
|
||||||
let _viewController = MastodonResendEmailViewController()
|
let _viewController = MastodonResendEmailViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
|
case .mastodonWebView(let viewModel):
|
||||||
|
let _viewController = WebViewController()
|
||||||
|
_viewController.viewModel = viewModel
|
||||||
|
viewController = _viewController
|
||||||
case .compose(let viewModel):
|
case .compose(let viewModel):
|
||||||
let _viewController = ComposeViewController()
|
let _viewController = ComposeViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
|
case .profile(let viewModel):
|
||||||
|
let _viewController = ProfileViewController()
|
||||||
|
_viewController.viewModel = viewModel
|
||||||
|
viewController = _viewController
|
||||||
case .alertController(let alertController):
|
case .alertController(let alertController):
|
||||||
if let popoverPresentationController = alertController.popoverPresentationController {
|
if let popoverPresentationController = alertController.popoverPresentationController {
|
||||||
assert(
|
assert(
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// StatusFetchedResultsController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class StatusFetchedResultsController: NSObject {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let fetchedResultsController: NSFetchedResultsController<Status>
|
||||||
|
|
||||||
|
// input
|
||||||
|
let domain = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
let statusIDs = CurrentValueSubject<[Mastodon.Entity.Status.ID], Never>([])
|
||||||
|
|
||||||
|
// output
|
||||||
|
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||||
|
|
||||||
|
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate) {
|
||||||
|
self.domain.value = domain ?? ""
|
||||||
|
self.fetchedResultsController = {
|
||||||
|
let fetchRequest = Status.sortedFetchRequest
|
||||||
|
fetchRequest.predicate = Status.predicate(domain: domain ?? "", ids: [])
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
fetchRequest.fetchBatchSize = 20
|
||||||
|
let controller = NSFetchedResultsController(
|
||||||
|
fetchRequest: fetchRequest,
|
||||||
|
managedObjectContext: managedObjectContext,
|
||||||
|
sectionNameKeyPath: nil,
|
||||||
|
cacheName: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
return controller
|
||||||
|
}()
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
fetchedResultsController.delegate = self
|
||||||
|
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
self.domain.removeDuplicates().eraseToAnyPublisher(),
|
||||||
|
self.statusIDs.removeDuplicates().eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] domain, ids in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
Status.predicate(domain: domain ?? "", ids: ids),
|
||||||
|
additionalTweetPredicate
|
||||||
|
])
|
||||||
|
do {
|
||||||
|
try self.fetchedResultsController.performFetch()
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
|
extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||||
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
let indexes = statusIDs.value
|
||||||
|
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||||
|
|
||||||
|
let items: [NSManagedObjectID] = objects
|
||||||
|
.compactMap { object in
|
||||||
|
indexes.firstIndex(of: object.id).map { index in (index, object) }
|
||||||
|
}
|
||||||
|
.sorted { $0.0 < $1.0 }
|
||||||
|
.map { $0.1.objectID }
|
||||||
|
self.objectIDs.value = items
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,40 +16,44 @@ enum Item {
|
||||||
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||||
|
|
||||||
// normal list
|
// normal list
|
||||||
case toot(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
case status(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||||
|
|
||||||
// loader
|
// loader
|
||||||
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
|
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
|
||||||
case publicMiddleLoader(tootID: String)
|
case publicMiddleLoader(statusID: String)
|
||||||
case bottomLoader
|
case bottomLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol StatusContentWarningAttribute {
|
protocol StatusContentWarningAttribute {
|
||||||
var isStatusTextSensitive: Bool { get set }
|
var isStatusTextSensitive: Bool? { get set }
|
||||||
var isStatusSensitive: Bool { get set }
|
var isStatusSensitive: Bool? { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Item {
|
extension Item {
|
||||||
class StatusAttribute: Equatable, Hashable, StatusContentWarningAttribute {
|
class StatusAttribute: StatusContentWarningAttribute {
|
||||||
var isStatusTextSensitive: Bool
|
var isStatusTextSensitive: Bool?
|
||||||
var isStatusSensitive: Bool
|
var isStatusSensitive: Bool?
|
||||||
|
|
||||||
public init(
|
init(
|
||||||
isStatusTextSensitive: Bool,
|
isStatusTextSensitive: Bool? = nil,
|
||||||
isStatusSensitive: Bool
|
isStatusSensitive: Bool? = nil
|
||||||
) {
|
) {
|
||||||
self.isStatusTextSensitive = isStatusTextSensitive
|
self.isStatusTextSensitive = isStatusTextSensitive
|
||||||
self.isStatusSensitive = isStatusSensitive
|
self.isStatusSensitive = isStatusSensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: Item.StatusAttribute, rhs: Item.StatusAttribute) -> Bool {
|
// delay attribute init
|
||||||
return lhs.isStatusTextSensitive == rhs.isStatusTextSensitive &&
|
func setupForStatus(status: Status) {
|
||||||
lhs.isStatusSensitive == rhs.isStatusSensitive
|
if isStatusTextSensitive == nil {
|
||||||
|
isStatusTextSensitive = {
|
||||||
|
guard let spoilerText = status.spoilerText, !spoilerText.isEmpty else { return false }
|
||||||
|
return true
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
if isStatusSensitive == nil {
|
||||||
hasher.combine(isStatusTextSensitive)
|
isStatusSensitive = status.sensitive
|
||||||
hasher.combine(isStatusSensitive)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +63,7 @@ extension Item: Equatable {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)):
|
case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)):
|
||||||
return objectIDLeft == objectIDRight
|
return objectIDLeft == objectIDRight
|
||||||
case (.toot(let objectIDLeft, _), .toot(let objectIDRight, _)):
|
case (.status(let objectIDLeft, _), .status(let objectIDRight, _)):
|
||||||
return objectIDLeft == objectIDRight
|
return objectIDLeft == objectIDRight
|
||||||
case (.bottomLoader, .bottomLoader):
|
case (.bottomLoader, .bottomLoader):
|
||||||
return true
|
return true
|
||||||
|
@ -78,7 +82,7 @@ extension Item: Hashable {
|
||||||
switch self {
|
switch self {
|
||||||
case .homeTimelineIndex(let objectID, _):
|
case .homeTimelineIndex(let objectID, _):
|
||||||
hasher.combine(objectID)
|
hasher.combine(objectID)
|
||||||
case .toot(let objectID, _):
|
case .status(let objectID, _):
|
||||||
hasher.combine(objectID)
|
hasher.combine(objectID)
|
||||||
case .publicMiddleLoader(let upper):
|
case .publicMiddleLoader(let upper):
|
||||||
hasher.combine(String(describing: Item.publicMiddleLoader.self))
|
hasher.combine(String(describing: Item.publicMiddleLoader.self))
|
||||||
|
|
|
@ -49,14 +49,14 @@ extension ComposeStatusSection {
|
||||||
] collectionView, indexPath, item -> UICollectionViewCell? in
|
] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
case .replyTo(let repliedToStatusObjectID):
|
case .replyTo(let repliedToStatusObjectID):
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToTootContentCollectionViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentCollectionViewCell
|
||||||
return cell
|
return cell
|
||||||
case .input(let replyToTootObjectID, let attribute):
|
case .input(let replyToStatusObjectID, let attribute):
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
|
||||||
cell.textEditorView.text = attribute.composeContent.value ?? ""
|
cell.textEditorView.text = attribute.composeContent.value ?? ""
|
||||||
managedObjectContext.perform {
|
managedObjectContext.perform {
|
||||||
guard let replyToTootObjectID = replyToTootObjectID,
|
guard let replyToStatusObjectID = replyToStatusObjectID,
|
||||||
let replyTo = managedObjectContext.object(with: replyToTootObjectID) as? Toot else {
|
let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
||||||
cell.statusView.headerContainerStackView.isHidden = true
|
cell.statusView.headerContainerStackView.isHidden = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,41 +39,41 @@ extension StatusSection {
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
toot: timelineIndex.toot,
|
status: timelineIndex.status,
|
||||||
requestUserID: timelineIndex.userID,
|
requestUserID: timelineIndex.userID,
|
||||||
statusItemAttribute: attribute
|
statusItemAttribute: attribute
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cell.delegate = statusTableViewCellDelegate
|
cell.delegate = statusTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
case .toot(let objectID, let attribute):
|
case .status(let objectID, let attribute):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
||||||
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
|
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
|
||||||
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
|
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
|
||||||
// configure cell
|
// configure cell
|
||||||
managedObjectContext.performAndWait {
|
managedObjectContext.performAndWait {
|
||||||
let toot = managedObjectContext.object(with: objectID) as! Toot
|
let status = managedObjectContext.object(with: objectID) as! Status
|
||||||
StatusSection.configure(
|
StatusSection.configure(
|
||||||
cell: cell,
|
cell: cell,
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
toot: toot,
|
status: status,
|
||||||
requestUserID: requestUserID,
|
requestUserID: requestUserID,
|
||||||
statusItemAttribute: attribute
|
statusItemAttribute: attribute
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cell.delegate = statusTableViewCellDelegate
|
cell.delegate = statusTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
case .publicMiddleLoader(let upperTimelineTootID):
|
case .publicMiddleLoader(let upperTimelineStatusID):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
||||||
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
|
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
|
||||||
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineTootID: upperTimelineTootID, timelineIndexobjectID: nil)
|
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineStatusID: upperTimelineStatusID, timelineIndexobjectID: nil)
|
||||||
return cell
|
return cell
|
||||||
case .homeMiddleLoader(let upperTimelineIndexObjectID):
|
case .homeMiddleLoader(let upperTimelineIndexObjectID):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
||||||
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
|
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
|
||||||
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineTootID: nil, timelineIndexobjectID: upperTimelineIndexObjectID)
|
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineStatusID: nil, timelineIndexobjectID: upperTimelineIndexObjectID)
|
||||||
return cell
|
return cell
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
|
@ -90,47 +90,50 @@ extension StatusSection {
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
readableLayoutFrame: CGRect?,
|
readableLayoutFrame: CGRect?,
|
||||||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||||
toot: Toot,
|
status: Status,
|
||||||
requestUserID: String,
|
requestUserID: String,
|
||||||
statusItemAttribute: Item.StatusAttribute
|
statusItemAttribute: Item.StatusAttribute
|
||||||
) {
|
) {
|
||||||
|
// setup attribute
|
||||||
|
statusItemAttribute.setupForStatus(status: status.reblog ?? status)
|
||||||
|
|
||||||
// set header
|
// set header
|
||||||
StatusSection.configureHeader(cell: cell, toot: toot)
|
StatusSection.configureHeader(cell: cell, status: status)
|
||||||
ManagedObjectObserver.observe(object: toot)
|
ManagedObjectObserver.observe(object: status)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { change in
|
} receiveValue: { change in
|
||||||
guard case .update(let object) = change.changeType,
|
guard case .update(let object) = change.changeType,
|
||||||
let newToot = object as? Toot else { return }
|
let newStatus = object as? Status else { return }
|
||||||
StatusSection.configureHeader(cell: cell, toot: newToot)
|
StatusSection.configureHeader(cell: cell, status: newStatus)
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
// set name username
|
// set name username
|
||||||
cell.statusView.nameLabel.text = {
|
cell.statusView.nameLabel.text = {
|
||||||
let author = (toot.reblog ?? toot).author
|
let author = (status.reblog ?? status).author
|
||||||
return author.displayName.isEmpty ? author.username : author.displayName
|
return author.displayName.isEmpty ? author.username : author.displayName
|
||||||
}()
|
}()
|
||||||
cell.statusView.usernameLabel.text = "@" + (toot.reblog ?? toot).author.acct
|
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||||
// set avatar
|
// set avatar
|
||||||
if let reblog = toot.reblog {
|
if let reblog = status.reblog {
|
||||||
cell.statusView.avatarButton.isHidden = true
|
cell.statusView.avatarButton.isHidden = true
|
||||||
cell.statusView.avatarStackedContainerButton.isHidden = false
|
cell.statusView.avatarStackedContainerButton.isHidden = false
|
||||||
cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL()))
|
cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL()))
|
||||||
cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL()))
|
cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
||||||
} else {
|
} else {
|
||||||
cell.statusView.avatarButton.isHidden = false
|
cell.statusView.avatarButton.isHidden = false
|
||||||
cell.statusView.avatarStackedContainerButton.isHidden = true
|
cell.statusView.avatarStackedContainerButton.isHidden = true
|
||||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL()))
|
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// set text
|
// set text
|
||||||
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content)
|
||||||
|
|
||||||
// set status text content warning
|
// set status text content warning
|
||||||
let spoilerText = (toot.reblog ?? toot).spoilerText ?? ""
|
let isStatusTextSensitive = statusItemAttribute.isStatusTextSensitive ?? false
|
||||||
let isStatusTextSensitive = statusItemAttribute.isStatusTextSensitive
|
let spoilerText = (status.reblog ?? status).spoilerText ?? ""
|
||||||
cell.statusView.isStatusTextSensitive = isStatusTextSensitive
|
cell.statusView.isStatusTextSensitive = isStatusTextSensitive
|
||||||
cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive)
|
cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive)
|
||||||
cell.statusView.contentWarningTitle.text = {
|
cell.statusView.contentWarningTitle.text = {
|
||||||
|
@ -142,7 +145,7 @@ extension StatusSection {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// prepare media attachments
|
// prepare media attachments
|
||||||
let mediaAttachments = Array((toot.reblog ?? toot).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
||||||
|
|
||||||
// set image
|
// set image
|
||||||
let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments)
|
let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments)
|
||||||
|
@ -184,7 +187,7 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
|
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
|
||||||
let isStatusSensitive = statusItemAttribute.isStatusSensitive
|
let isStatusSensitive = statusItemAttribute.isStatusSensitive ?? false
|
||||||
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil
|
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil
|
||||||
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
|
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
|
||||||
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive
|
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive
|
||||||
|
@ -251,7 +254,7 @@ extension StatusSection {
|
||||||
cell.statusView.playerContainerView.playerViewController.player = nil
|
cell.statusView.playerContainerView.playerViewController.player = nil
|
||||||
}
|
}
|
||||||
// set poll
|
// set poll
|
||||||
let poll = (toot.reblog ?? toot).poll
|
let poll = (status.reblog ?? status).poll
|
||||||
StatusSection.configurePoll(
|
StatusSection.configurePoll(
|
||||||
cell: cell,
|
cell: cell,
|
||||||
poll: poll,
|
poll: poll,
|
||||||
|
@ -278,10 +281,10 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolbar
|
// toolbar
|
||||||
StatusSection.configureActionToolBar(cell: cell, toot: toot, requestUserID: requestUserID)
|
StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
|
||||||
|
|
||||||
// set date
|
// set date
|
||||||
let createdAt = (toot.reblog ?? toot).createdAt
|
let createdAt = (status.reblog ?? status).createdAt
|
||||||
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
||||||
timestampUpdatePublisher
|
timestampUpdatePublisher
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
|
@ -290,34 +293,34 @@ extension StatusSection {
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
// observe model change
|
// observe model change
|
||||||
ManagedObjectObserver.observe(object: toot.reblog ?? toot)
|
ManagedObjectObserver.observe(object: status.reblog ?? status)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { change in
|
} receiveValue: { change in
|
||||||
guard case .update(let object) = change.changeType,
|
guard case .update(let object) = change.changeType,
|
||||||
let toot = object as? Toot else { return }
|
let status = object as? Status else { return }
|
||||||
StatusSection.configureActionToolBar(cell: cell, toot: toot, requestUserID: requestUserID)
|
StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
|
||||||
|
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: reblog count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.reblogsCount.intValue)
|
os_log("%{public}s[%{public}ld], %{public}s: reblog count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.reblogsCount.intValue)
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: like count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.favouritesCount.intValue)
|
os_log("%{public}s[%{public}ld], %{public}s: like count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.favouritesCount.intValue)
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func configureHeader(
|
static func configureHeader(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusTableViewCell,
|
||||||
toot: Toot
|
status: Status
|
||||||
) {
|
) {
|
||||||
if toot.reblog != nil {
|
if status.reblog != nil {
|
||||||
cell.statusView.headerContainerStackView.isHidden = false
|
cell.statusView.headerContainerStackView.isHidden = false
|
||||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage)
|
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage)
|
||||||
cell.statusView.headerInfoLabel.text = {
|
cell.statusView.headerInfoLabel.text = {
|
||||||
let author = toot.author
|
let author = status.author
|
||||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||||
return L10n.Common.Controls.Status.userReblogged(name)
|
return L10n.Common.Controls.Status.userReblogged(name)
|
||||||
}()
|
}()
|
||||||
} else if let replyTo = toot.replyTo {
|
} else if let replyTo = status.replyTo {
|
||||||
cell.statusView.headerContainerStackView.isHidden = false
|
cell.statusView.headerContainerStackView.isHidden = false
|
||||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||||
cell.statusView.headerInfoLabel.text = {
|
cell.statusView.headerInfoLabel.text = {
|
||||||
|
@ -332,29 +335,29 @@ extension StatusSection {
|
||||||
|
|
||||||
static func configureActionToolBar(
|
static func configureActionToolBar(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusTableViewCell,
|
||||||
toot: Toot,
|
status: Status,
|
||||||
requestUserID: String
|
requestUserID: String
|
||||||
) {
|
) {
|
||||||
let toot = toot.reblog ?? toot
|
let status = status.reblog ?? status
|
||||||
|
|
||||||
// set reply
|
// set reply
|
||||||
let replyCountTitle: String = {
|
let replyCountTitle: String = {
|
||||||
let count = toot.repliesCount?.intValue ?? 0
|
let count = status.repliesCount?.intValue ?? 0
|
||||||
return StatusSection.formattedNumberTitleForActionButton(count)
|
return StatusSection.formattedNumberTitleForActionButton(count)
|
||||||
}()
|
}()
|
||||||
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
|
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
|
||||||
// set reblog
|
// set reblog
|
||||||
let isReblogged = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||||
let reblogCountTitle: String = {
|
let reblogCountTitle: String = {
|
||||||
let count = toot.reblogsCount.intValue
|
let count = status.reblogsCount.intValue
|
||||||
return StatusSection.formattedNumberTitleForActionButton(count)
|
return StatusSection.formattedNumberTitleForActionButton(count)
|
||||||
}()
|
}()
|
||||||
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal)
|
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal)
|
||||||
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged
|
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged
|
||||||
// set like
|
// set like
|
||||||
let isLike = toot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||||
let favoriteCountTitle: String = {
|
let favoriteCountTitle: String = {
|
||||||
let count = toot.favouritesCount.intValue
|
let count = status.favouritesCount.intValue
|
||||||
return StatusSection.formattedNumberTitleForActionButton(count)
|
return StatusSection.formattedNumberTitleForActionButton(count)
|
||||||
}()
|
}()
|
||||||
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
|
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
|
||||||
|
|
|
@ -14,40 +14,61 @@ extension ActiveLabel {
|
||||||
|
|
||||||
enum Style {
|
enum Style {
|
||||||
case `default`
|
case `default`
|
||||||
case timelineHeaderView
|
case profileField
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(style: Style) {
|
convenience init(style: Style) {
|
||||||
self.init()
|
self.init()
|
||||||
|
|
||||||
switch style {
|
|
||||||
case .default:
|
|
||||||
font = .preferredFont(forTextStyle: .body)
|
|
||||||
textColor = Asset.Colors.Label.primary.color
|
|
||||||
case .timelineHeaderView:
|
|
||||||
font = .preferredFont(forTextStyle: .footnote)
|
|
||||||
textColor = .secondaryLabel
|
|
||||||
}
|
|
||||||
|
|
||||||
numberOfLines = 0
|
numberOfLines = 0
|
||||||
lineSpacing = 5
|
lineSpacing = 5
|
||||||
mentionColor = Asset.Colors.Label.highlight.color
|
mentionColor = Asset.Colors.Label.highlight.color
|
||||||
hashtagColor = Asset.Colors.Label.highlight.color
|
hashtagColor = Asset.Colors.Label.highlight.color
|
||||||
URLColor = Asset.Colors.Label.highlight.color
|
URLColor = Asset.Colors.Label.highlight.color
|
||||||
|
#if DEBUG
|
||||||
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
switch style {
|
||||||
|
case .default:
|
||||||
|
font = .preferredFont(forTextStyle: .body)
|
||||||
|
textColor = Asset.Colors.Label.primary.color
|
||||||
|
case .profileField:
|
||||||
|
font = .preferredFont(forTextStyle: .body)
|
||||||
|
textColor = Asset.Colors.Label.primary.color
|
||||||
|
numberOfLines = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ActiveLabel {
|
extension ActiveLabel {
|
||||||
func config(content: String) {
|
/// status content
|
||||||
|
func configure(content: String) {
|
||||||
activeEntities.removeAll()
|
activeEntities.removeAll()
|
||||||
if let parseResult = try? TootContent.parse(toot: content) {
|
if let parseResult = try? MastodonStatusContent.parse(status: content) {
|
||||||
text = parseResult.trimmed
|
text = parseResult.trimmed
|
||||||
activeEntities = parseResult.activeEntities
|
activeEntities = parseResult.activeEntities
|
||||||
} else {
|
} else {
|
||||||
text = ""
|
text = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// account note
|
||||||
|
func configure(note: String) {
|
||||||
|
configure(content: note)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ActiveLabel {
|
||||||
|
/// account field
|
||||||
|
func configure(field: String) {
|
||||||
|
activeEntities.removeAll()
|
||||||
|
if let parseResult = try? MastodonField.parse(field: field) {
|
||||||
|
text = parseResult.value
|
||||||
|
activeEntities = parseResult.activeEntities
|
||||||
|
} else {
|
||||||
|
text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
//
|
||||||
|
// CGImage.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-31.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreImage
|
||||||
|
|
||||||
|
extension CGImage {
|
||||||
|
// Reference
|
||||||
|
// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
|
||||||
|
// Luma Y = 0.2126R + 0.7152G + 0.0722B
|
||||||
|
var brightness: CGFloat? {
|
||||||
|
let context = CIContext() // default with metal accelerate
|
||||||
|
let ciImage = CIImage(cgImage: self)
|
||||||
|
let rec709Image = context.createCGImage(
|
||||||
|
ciImage,
|
||||||
|
from: ciImage.extent,
|
||||||
|
format: .RGBA8,
|
||||||
|
colorSpace: CGColorSpace(name: CGColorSpace.itur_709) // BT.709 a.k.a Rec.709
|
||||||
|
)
|
||||||
|
guard let image = rec709Image,
|
||||||
|
image.bitsPerPixel == 32,
|
||||||
|
let data = rec709Image?.dataProvider?.data,
|
||||||
|
let pointer = CFDataGetBytePtr(data) else { return nil }
|
||||||
|
|
||||||
|
let length = CFDataGetLength(data)
|
||||||
|
guard length > 0 else { return nil}
|
||||||
|
|
||||||
|
var luma: CGFloat = 0.0
|
||||||
|
for i in stride(from: 0, to: length, by: 4) {
|
||||||
|
let r = pointer[i]
|
||||||
|
let g = pointer[i + 1]
|
||||||
|
let b = pointer[i + 2]
|
||||||
|
let Y = 0.2126 * CGFloat(r) + 0.7152 * CGFloat(g) + 0.0722 * CGFloat(b)
|
||||||
|
luma += Y
|
||||||
|
}
|
||||||
|
luma /= CGFloat(width * height)
|
||||||
|
return luma
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class BrightnessView: UIView {
|
||||||
|
let label = UILabel()
|
||||||
|
let imageView = UIImageView()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
let stackView = UIStackView()
|
||||||
|
stackView.axis = .horizontal
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(stackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
stackView.distribution = .fillEqually
|
||||||
|
stackView.addArrangedSubview(imageView)
|
||||||
|
stackView.addArrangedSubview(label)
|
||||||
|
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.numberOfLines = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setImage(_ image: UIImage) {
|
||||||
|
imageView.image = image
|
||||||
|
|
||||||
|
guard let brightness = image.cgImage?.brightness,
|
||||||
|
let style = image.domainLumaCoefficientsStyle else {
|
||||||
|
label.text = "<nil>"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let styleDescription: String = {
|
||||||
|
switch style {
|
||||||
|
case .light: return "Light"
|
||||||
|
case .dark: return "Dark"
|
||||||
|
case .unspecified: fallthrough
|
||||||
|
@unknown default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
label.text = styleDescription + "\n" + "\(brightness)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CGImage_Brightness_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = BrightnessView()
|
||||||
|
view.setImage(.placeholder(color: .black))
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = BrightnessView()
|
||||||
|
view.setImage(.placeholder(color: .gray))
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = BrightnessView()
|
||||||
|
view.setImage(.placeholder(color: .separator))
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = BrightnessView()
|
||||||
|
view.setImage(.placeholder(color: .red))
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = BrightnessView()
|
||||||
|
view.setImage(.placeholder(color: .green))
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = BrightnessView()
|
||||||
|
view.setImage(.placeholder(color: .blue))
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = BrightnessView()
|
||||||
|
view.setImage(.placeholder(color: .secondarySystemGroupedBackground))
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,13 @@ extension MastodonUser.Property {
|
||||||
displayName: entity.displayName,
|
displayName: entity.displayName,
|
||||||
avatar: entity.avatar,
|
avatar: entity.avatar,
|
||||||
avatarStatic: entity.avatarStatic,
|
avatarStatic: entity.avatarStatic,
|
||||||
|
header: entity.header,
|
||||||
|
headerStatic: entity.headerStatic,
|
||||||
|
note: entity.note,
|
||||||
|
url: entity.url,
|
||||||
|
statusesCount: entity.statusesCount,
|
||||||
|
followingCount: entity.followingCount,
|
||||||
|
followersCount: entity.followersCount,
|
||||||
createdAt: entity.createdAt,
|
createdAt: entity.createdAt,
|
||||||
networkDate: networkDate
|
networkDate: networkDate
|
||||||
)
|
)
|
||||||
|
@ -26,7 +33,25 @@ extension MastodonUser.Property {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonUser {
|
extension MastodonUser {
|
||||||
|
|
||||||
|
var displayNameWithFallback: String {
|
||||||
|
return !displayName.isEmpty ? displayName : username
|
||||||
|
}
|
||||||
|
|
||||||
|
var acctWithDomain: String {
|
||||||
|
return username + "@" + domain
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonUser {
|
||||||
|
|
||||||
|
public func headerImageURL() -> URL? {
|
||||||
|
return URL(string: header)
|
||||||
|
}
|
||||||
|
|
||||||
public func avatarImageURL() -> URL? {
|
public func avatarImageURL() -> URL? {
|
||||||
return URL(string: avatar)
|
return URL(string: avatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// Toot.swift
|
// Status.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021/2/4.
|
// Created by MainasuK Cirno on 2021/2/4.
|
||||||
|
@ -9,7 +9,7 @@ import Foundation
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
extension Toot.Property {
|
extension Status.Property {
|
||||||
init(entity: Mastodon.Entity.Status, domain: String, networkDate: Date) {
|
init(entity: Mastodon.Entity.Status, domain: String, networkDate: Date) {
|
||||||
self.init(
|
self.init(
|
||||||
domain: domain,
|
domain: domain,
|
|
@ -39,6 +39,13 @@ extension UIImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension UIImage {
|
||||||
|
var domainLumaCoefficientsStyle: UIUserInterfaceStyle? {
|
||||||
|
guard let brightness = cgImage?.brightness else { return nil }
|
||||||
|
return brightness > 100 ? .light : .dark // 0 ~ 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension UIImage {
|
extension UIImage {
|
||||||
func blur(radius: CGFloat) -> UIImage? {
|
func blur(radius: CGFloat) -> UIImage? {
|
||||||
guard let inputImage = CIImage(image: self) else { return nil }
|
guard let inputImage = CIImage(image: self) else { return nil }
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
// UINavigationController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-31.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
// This not works!
|
||||||
|
// SeeAlso: `AdaptiveStatusBarStyleNavigationController`
|
||||||
|
extension UINavigationController {
|
||||||
|
open override var childForStatusBarStyle: UIViewController? {
|
||||||
|
assertionFailure("Won't enter here")
|
||||||
|
return visibleViewController
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
//
|
||||||
|
// UITabBarController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-31.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UITabBarController {
|
||||||
|
open override var childForStatusBarStyle: UIViewController? {
|
||||||
|
return selectedViewController
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ internal enum Asset {
|
||||||
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
|
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
|
||||||
internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight")
|
internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight")
|
||||||
}
|
}
|
||||||
|
internal static let alertYellow = ColorAsset(name: "Colors/Background/alert.yellow")
|
||||||
internal static let dangerBorder = ColorAsset(name: "Colors/Background/danger.border")
|
internal static let dangerBorder = ColorAsset(name: "Colors/Background/danger.border")
|
||||||
internal static let danger = ColorAsset(name: "Colors/Background/danger")
|
internal static let danger = ColorAsset(name: "Colors/Background/danger")
|
||||||
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor")
|
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor")
|
||||||
|
|
|
@ -60,6 +60,8 @@ internal enum L10n {
|
||||||
internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue")
|
internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue")
|
||||||
/// Discard
|
/// Discard
|
||||||
internal static let discard = L10n.tr("Localizable", "Common.Controls.Actions.Discard")
|
internal static let discard = L10n.tr("Localizable", "Common.Controls.Actions.Discard")
|
||||||
|
/// Done
|
||||||
|
internal static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done")
|
||||||
/// Edit
|
/// Edit
|
||||||
internal static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit")
|
internal static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit")
|
||||||
/// OK
|
/// OK
|
||||||
|
@ -85,6 +87,22 @@ internal enum L10n {
|
||||||
/// Try Again
|
/// Try Again
|
||||||
internal static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain")
|
internal static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain")
|
||||||
}
|
}
|
||||||
|
internal enum Firendship {
|
||||||
|
/// Block
|
||||||
|
internal static let block = L10n.tr("Localizable", "Common.Controls.Firendship.Block")
|
||||||
|
/// Blocked
|
||||||
|
internal static let blocked = L10n.tr("Localizable", "Common.Controls.Firendship.Blocked")
|
||||||
|
/// Edit info
|
||||||
|
internal static let editInfo = L10n.tr("Localizable", "Common.Controls.Firendship.EditInfo")
|
||||||
|
/// Follow
|
||||||
|
internal static let follow = L10n.tr("Localizable", "Common.Controls.Firendship.Follow")
|
||||||
|
/// Following
|
||||||
|
internal static let following = L10n.tr("Localizable", "Common.Controls.Firendship.Following")
|
||||||
|
/// Mute
|
||||||
|
internal static let mute = L10n.tr("Localizable", "Common.Controls.Firendship.Mute")
|
||||||
|
/// Muted
|
||||||
|
internal static let muted = L10n.tr("Localizable", "Common.Controls.Firendship.Muted")
|
||||||
|
}
|
||||||
internal enum Status {
|
internal enum Status {
|
||||||
/// Tap to reveal that may be sensitive
|
/// Tap to reveal that may be sensitive
|
||||||
internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning")
|
internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning")
|
||||||
|
@ -269,6 +287,24 @@ internal enum L10n {
|
||||||
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
|
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
internal enum Profile {
|
||||||
|
internal enum Dashboard {
|
||||||
|
/// followers
|
||||||
|
internal static let followers = L10n.tr("Localizable", "Scene.Profile.Dashboard.Followers")
|
||||||
|
/// following
|
||||||
|
internal static let following = L10n.tr("Localizable", "Scene.Profile.Dashboard.Following")
|
||||||
|
/// posts
|
||||||
|
internal static let posts = L10n.tr("Localizable", "Scene.Profile.Dashboard.Posts")
|
||||||
|
}
|
||||||
|
internal enum SegmentedControl {
|
||||||
|
/// Media
|
||||||
|
internal static let media = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Media")
|
||||||
|
/// Posts
|
||||||
|
internal static let posts = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Posts")
|
||||||
|
/// Replies
|
||||||
|
internal static let replies = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Replies")
|
||||||
|
}
|
||||||
|
}
|
||||||
internal enum PublicTimeline {
|
internal enum PublicTimeline {
|
||||||
/// Public
|
/// Public
|
||||||
internal static let title = L10n.tr("Localizable", "Scene.PublicTimeline.Title")
|
internal static let title = L10n.tr("Localizable", "Scene.PublicTimeline.Title")
|
||||||
|
@ -416,6 +452,8 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
internal enum ServerRules {
|
internal enum ServerRules {
|
||||||
|
/// privacy policy
|
||||||
|
internal static let privacyPolicy = L10n.tr("Localizable", "Scene.ServerRules.PrivacyPolicy")
|
||||||
/// By continuing, you're subject to the terms of service and privacy policy for %@.
|
/// By continuing, you're subject to the terms of service and privacy policy for %@.
|
||||||
internal static func prompt(_ p1: Any) -> String {
|
internal static func prompt(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "Scene.ServerRules.Prompt", String(describing: p1))
|
return L10n.tr("Localizable", "Scene.ServerRules.Prompt", String(describing: p1))
|
||||||
|
@ -424,6 +462,8 @@ internal enum L10n {
|
||||||
internal static func subtitle(_ p1: Any) -> String {
|
internal static func subtitle(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "Scene.ServerRules.Subtitle", String(describing: p1))
|
return L10n.tr("Localizable", "Scene.ServerRules.Subtitle", String(describing: p1))
|
||||||
}
|
}
|
||||||
|
/// terms of service
|
||||||
|
internal static let termsOfService = L10n.tr("Localizable", "Scene.ServerRules.TermsOfService")
|
||||||
/// Some ground rules.
|
/// Some ground rules.
|
||||||
internal static let title = L10n.tr("Localizable", "Scene.ServerRules.Title")
|
internal static let title = L10n.tr("Localizable", "Scene.ServerRules.Title")
|
||||||
internal enum Button {
|
internal enum Button {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// MastodonField.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
|
enum MastodonField {
|
||||||
|
|
||||||
|
static func parse(field string: String) -> ParseResult {
|
||||||
|
let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+))")
|
||||||
|
let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))")
|
||||||
|
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
||||||
|
|
||||||
|
var entities: [ActiveEntity] = []
|
||||||
|
|
||||||
|
for match in mentionMatches {
|
||||||
|
guard let text = string.substring(with: match, at: 0) else { continue }
|
||||||
|
let entity = ActiveEntity(range: match.range, type: .mention(text, userInfo: nil))
|
||||||
|
entities.append(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
for match in hashtagMatches {
|
||||||
|
guard let text = string.substring(with: match, at: 0) else { continue }
|
||||||
|
let entity = ActiveEntity(range: match.range, type: .hashtag(text, userInfo: nil))
|
||||||
|
entities.append(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
for match in urlMatches {
|
||||||
|
guard let text = string.substring(with: match, at: 0) else { continue }
|
||||||
|
let entity = ActiveEntity(range: match.range, type: .url(text, trimmed: text, url: text, userInfo: nil))
|
||||||
|
entities.append(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseResult(value: string, activeEntities: entities)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonField {
|
||||||
|
struct ParseResult {
|
||||||
|
let value: String
|
||||||
|
let activeEntities: [ActiveEntity]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// TootContent.swift
|
// MastodonStatusContent.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021/2/1.
|
// Created by MainasuK Cirno on 2021/2/1.
|
||||||
|
@ -9,15 +9,15 @@ import Foundation
|
||||||
import Kanna
|
import Kanna
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
|
||||||
enum TootContent {
|
enum MastodonStatusContent {
|
||||||
|
|
||||||
static func parse(toot: String) throws -> TootContent.ParseResult {
|
static func parse(status: String) throws -> MastodonStatusContent.ParseResult {
|
||||||
let toot = toot.replacingOccurrences(of: "<br/>", with: "\n")
|
let status = status.replacingOccurrences(of: "<br/>", with: "\n")
|
||||||
let rootNode = try Node.parse(document: toot)
|
let rootNode = try Node.parse(document: status)
|
||||||
let text = String(rootNode.text)
|
let text = String(rootNode.text)
|
||||||
|
|
||||||
var activeEntities: [ActiveEntity] = []
|
var activeEntities: [ActiveEntity] = []
|
||||||
let entities = TootContent.Node.entities(in: rootNode)
|
let entities = MastodonStatusContent.Node.entities(in: rootNode)
|
||||||
for entity in entities {
|
for entity in entities {
|
||||||
let range = NSRange(entity.text.startIndex..<entity.text.endIndex, in: text)
|
let range = NSRange(entity.text.startIndex..<entity.text.endIndex, in: text)
|
||||||
|
|
||||||
|
@ -48,22 +48,22 @@ enum TootContent {
|
||||||
var trimmed = text
|
var trimmed = text
|
||||||
for activeEntity in activeEntities {
|
for activeEntity in activeEntities {
|
||||||
guard case .url = activeEntity.type else { continue }
|
guard case .url = activeEntity.type else { continue }
|
||||||
TootContent.trimEntity(toot: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities)
|
MastodonStatusContent.trimEntity(status: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseResult(
|
return ParseResult(
|
||||||
document: toot,
|
document: status,
|
||||||
original: text,
|
original: text,
|
||||||
trimmed: trimmed,
|
trimmed: trimmed,
|
||||||
activeEntities: validate(text: trimmed, activeEntities: activeEntities) ? activeEntities : []
|
activeEntities: validate(text: trimmed, activeEntities: activeEntities) ? activeEntities : []
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func trimEntity(toot: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) {
|
static func trimEntity(status: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) {
|
||||||
guard case let .url(text, trimmed, _, _) = activeEntity.type else { return }
|
guard case let .url(text, trimmed, _, _) = activeEntity.type else { return }
|
||||||
guard let index = activeEntities.firstIndex(where: { $0.range == activeEntity.range }) else { return }
|
guard let index = activeEntities.firstIndex(where: { $0.range == activeEntity.range }) else { return }
|
||||||
guard let range = Range(activeEntity.range, in: toot) else { return }
|
guard let range = Range(activeEntity.range, in: status) else { return }
|
||||||
toot.replaceSubrange(range, with: trimmed)
|
status.replaceSubrange(range, with: trimmed)
|
||||||
|
|
||||||
let offset = trimmed.count - text.count
|
let offset = trimmed.count - text.count
|
||||||
activeEntity.range.length += offset
|
activeEntity.range.length += offset
|
||||||
|
@ -97,7 +97,7 @@ extension String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TootContent {
|
extension MastodonStatusContent {
|
||||||
struct ParseResult {
|
struct ParseResult {
|
||||||
let document: String
|
let document: String
|
||||||
let original: String
|
let original: String
|
||||||
|
@ -106,8 +106,7 @@ extension TootContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MastodonStatusContent {
|
||||||
extension TootContent {
|
|
||||||
|
|
||||||
class Node {
|
class Node {
|
||||||
|
|
||||||
|
@ -167,12 +166,12 @@ extension TootContent {
|
||||||
self.children = children
|
self.children = children
|
||||||
}
|
}
|
||||||
|
|
||||||
static func parse(document: String) throws -> TootContent.Node {
|
static func parse(document: String) throws -> MastodonStatusContent.Node {
|
||||||
let html = try HTML(html: document, encoding: .utf8)
|
let html = try HTML(html: document, encoding: .utf8)
|
||||||
let body = html.body ?? nil
|
let body = html.body ?? nil
|
||||||
let text = body?.text ?? ""
|
let text = body?.text ?? ""
|
||||||
let level = 0
|
let level = 0
|
||||||
let children: [TootContent.Node] = body.flatMap { body in
|
let children: [MastodonStatusContent.Node] = body.flatMap { body in
|
||||||
return Node.parse(element: body, parentText: text[...], parentLevel: level + 1)
|
return Node.parse(element: body, parentText: text[...], parentLevel: level + 1)
|
||||||
} ?? []
|
} ?? []
|
||||||
let node = Node(
|
let node = Node(
|
||||||
|
@ -253,32 +252,32 @@ extension TootContent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TootContent.Node {
|
extension MastodonStatusContent.Node {
|
||||||
enum `Type` {
|
enum `Type` {
|
||||||
case url
|
case url
|
||||||
case mention
|
case mention
|
||||||
case hashtag
|
case hashtag
|
||||||
}
|
}
|
||||||
|
|
||||||
static func entities(in node: TootContent.Node) -> [TootContent.Node] {
|
static func entities(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
|
||||||
return TootContent.Node.collect(node: node) { node in node.type != nil }
|
return MastodonStatusContent.Node.collect(node: node) { node in node.type != nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func hashtags(in node: TootContent.Node) -> [TootContent.Node] {
|
static func hashtags(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
|
||||||
return TootContent.Node.collect(node: node) { node in node.type == .hashtag }
|
return MastodonStatusContent.Node.collect(node: node) { node in node.type == .hashtag }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func mentions(in node: TootContent.Node) -> [TootContent.Node] {
|
static func mentions(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
|
||||||
return TootContent.Node.collect(node: node) { node in node.type == .mention }
|
return MastodonStatusContent.Node.collect(node: node) { node in node.type == .mention }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func urls(in node: TootContent.Node) -> [TootContent.Node] {
|
static func urls(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
|
||||||
return TootContent.Node.collect(node: node) { node in node.type == .url }
|
return MastodonStatusContent.Node.collect(node: node) { node in node.type == .url }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TootContent.Node: CustomDebugStringConvertible {
|
extension MastodonStatusContent.Node: CustomDebugStringConvertible {
|
||||||
var debugDescription: String {
|
var debugDescription: String {
|
||||||
let linkInfo: String = {
|
let linkInfo: String = {
|
||||||
switch (href, hrefEllipsis) {
|
switch (href, hrefEllipsis) {
|
|
@ -73,7 +73,5 @@
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -13,6 +13,19 @@ import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
|
||||||
|
// MARK: - StatusViewDelegate
|
||||||
|
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, headerInfoLabelDidPressed label: UILabel) {
|
||||||
|
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .secondary, provider: self, cell: cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarButtonDidPressed button: UIButton) {
|
||||||
|
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, cell: cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - ActionToolbarContainerDelegate
|
// MARK: - ActionToolbarContainerDelegate
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
|
||||||
|
@ -31,7 +44,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(_, let attribute):
|
case .homeTimelineIndex(_, let attribute):
|
||||||
attribute.isStatusTextSensitive = false
|
attribute.isStatusTextSensitive = false
|
||||||
case .toot(_, let attribute):
|
case .status(_, let attribute):
|
||||||
attribute.isStatusTextSensitive = false
|
attribute.isStatusTextSensitive = false
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
|
@ -66,7 +79,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(_, let attribute):
|
case .homeTimelineIndex(_, let attribute):
|
||||||
attribute.isStatusSensitive = false
|
attribute.isStatusSensitive = false
|
||||||
case .toot(_, let attribute):
|
case .status(_, let attribute):
|
||||||
attribute.isStatusSensitive = false
|
attribute.isStatusSensitive = false
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
|
@ -89,16 +102,16 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||||
toot(for: cell, indexPath: nil)
|
status(for: cell, indexPath: nil)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.compactMap { toot -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error>? in
|
.compactMap { status -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error>? in
|
||||||
guard let toot = (toot?.reblog ?? toot) else { return nil }
|
guard let status = (status?.reblog ?? status) else { return nil }
|
||||||
guard let poll = toot.poll else { return nil }
|
guard let poll = status.poll else { return nil }
|
||||||
|
|
||||||
let votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
|
let votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
|
||||||
let choices = votedOptions.map { $0.index.intValue }
|
let choices = votedOptions.map { $0.index.intValue }
|
||||||
let domain = poll.toot.domain
|
let domain = poll.status.domain
|
||||||
|
|
||||||
button.isEnabled = false
|
button.isEnabled = false
|
||||||
|
|
||||||
|
@ -137,7 +150,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
|
||||||
let poll = option.poll
|
let poll = option.poll
|
||||||
let pollObjectID = option.poll.objectID
|
let pollObjectID = option.poll.objectID
|
||||||
let domain = poll.toot.domain
|
let domain = poll.status.domain
|
||||||
|
|
||||||
if poll.multiple {
|
if poll.multiple {
|
||||||
var votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
|
var votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
|
||||||
|
|
|
@ -11,7 +11,7 @@ import CoreDataStack
|
||||||
|
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
func handleTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func handleTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
// prefetch reply toot
|
// prefetch reply status
|
||||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||||
let domain = activeMastodonAuthenticationBox.domain
|
let domain = activeMastodonAuthenticationBox.domain
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(let objectID, _):
|
case .homeTimelineIndex(let objectID, _):
|
||||||
let homeTimelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let homeTimelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
statusObjectIDs.append(homeTimelineIndex.toot.objectID)
|
statusObjectIDs.append(homeTimelineIndex.status.objectID)
|
||||||
case .toot(let objectID, _):
|
case .status(let objectID, _):
|
||||||
statusObjectIDs.append(objectID)
|
statusObjectIDs.append(objectID)
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
|
@ -32,15 +32,15 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
backgroundManagedObjectContext.perform { [weak self] in
|
backgroundManagedObjectContext.perform { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
for objectID in statusObjectIDs {
|
for objectID in statusObjectIDs {
|
||||||
let toot = backgroundManagedObjectContext.object(with: objectID) as! Toot
|
let status = backgroundManagedObjectContext.object(with: objectID) as! Status
|
||||||
guard let replyToID = toot.inReplyToID, toot.replyTo == nil else {
|
guard let replyToID = status.inReplyToID, status.replyTo == nil else {
|
||||||
// skip
|
// skip
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
self.context.statusPrefetchingService.prefetchReplyTo(
|
self.context.statusPrefetchingService.prefetchReplyTo(
|
||||||
domain: domain,
|
domain: domain,
|
||||||
statusObjectID: toot.objectID,
|
statusObjectID: status.objectID,
|
||||||
statusID: toot.id,
|
statusID: status.id,
|
||||||
replyToStatusID: replyToID,
|
replyToStatusID: replyToID,
|
||||||
authorizationBox: activeMastodonAuthenticationBox
|
authorizationBox: activeMastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,15 +17,15 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
// update poll when toot appear
|
// update poll when status appear
|
||||||
let now = Date()
|
let now = Date()
|
||||||
var pollID: Mastodon.Entity.Poll.ID?
|
var pollID: Mastodon.Entity.Poll.ID?
|
||||||
toot(for: cell, indexPath: indexPath)
|
status(for: cell, indexPath: indexPath)
|
||||||
.compactMap { [weak self] toot -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error>? in
|
.compactMap { [weak self] status -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error>? in
|
||||||
guard let self = self else { return nil }
|
guard let self = self else { return nil }
|
||||||
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
|
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
|
||||||
guard let toot = (toot?.reblog ?? toot) else { return nil }
|
guard let status = (status?.reblog ?? status) else { return nil }
|
||||||
guard let poll = toot.poll else { return nil }
|
guard let poll = status.poll else { return nil }
|
||||||
pollID = poll.id
|
pollID = poll.id
|
||||||
|
|
||||||
// not expired AND last update > 60s
|
// not expired AND last update > 60s
|
||||||
|
@ -46,7 +46,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info update…", (#file as NSString).lastPathComponent, #line, #function, poll.id)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info update…", (#file as NSString).lastPathComponent, #line, #function, poll.id)
|
||||||
|
|
||||||
return self.context.apiService.poll(
|
return self.context.apiService.poll(
|
||||||
domain: toot.domain,
|
domain: status.domain,
|
||||||
pollID: poll.id,
|
pollID: poll.id,
|
||||||
pollObjectID: poll.objectID,
|
pollObjectID: poll.objectID,
|
||||||
mastodonAuthenticationBox: authenticationBox
|
mastodonAuthenticationBox: authenticationBox
|
||||||
|
@ -68,11 +68,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
toot(for: cell, indexPath: indexPath)
|
status(for: cell, indexPath: indexPath)
|
||||||
.sink { [weak self] toot in
|
.sink { [weak self] status in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let toot = toot?.reblog ?? toot
|
let status = status?.reblog ?? status
|
||||||
guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
|
guard let media = (status?.mediaAttachments ?? Set()).first else { return }
|
||||||
guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return }
|
guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
@ -85,17 +85,17 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
func handleTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func handleTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
// os_log("%{public}s[%{public}ld], %{public}s: indexPath %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
// os_log("%{public}s[%{public}ld], %{public}s: indexPath %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
||||||
|
|
||||||
toot(for: cell, indexPath: indexPath)
|
status(for: cell, indexPath: indexPath)
|
||||||
.sink { [weak self] toot in
|
.sink { [weak self] status in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
|
guard let media = (status?.mediaAttachments ?? Set()).first else { return }
|
||||||
|
|
||||||
if let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) {
|
if let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
videoPlayerViewModel.didEndDisplaying()
|
videoPlayerViewModel.didEndDisplaying()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let currentAudioAttachment = self.context.audioPlaybackService.attachment, let _ = toot?.mediaAttachments?.contains(currentAudioAttachment) {
|
if let currentAudioAttachment = self.context.audioPlaybackService.attachment, let _ = status?.mediaAttachments?.contains(currentAudioAttachment) {
|
||||||
self.context.audioPlaybackService.pause()
|
self.context.audioPlaybackService.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,9 @@ import CoreDataStack
|
||||||
|
|
||||||
protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController {
|
protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController {
|
||||||
// async
|
// async
|
||||||
func toot() -> Future<Toot?, Never>
|
func status() -> Future<Status?, Never>
|
||||||
func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Toot?, Never>
|
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never>
|
||||||
func toot(for cell: UICollectionViewCell) -> Future<Toot?, Never>
|
func status(for cell: UICollectionViewCell) -> Future<Status?, Never>
|
||||||
|
|
||||||
// sync
|
// sync
|
||||||
var managedObjectContext: NSManagedObjectContext { get }
|
var managedObjectContext: NSManagedObjectContext { get }
|
||||||
|
|
|
@ -13,8 +13,53 @@ import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
|
||||||
enum StatusProviderFacade {
|
enum StatusProviderFacade { }
|
||||||
|
|
||||||
|
extension StatusProviderFacade {
|
||||||
|
|
||||||
|
static func coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider) {
|
||||||
|
_coordinateToStatusAuthorProfileScene(
|
||||||
|
for: target,
|
||||||
|
provider: provider,
|
||||||
|
status: provider.status()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell) {
|
||||||
|
_coordinateToStatusAuthorProfileScene(
|
||||||
|
for: target,
|
||||||
|
provider: provider,
|
||||||
|
status: provider.status(for: cell, indexPath: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func _coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider, status: Future<Status?, Never>) {
|
||||||
|
status
|
||||||
|
.sink { [weak provider] status in
|
||||||
|
guard let provider = provider else { return }
|
||||||
|
let _status: Status? = {
|
||||||
|
switch target {
|
||||||
|
case .primary: return status?.reblog ?? status // original status
|
||||||
|
case .secondary: return status?.replyTo ?? status // reblog or reply to status
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
guard let status = _status else { return }
|
||||||
|
|
||||||
|
let mastodonUser = status.author
|
||||||
|
let profileViewModel = CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if provider.navigationController == nil {
|
||||||
|
let from = provider.presentingViewController ?? provider
|
||||||
|
provider.dismiss(animated: true) {
|
||||||
|
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &provider.disposeBag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusProviderFacade {
|
extension StatusProviderFacade {
|
||||||
|
@ -22,18 +67,18 @@ extension StatusProviderFacade {
|
||||||
static func responseToStatusLikeAction(provider: StatusProvider) {
|
static func responseToStatusLikeAction(provider: StatusProvider) {
|
||||||
_responseToStatusLikeAction(
|
_responseToStatusLikeAction(
|
||||||
provider: provider,
|
provider: provider,
|
||||||
toot: provider.toot()
|
status: provider.status()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func responseToStatusLikeAction(provider: StatusProvider, cell: UITableViewCell) {
|
static func responseToStatusLikeAction(provider: StatusProvider, cell: UITableViewCell) {
|
||||||
_responseToStatusLikeAction(
|
_responseToStatusLikeAction(
|
||||||
provider: provider,
|
provider: provider,
|
||||||
toot: provider.toot(for: cell, indexPath: nil)
|
status: provider.status(for: cell, indexPath: nil)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func _responseToStatusLikeAction(provider: StatusProvider, toot: Future<Toot?, Never>) {
|
private static func _responseToStatusLikeAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||||
// prepare authentication
|
// prepare authentication
|
||||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
@ -55,22 +100,22 @@ extension StatusProviderFacade {
|
||||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||||
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
|
||||||
toot
|
status
|
||||||
.compactMap { toot -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in
|
.compactMap { status -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in
|
||||||
guard let toot = toot?.reblog ?? toot else { return nil }
|
guard let status = status?.reblog ?? status else { return nil }
|
||||||
let favoriteKind: Mastodon.API.Favorites.FavoriteKind = {
|
let favoriteKind: Mastodon.API.Favorites.FavoriteKind = {
|
||||||
let isLiked = toot.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
|
let isLiked = status.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
|
||||||
return isLiked ? .destroy : .create
|
return isLiked ? .destroy : .create
|
||||||
}()
|
}()
|
||||||
return (toot.objectID, favoriteKind)
|
return (status.objectID, favoriteKind)
|
||||||
}
|
}
|
||||||
.map { tootObjectID, favoriteKind -> AnyPublisher<(Toot.ID, Mastodon.API.Favorites.FavoriteKind), Error> in
|
.map { statusObjectID, favoriteKind -> AnyPublisher<(Status.ID, Mastodon.API.Favorites.FavoriteKind), Error> in
|
||||||
return context.apiService.like(
|
return context.apiService.like(
|
||||||
tootObjectID: tootObjectID,
|
statusObjectID: statusObjectID,
|
||||||
mastodonUserObjectID: mastodonUserObjectID,
|
mastodonUserObjectID: mastodonUserObjectID,
|
||||||
favoriteKind: favoriteKind
|
favoriteKind: favoriteKind
|
||||||
)
|
)
|
||||||
.map { tootID in (tootID, favoriteKind) }
|
.map { statusID in (statusID, favoriteKind) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
|
@ -82,7 +127,7 @@ extension StatusProviderFacade {
|
||||||
responseFeedbackGenerator.prepare()
|
responseFeedbackGenerator.prepare()
|
||||||
} receiveOutput: { _, favoriteKind in
|
} receiveOutput: { _, favoriteKind in
|
||||||
generator.impactOccurred()
|
generator.impactOccurred()
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: [Like] update local toot like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike")
|
os_log("%{public}s[%{public}ld], %{public}s: [Like] update local status like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike")
|
||||||
} receiveCompletion: { completion in
|
} receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure:
|
case .failure:
|
||||||
|
@ -92,9 +137,9 @@ extension StatusProviderFacade {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { tootID, favoriteKind in
|
.map { statusID, favoriteKind in
|
||||||
return context.apiService.like(
|
return context.apiService.like(
|
||||||
statusID: tootID,
|
statusID: statusID,
|
||||||
favoriteKind: favoriteKind,
|
favoriteKind: favoriteKind,
|
||||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
|
@ -126,18 +171,18 @@ extension StatusProviderFacade {
|
||||||
static func responseToStatusReblogAction(provider: StatusProvider) {
|
static func responseToStatusReblogAction(provider: StatusProvider) {
|
||||||
_responseToStatusReblogAction(
|
_responseToStatusReblogAction(
|
||||||
provider: provider,
|
provider: provider,
|
||||||
toot: provider.toot()
|
status: provider.status()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func responseToStatusReblogAction(provider: StatusProvider, cell: UITableViewCell) {
|
static func responseToStatusReblogAction(provider: StatusProvider, cell: UITableViewCell) {
|
||||||
_responseToStatusReblogAction(
|
_responseToStatusReblogAction(
|
||||||
provider: provider,
|
provider: provider,
|
||||||
toot: provider.toot(for: cell, indexPath: nil)
|
status: provider.status(for: cell, indexPath: nil)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func _responseToStatusReblogAction(provider: StatusProvider, toot: Future<Toot?, Never>) {
|
private static func _responseToStatusReblogAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||||
// prepare authentication
|
// prepare authentication
|
||||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
@ -159,22 +204,22 @@ extension StatusProviderFacade {
|
||||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||||
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
|
||||||
toot
|
status
|
||||||
.compactMap { toot -> (NSManagedObjectID, Mastodon.API.Reblog.ReblogKind)? in
|
.compactMap { status -> (NSManagedObjectID, Mastodon.API.Reblog.ReblogKind)? in
|
||||||
guard let toot = toot?.reblog ?? toot else { return nil }
|
guard let status = status?.reblog ?? status else { return nil }
|
||||||
let reblogKind: Mastodon.API.Reblog.ReblogKind = {
|
let reblogKind: Mastodon.API.Reblog.ReblogKind = {
|
||||||
let isReblogged = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
|
let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
|
||||||
return isReblogged ? .undoReblog : .reblog(query: .init(visibility: nil))
|
return isReblogged ? .undoReblog : .reblog(query: .init(visibility: nil))
|
||||||
}()
|
}()
|
||||||
return (toot.objectID, reblogKind)
|
return (status.objectID, reblogKind)
|
||||||
}
|
}
|
||||||
.map { tootObjectID, reblogKind -> AnyPublisher<(Toot.ID, Mastodon.API.Reblog.ReblogKind), Error> in
|
.map { statusObjectID, reblogKind -> AnyPublisher<(Status.ID, Mastodon.API.Reblog.ReblogKind), Error> in
|
||||||
return context.apiService.reblog(
|
return context.apiService.reblog(
|
||||||
tootObjectID: tootObjectID,
|
statusObjectID: statusObjectID,
|
||||||
mastodonUserObjectID: mastodonUserObjectID,
|
mastodonUserObjectID: mastodonUserObjectID,
|
||||||
reblogKind: reblogKind
|
reblogKind: reblogKind
|
||||||
)
|
)
|
||||||
.map { tootID in (tootID, reblogKind) }
|
.map { statusID in (statusID, reblogKind) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
|
@ -188,9 +233,9 @@ extension StatusProviderFacade {
|
||||||
generator.impactOccurred()
|
generator.impactOccurred()
|
||||||
switch reblogKind {
|
switch reblogKind {
|
||||||
case .reblog:
|
case .reblog:
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "reblog")
|
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local status reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "reblog")
|
||||||
case .undoReblog:
|
case .undoReblog:
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unreblog")
|
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local status reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unreblog")
|
||||||
}
|
}
|
||||||
} receiveCompletion: { completion in
|
} receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
|
@ -201,9 +246,9 @@ extension StatusProviderFacade {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { tootID, reblogKind in
|
.map { statusID, reblogKind in
|
||||||
return context.apiService.reblog(
|
return context.apiService.reblog(
|
||||||
statusID: tootID,
|
statusID: statusID,
|
||||||
reblogKind: reblogKind,
|
reblogKind: reblogKind,
|
||||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
|
@ -231,8 +276,8 @@ extension StatusProviderFacade {
|
||||||
|
|
||||||
extension StatusProviderFacade {
|
extension StatusProviderFacade {
|
||||||
enum Target {
|
enum Target {
|
||||||
case toot
|
case primary // original
|
||||||
case reblog
|
case secondary // attachment reblog or reply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.016",
|
||||||
|
"green" : "0.561",
|
||||||
|
"red" : "0.792"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,2 @@
|
||||||
"NSCameraUsageDescription" = "Used to take photo for toot";
|
"NSCameraUsageDescription" = "Used to take photo for post status";
|
||||||
"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library";
|
"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library";
|
|
@ -15,6 +15,7 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.Confirm" = "Confirm";
|
"Common.Controls.Actions.Confirm" = "Confirm";
|
||||||
"Common.Controls.Actions.Continue" = "Continue";
|
"Common.Controls.Actions.Continue" = "Continue";
|
||||||
"Common.Controls.Actions.Discard" = "Discard";
|
"Common.Controls.Actions.Discard" = "Discard";
|
||||||
|
"Common.Controls.Actions.Done" = "Done";
|
||||||
"Common.Controls.Actions.Edit" = "Edit";
|
"Common.Controls.Actions.Edit" = "Edit";
|
||||||
"Common.Controls.Actions.Ok" = "OK";
|
"Common.Controls.Actions.Ok" = "OK";
|
||||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||||
|
@ -27,6 +28,13 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.SignUp" = "Sign Up";
|
"Common.Controls.Actions.SignUp" = "Sign Up";
|
||||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||||
"Common.Controls.Actions.TryAgain" = "Try Again";
|
"Common.Controls.Actions.TryAgain" = "Try Again";
|
||||||
|
"Common.Controls.Firendship.Block" = "Block";
|
||||||
|
"Common.Controls.Firendship.Blocked" = "Blocked";
|
||||||
|
"Common.Controls.Firendship.EditInfo" = "Edit info";
|
||||||
|
"Common.Controls.Firendship.Follow" = "Follow";
|
||||||
|
"Common.Controls.Firendship.Following" = "Following";
|
||||||
|
"Common.Controls.Firendship.Mute" = "Mute";
|
||||||
|
"Common.Controls.Firendship.Muted" = "Muted";
|
||||||
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
||||||
"Common.Controls.Status.Poll.Closed" = "Closed";
|
"Common.Controls.Status.Poll.Closed" = "Closed";
|
||||||
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
|
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
|
||||||
|
@ -86,6 +94,12 @@ tap the link to confirm your account.";
|
||||||
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
|
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
|
||||||
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
|
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
|
||||||
"Scene.HomeTimeline.Title" = "Home";
|
"Scene.HomeTimeline.Title" = "Home";
|
||||||
|
"Scene.Profile.Dashboard.Followers" = "followers";
|
||||||
|
"Scene.Profile.Dashboard.Following" = "following";
|
||||||
|
"Scene.Profile.Dashboard.Posts" = "posts";
|
||||||
|
"Scene.Profile.SegmentedControl.Media" = "Media";
|
||||||
|
"Scene.Profile.SegmentedControl.Posts" = "Posts";
|
||||||
|
"Scene.Profile.SegmentedControl.Replies" = "Replies";
|
||||||
"Scene.PublicTimeline.Title" = "Public";
|
"Scene.PublicTimeline.Title" = "Public";
|
||||||
"Scene.Register.Error.Item.Agreement" = "Agreement";
|
"Scene.Register.Error.Item.Agreement" = "Agreement";
|
||||||
"Scene.Register.Error.Item.Email" = "Email";
|
"Scene.Register.Error.Item.Email" = "Email";
|
||||||
|
@ -130,8 +144,10 @@ tap the link to confirm your account.";
|
||||||
"Scene.ServerPicker.Title" = "Pick a Server,
|
"Scene.ServerPicker.Title" = "Pick a Server,
|
||||||
any server.";
|
any server.";
|
||||||
"Scene.ServerRules.Button.Confirm" = "I Agree";
|
"Scene.ServerRules.Button.Confirm" = "I Agree";
|
||||||
|
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
|
||||||
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
|
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
|
||||||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||||
|
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||||
"Scene.Welcome.Slogan" = "Social networking
|
"Scene.Welcome.Slogan" = "Social networking
|
||||||
back in your hands.";
|
back in your hands.";
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// ComposeRepliedToTootContentCollectionViewCell.swift
|
// ComposeRepliedToStatusContentCollectionViewCell.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021-3-11.
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class ComposeRepliedToTootContentCollectionViewCell: UICollectionViewCell {
|
final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
@ -21,7 +21,7 @@ final class ComposeRepliedToTootContentCollectionViewCell: UICollectionViewCell
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeRepliedToTootContentCollectionViewCell {
|
extension ComposeRepliedToStatusContentCollectionViewCell {
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
|
|
|
@ -43,7 +43,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
let collectionView: UICollectionView = {
|
let collectionView: UICollectionView = {
|
||||||
let collectionViewLayout = ComposeViewController.createLayout()
|
let collectionViewLayout = ComposeViewController.createLayout()
|
||||||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
collectionView.register(ComposeRepliedToTootContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self))
|
collectionView.register(ComposeRepliedToStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self))
|
||||||
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
|
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
|
||||||
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
||||||
collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
|
collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
|
||||||
|
|
|
@ -65,7 +65,7 @@ extension HomeTimelineViewController {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.moveToFirstVideoStatus(action)
|
self.moveToFirstVideoStatus(action)
|
||||||
}),
|
}),
|
||||||
UIAction(title: "First GIF Toot", image: nil, attributes: [], handler: { [weak self] action in
|
UIAction(title: "First GIF status", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.moveToFirstGIFStatus(action)
|
self.moveToFirstGIFStatus(action)
|
||||||
}),
|
}),
|
||||||
|
@ -112,7 +112,7 @@ extension HomeTimelineViewController {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(let objectID, _):
|
case .homeTimelineIndex(let objectID, _):
|
||||||
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
return homeTimelineIndex.toot.reblog != nil
|
return homeTimelineIndex.status.reblog != nil
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ extension HomeTimelineViewController {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(let objectID, _):
|
case .homeTimelineIndex(let objectID, _):
|
||||||
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
let post = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
|
let post = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status
|
||||||
return post.poll != nil
|
return post.poll != nil
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -153,7 +153,7 @@ extension HomeTimelineViewController {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(let objectID, _):
|
case .homeTimelineIndex(let objectID, _):
|
||||||
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
guard homeTimelineIndex.toot.inReplyToID != nil else {
|
guard homeTimelineIndex.status.inReplyToID != nil else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -176,8 +176,8 @@ extension HomeTimelineViewController {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(let objectID, _):
|
case .homeTimelineIndex(let objectID, _):
|
||||||
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
|
let status = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status
|
||||||
return toot.mediaAttachments?.contains(where: { $0.type == .audio }) ?? false
|
return status.mediaAttachments?.contains(where: { $0.type == .audio }) ?? false
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ extension HomeTimelineViewController {
|
||||||
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
||||||
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
||||||
} else {
|
} else {
|
||||||
print("Not found audio toot")
|
print("Not found audio status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,8 +197,8 @@ extension HomeTimelineViewController {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(let objectID, _):
|
case .homeTimelineIndex(let objectID, _):
|
||||||
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
|
let status = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status
|
||||||
return toot.mediaAttachments?.contains(where: { $0.type == .video }) ?? false
|
return status.mediaAttachments?.contains(where: { $0.type == .video }) ?? false
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -218,8 +218,8 @@ extension HomeTimelineViewController {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(let objectID, _):
|
case .homeTimelineIndex(let objectID, _):
|
||||||
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
|
let status = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status
|
||||||
return toot.mediaAttachments?.contains(where: { $0.type == .gifv }) ?? false
|
return status.mediaAttachments?.contains(where: { $0.type == .gifv }) ?? false
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -242,12 +242,12 @@ extension HomeTimelineViewController {
|
||||||
default: return nil
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var droppingTootObjectIDs: [NSManagedObjectID] = []
|
var droppingStatusObjectIDs: [NSManagedObjectID] = []
|
||||||
context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
|
context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
for objectID in droppingObjectIDs {
|
for objectID in droppingObjectIDs {
|
||||||
guard let homeTimelineIndex = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { continue }
|
guard let homeTimelineIndex = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { continue }
|
||||||
droppingTootObjectIDs.append(homeTimelineIndex.toot.objectID)
|
droppingStatusObjectIDs.append(homeTimelineIndex.status.objectID)
|
||||||
self.context.apiService.backgroundManagedObjectContext.delete(homeTimelineIndex)
|
self.context.apiService.backgroundManagedObjectContext.delete(homeTimelineIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -257,8 +257,8 @@ extension HomeTimelineViewController {
|
||||||
case .success:
|
case .success:
|
||||||
self.context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
|
self.context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
for objectID in droppingTootObjectIDs {
|
for objectID in droppingStatusObjectIDs {
|
||||||
guard let post = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Toot else { continue }
|
guard let post = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Status else { continue }
|
||||||
self.context.apiService.backgroundManagedObjectContext.delete(post)
|
self.context.apiService.backgroundManagedObjectContext.delete(post)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,11 @@ import CoreDataStack
|
||||||
// MARK: - StatusProvider
|
// MARK: - StatusProvider
|
||||||
extension HomeTimelineViewController: StatusProvider {
|
extension HomeTimelineViewController: StatusProvider {
|
||||||
|
|
||||||
func toot() -> Future<Toot?, Never> {
|
func status() -> Future<Status?, Never> {
|
||||||
return Future { promise in promise(.success(nil)) }
|
return Future { promise in promise(.success(nil)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Toot?, Never> {
|
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||||
return Future { promise in
|
return Future { promise in
|
||||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
@ -36,7 +36,7 @@ extension HomeTimelineViewController: StatusProvider {
|
||||||
let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext
|
let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext
|
||||||
managedObjectContext.perform {
|
managedObjectContext.perform {
|
||||||
let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex
|
let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex
|
||||||
promise(.success(timelineIndex?.toot))
|
promise(.success(timelineIndex?.status))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
promise(.success(nil))
|
promise(.success(nil))
|
||||||
|
@ -44,7 +44,7 @@ extension HomeTimelineViewController: StatusProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toot(for cell: UICollectionViewCell) -> Future<Toot?, Never> {
|
func status(for cell: UICollectionViewCell) -> Future<Status?, Never> {
|
||||||
return Future { promise in promise(.success(nil)) }
|
return Future { promise in promise(.success(nil)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -176,6 +176,13 @@ extension HomeTimelineViewController {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
// needs trigger manually after onboarding dismiss
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
@ -313,7 +320,7 @@ extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControl
|
||||||
|
|
||||||
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
||||||
extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
||||||
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String?, timelineIndexobjectID: NSManagedObjectID?) {
|
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineStatusID: String?, timelineIndexobjectID: NSManagedObjectID?) {
|
||||||
guard let upperTimelineIndexObjectID = timelineIndexobjectID else {
|
guard let upperTimelineIndexObjectID = timelineIndexobjectID else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,10 @@ extension HomeTimelineViewModel {
|
||||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
||||||
|
// snapshot.appendSections([.main])
|
||||||
|
// diffableDataSource?.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -83,12 +87,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
var newTimelineItems: [Item] = []
|
var newTimelineItems: [Item] = []
|
||||||
|
|
||||||
for (i, timelineIndex) in timelineIndexes.enumerated() {
|
for (i, timelineIndex) in timelineIndexes.enumerated() {
|
||||||
let toot = timelineIndex.toot.reblog ?? timelineIndex.toot
|
let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute()
|
||||||
let isStatusTextSensitive: Bool = {
|
|
||||||
guard let spoilerText = toot.spoilerText, !spoilerText.isEmpty else { return false }
|
|
||||||
return true
|
|
||||||
}()
|
|
||||||
let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: toot.sensitive)
|
|
||||||
|
|
||||||
// append new item into snapshot
|
// append new item into snapshot
|
||||||
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
|
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
|
||||||
|
|
|
@ -55,7 +55,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||||
|
|
||||||
managedObjectContext.perform {
|
managedObjectContext.perform {
|
||||||
let start = CACurrentMediaTime()
|
let start = CACurrentMediaTime()
|
||||||
let latestTootIDs: [Toot.ID]
|
let latestStatusIDs: [Status.ID]
|
||||||
let request = HomeTimelineIndex.sortedFetchRequest
|
let request = HomeTimelineIndex.sortedFetchRequest
|
||||||
request.returnsObjectsAsFaults = false
|
request.returnsObjectsAsFaults = false
|
||||||
request.predicate = predicate
|
request.predicate = predicate
|
||||||
|
@ -64,10 +64,10 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||||
let timelineIndexes = try managedObjectContext.fetch(request)
|
let timelineIndexes = try managedObjectContext.fetch(request)
|
||||||
let endFetch = CACurrentMediaTime()
|
let endFetch = CACurrentMediaTime()
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect timelineIndexes cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, endFetch - start)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect timelineIndexes cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, endFetch - start)
|
||||||
latestTootIDs = timelineIndexes
|
latestStatusIDs = timelineIndexes
|
||||||
.prefix(APIService.onceRequestTootMaxCount) // avoid performance issue
|
.prefix(APIService.onceRequestStatusMaxCount) // avoid performance issue
|
||||||
.compactMap { timelineIndex in
|
.compactMap { timelineIndex in
|
||||||
timelineIndex.value(forKeyPath: #keyPath(HomeTimelineIndex.toot.id)) as? Toot.ID
|
timelineIndex.value(forKeyPath: #keyPath(HomeTimelineIndex.status.id)) as? Status.ID
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
|
@ -75,7 +75,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||||
}
|
}
|
||||||
|
|
||||||
let end = CACurrentMediaTime()
|
let end = CACurrentMediaTime()
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect toots id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect statuses id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
|
||||||
|
|
||||||
// TODO: only set large count when using Wi-Fi
|
// TODO: only set large count when using Wi-Fi
|
||||||
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
|
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
|
||||||
|
@ -86,7 +86,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
viewModel.isFetchingLatestTimeline.value = false
|
viewModel.isFetchingLatestTimeline.value = false
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
case .finished:
|
case .finished:
|
||||||
// handle isFetchingLatestTimeline in fetch controller delegate
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
break
|
break
|
||||||
|
@ -95,15 +95,15 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||||
stateMachine.enter(Idle.self)
|
stateMachine.enter(Idle.self)
|
||||||
|
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
// stop refresher if no new toots
|
// stop refresher if no new statuses
|
||||||
let toots = response.value
|
let statuses = response.value
|
||||||
let newToots = toots.filter { !latestTootIDs.contains($0.id) }
|
let newStatuses = statuses.filter { !latestStatusIDs.contains($0.id) }
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new toots", ((#file as NSString).lastPathComponent), #line, #function, newToots.count)
|
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new statuses", ((#file as NSString).lastPathComponent), #line, #function, newStatuses.count)
|
||||||
|
|
||||||
if newToots.isEmpty {
|
if newStatuses.isEmpty {
|
||||||
viewModel.isFetchingLatestTimeline.value = false
|
viewModel.isFetchingLatestTimeline.value = false
|
||||||
} else {
|
} else {
|
||||||
if !latestTootIDs.isEmpty {
|
if !latestStatusIDs.isEmpty {
|
||||||
viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming()
|
viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,12 +58,12 @@ extension HomeTimelineViewModel.LoadMiddleState {
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let tootIDs = (viewModel.fetchedResultsController.fetchedObjects ?? []).compactMap { timelineIndex in
|
let statusIDs = (viewModel.fetchedResultsController.fetchedObjects ?? []).compactMap { timelineIndex in
|
||||||
timelineIndex.toot.id
|
timelineIndex.status.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: only set large count when using Wi-Fi
|
// TODO: only set large count when using Wi-Fi
|
||||||
let maxID = timelineIndex.toot.id
|
let maxID = timelineIndex.status.id
|
||||||
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain,maxID: maxID, authorizationBox: activeMastodonAuthenticationBox)
|
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain,maxID: maxID, authorizationBox: activeMastodonAuthenticationBox)
|
||||||
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -72,16 +72,16 @@ extension HomeTimelineViewModel.LoadMiddleState {
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
os_log("%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
let toots = response.value
|
let statuses = response.value
|
||||||
let newToots = toots.filter { !tootIDs.contains($0.id) }
|
let newStatuses = statuses.filter { !statusIDs.contains($0.id) }
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld toots, %{public}%ld new toots", ((#file as NSString).lastPathComponent), #line, #function, toots.count, newToots.count)
|
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld statuses, %{public}%ld new statuses", ((#file as NSString).lastPathComponent), #line, #function, statuses.count, newStatuses.count)
|
||||||
if newToots.isEmpty {
|
if newStatuses.isEmpty {
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
} else {
|
} else {
|
||||||
stateMachine.enter(Success.self)
|
stateMachine.enter(Success.self)
|
||||||
|
|
|
@ -53,7 +53,7 @@ extension HomeTimelineViewModel.LoadOldestState {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: only set large count when using Wi-Fi
|
// TODO: only set large count when using Wi-Fi
|
||||||
let maxID = last.toot.id
|
let maxID = last.status.id
|
||||||
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, maxID: maxID, authorizationBox: activeMastodonAuthenticationBox)
|
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, maxID: maxID, authorizationBox: activeMastodonAuthenticationBox)
|
||||||
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -61,15 +61,15 @@ extension HomeTimelineViewModel.LoadOldestState {
|
||||||
viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion)
|
viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion)
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
os_log("%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
case .finished:
|
case .finished:
|
||||||
// handle isFetchingLatestTimeline in fetch controller delegate
|
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
let toots = response.value
|
let statuses = response.value
|
||||||
// enter no more state when no new toots
|
// enter no more state when no new statuses
|
||||||
if toots.isEmpty || (toots.count == 1 && toots[0].id == maxID) {
|
if statuses.isEmpty || (statuses.count == 1 && statuses[0].id == maxID) {
|
||||||
stateMachine.enter(NoMore.self)
|
stateMachine.enter(NoMore.self)
|
||||||
} else {
|
} else {
|
||||||
stateMachine.enter(Idle.self)
|
stateMachine.enter(Idle.self)
|
||||||
|
|
|
@ -74,7 +74,7 @@ final class HomeTimelineViewModel: NSObject {
|
||||||
let fetchRequest = HomeTimelineIndex.sortedFetchRequest
|
let fetchRequest = HomeTimelineIndex.sortedFetchRequest
|
||||||
fetchRequest.fetchBatchSize = 20
|
fetchRequest.fetchBatchSize = 20
|
||||||
fetchRequest.returnsObjectsAsFaults = false
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(HomeTimelineIndex.toot)]
|
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(HomeTimelineIndex.status)]
|
||||||
let controller = NSFetchedResultsController(
|
let controller = NSFetchedResultsController(
|
||||||
fetchRequest: fetchRequest,
|
fetchRequest: fetchRequest,
|
||||||
managedObjectContext: context.managedObjectContext,
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
|
|
@ -63,10 +63,11 @@ class MainTabBarController: UITabBarController {
|
||||||
let _viewController = ProfileViewController()
|
let _viewController = ProfileViewController()
|
||||||
_viewController.context = context
|
_viewController.context = context
|
||||||
_viewController.coordinator = coordinator
|
_viewController.coordinator = coordinator
|
||||||
|
_viewController.viewModel = MeProfileViewModel(context: context)
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
}
|
}
|
||||||
viewController.title = self.title
|
viewController.title = self.title
|
||||||
return UINavigationController(rootViewController: viewController)
|
return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +85,11 @@ class MainTabBarController: UITabBarController {
|
||||||
|
|
||||||
extension MainTabBarController {
|
extension MainTabBarController {
|
||||||
|
|
||||||
|
|
||||||
|
open override var childForStatusBarStyle: UIViewController? {
|
||||||
|
return selectedViewController
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -100,9 +106,9 @@ extension MainTabBarController {
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
|
|
||||||
// TODO: custom accent color
|
// TODO: custom accent color
|
||||||
let tabBarAppearance = UITabBarAppearance()
|
// let tabBarAppearance = UITabBarAppearance()
|
||||||
tabBarAppearance.configureWithDefaultBackground()
|
// tabBarAppearance.configureWithDefaultBackground()
|
||||||
tabBar.standardAppearance = tabBarAppearance
|
// tabBar.standardAppearance = tabBarAppearance
|
||||||
|
|
||||||
context.apiService.error
|
context.apiService.error
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -151,7 +157,7 @@ extension MainTabBarController {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// selectedIndex = 1
|
// selectedIndex = 3
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,10 @@ final class MastodonConfirmEmailViewController: UIViewController, NeedsDependenc
|
||||||
|
|
||||||
extension MastodonConfirmEmailViewController {
|
extension MastodonConfirmEmailViewController {
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .darkContent
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
|
||||||
setupOnboardingAppearance()
|
setupOnboardingAppearance()
|
||||||
|
|
|
@ -57,6 +57,10 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
||||||
|
|
||||||
extension MastodonPickServerViewController {
|
extension MastodonPickServerViewController {
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .darkContent
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
|
@ -235,6 +235,10 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
extension MastodonRegisterViewController {
|
extension MastodonRegisterViewController {
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .darkContent
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,11 @@ final class MastodonResendEmailViewController: UIViewController, NeedsDependency
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonResendEmailViewController {
|
extension MastodonResendEmailViewController {
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .darkContent
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -59,6 +64,7 @@ extension MastodonResendEmailViewController {
|
||||||
webView.load(request)
|
webView.load(request)
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: resendEmail via: %s", (#file as NSString).lastPathComponent, #line, #function, viewModel.resendEmailURL.debugDescription)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: resendEmail via: %s", (#file as NSString).lastPathComponent, #line, #function, viewModel.resendEmailURL.debugDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonResendEmailViewController {
|
extension MastodonResendEmailViewController {
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
import MastodonSDK
|
||||||
|
import SafariServices
|
||||||
|
|
||||||
final class MastodonServerRulesViewController: UIViewController, NeedsDependency {
|
final class MastodonServerRulesViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
@ -44,19 +46,20 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let bottonContainerView: UIView = {
|
let bottomContainerView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = Asset.Colors.Background.onboardingBackground.color
|
view.backgroundColor = Asset.Colors.Background.onboardingBackground.color
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private(set) lazy var bottomPromptLabel: UILabel = {
|
private(set) lazy var bottomPromptTextView: UITextView = {
|
||||||
let label = UILabel()
|
let textView = UITextView()
|
||||||
label.font = .preferredFont(forTextStyle: .body)
|
textView.font = .preferredFont(forTextStyle: .body)
|
||||||
label.textColor = .label
|
textView.textColor = .label
|
||||||
label.text = L10n.Scene.ServerRules.prompt(viewModel.domain)
|
textView.isSelectable = true
|
||||||
label.numberOfLines = 0
|
textView.isEditable = false
|
||||||
return label
|
textView.backgroundColor = Asset.Colors.Background.onboardingBackground.color
|
||||||
|
return textView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let confirmButton: PrimaryActionButton = {
|
let confirmButton: PrimaryActionButton = {
|
||||||
|
@ -82,40 +85,47 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
|
||||||
|
|
||||||
extension MastodonServerRulesViewController {
|
extension MastodonServerRulesViewController {
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .darkContent
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
setupOnboardingAppearance()
|
setupOnboardingAppearance()
|
||||||
|
configTextView()
|
||||||
|
|
||||||
defer { setupNavigationBarBackgroundView() }
|
defer { setupNavigationBarBackgroundView() }
|
||||||
|
|
||||||
bottonContainerView.translatesAutoresizingMaskIntoConstraints = false
|
bottomContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(bottonContainerView)
|
view.addSubview(bottomContainerView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
view.bottomAnchor.constraint(equalTo: bottonContainerView.bottomAnchor),
|
view.bottomAnchor.constraint(equalTo: bottomContainerView.bottomAnchor),
|
||||||
bottonContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
bottomContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
bottonContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
bottomContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
])
|
])
|
||||||
bottonContainerView.preservesSuperviewLayoutMargins = true
|
bottomContainerView.preservesSuperviewLayoutMargins = true
|
||||||
defer {
|
defer {
|
||||||
view.bringSubviewToFront(bottonContainerView)
|
view.bringSubviewToFront(bottomContainerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmButton.translatesAutoresizingMaskIntoConstraints = false
|
confirmButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
bottonContainerView.addSubview(confirmButton)
|
bottomContainerView.addSubview(confirmButton)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
bottonContainerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: confirmButton.bottomAnchor, constant: MastodonServerRulesViewController.viewBottomPaddingHeight),
|
bottomContainerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: confirmButton.bottomAnchor, constant: MastodonServerRulesViewController.viewBottomPaddingHeight),
|
||||||
confirmButton.leadingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.leadingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin),
|
confirmButton.leadingAnchor.constraint(equalTo: bottomContainerView.readableContentGuide.leadingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin),
|
||||||
bottonContainerView.readableContentGuide.trailingAnchor.constraint(equalTo: confirmButton.trailingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin),
|
bottomContainerView.readableContentGuide.trailingAnchor.constraint(equalTo: confirmButton.trailingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin),
|
||||||
confirmButton.heightAnchor.constraint(equalToConstant: MastodonServerRulesViewController.actionButtonHeight).priority(.defaultHigh),
|
confirmButton.heightAnchor.constraint(equalToConstant: MastodonServerRulesViewController.actionButtonHeight).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
|
|
||||||
bottomPromptLabel.translatesAutoresizingMaskIntoConstraints = false
|
bottomPromptTextView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
bottonContainerView.addSubview(bottomPromptLabel)
|
bottomContainerView.addSubview(bottomPromptTextView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
bottomPromptLabel.topAnchor.constraint(equalTo: bottonContainerView.topAnchor, constant: 20),
|
bottomPromptTextView.frameLayoutGuide.topAnchor.constraint(equalTo: bottomContainerView.topAnchor, constant: 20),
|
||||||
bottomPromptLabel.leadingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.leadingAnchor),
|
bottomPromptTextView.frameLayoutGuide.leadingAnchor.constraint(equalTo: bottomContainerView.readableContentGuide.leadingAnchor),
|
||||||
bottomPromptLabel.trailingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.trailingAnchor),
|
bottomPromptTextView.frameLayoutGuide.trailingAnchor.constraint(equalTo: bottomContainerView.readableContentGuide.trailingAnchor),
|
||||||
confirmButton.topAnchor.constraint(equalTo: bottomPromptLabel.bottomAnchor, constant: 20),
|
bottomPromptTextView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 50),
|
||||||
|
confirmButton.topAnchor.constraint(equalTo: bottomPromptTextView.frameLayoutGuide.bottomAnchor, constant: 20),
|
||||||
])
|
])
|
||||||
|
|
||||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -165,8 +175,32 @@ extension MastodonServerRulesViewController {
|
||||||
extension MastodonServerRulesViewController {
|
extension MastodonServerRulesViewController {
|
||||||
func updateScrollViewContentInset() {
|
func updateScrollViewContentInset() {
|
||||||
view.layoutIfNeeded()
|
view.layoutIfNeeded()
|
||||||
scrollView.contentInset.bottom = bottonContainerView.frame.height
|
scrollView.contentInset.bottom = bottomContainerView.frame.height
|
||||||
scrollView.verticalScrollIndicatorInsets.bottom = bottonContainerView.frame.height
|
scrollView.verticalScrollIndicatorInsets.bottom = bottomContainerView.frame.height
|
||||||
|
}
|
||||||
|
|
||||||
|
func configTextView() {
|
||||||
|
let linkColor = Asset.Colors.Button.normal.color
|
||||||
|
|
||||||
|
let str = NSString(string: L10n.Scene.ServerRules.prompt(viewModel.domain))
|
||||||
|
let termsOfServiceRange = str.range(of: L10n.Scene.ServerRules.termsOfService)
|
||||||
|
let privacyRange = str.range(of: L10n.Scene.ServerRules.privacyPolicy)
|
||||||
|
let attributeString = NSMutableAttributedString(string: L10n.Scene.ServerRules.prompt(viewModel.domain), attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body), NSAttributedString.Key.foregroundColor: UIColor.label])
|
||||||
|
attributeString.addAttribute(.link, value: Mastodon.API.serverRulesURL(domain: viewModel.domain), range: termsOfServiceRange)
|
||||||
|
attributeString.addAttribute(.link, value: Mastodon.API.privacyURL(domain: viewModel.domain), range: privacyRange)
|
||||||
|
let linkAttributes = [NSAttributedString.Key.foregroundColor:linkColor]
|
||||||
|
bottomPromptTextView.attributedText = attributeString
|
||||||
|
bottomPromptTextView.linkTextAttributes = linkAttributes
|
||||||
|
bottomPromptTextView.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonServerRulesViewController: UITextViewDelegate {
|
||||||
|
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||||
|
let safariVC = SFSafariViewController(url: URL)
|
||||||
|
self.present(safariVC, animated: true, completion: nil)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,13 +35,16 @@ final class MastodonServerRulesViewModel {
|
||||||
|
|
||||||
var rulesAttributedString: NSAttributedString {
|
var rulesAttributedString: NSAttributedString {
|
||||||
let attributedString = NSMutableAttributedString(string: "\n")
|
let attributedString = NSMutableAttributedString(string: "\n")
|
||||||
|
let configuration = UIImage.SymbolConfiguration(font: .preferredFont(forTextStyle: .title3))
|
||||||
for (i, rule) in rules.enumerated() {
|
for (i, rule) in rules.enumerated() {
|
||||||
let index = String(i + 1)
|
let imageName = String(i + 1) + ".circle.fill"
|
||||||
let indexString = NSAttributedString(string: index + ". ", attributes: [
|
let image = UIImage(systemName: imageName, withConfiguration: configuration)!
|
||||||
NSAttributedString.Key.foregroundColor: UIColor.secondaryLabel
|
let attachment = NSTextAttachment()
|
||||||
])
|
attachment.image = image.withTintColor(.black)
|
||||||
let ruleString = NSAttributedString(string: rule.text + "\n\n")
|
let imageAttribute = NSAttributedString(attachment: attachment)
|
||||||
attributedString.append(indexString)
|
|
||||||
|
let ruleString = NSAttributedString(string: " " + rule.text + "\n\n")
|
||||||
|
attributedString.append(imageAttribute)
|
||||||
attributedString.append(ruleString)
|
attributedString.append(ruleString)
|
||||||
}
|
}
|
||||||
return attributedString
|
return attributedString
|
||||||
|
|
|
@ -64,6 +64,10 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
extension WelcomeViewController {
|
extension WelcomeViewController {
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .darkContent
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
// CachedProfileViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-31.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
final class CachedProfileViewModel: ProfileViewModel {
|
||||||
|
|
||||||
|
convenience init(context: AppContext, mastodonUser: MastodonUser) {
|
||||||
|
self.init(context: context, optionalMastodonUser: mastodonUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
//
|
||||||
|
// ProfileHeaderViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ProfileHeaderViewControllerDelegate: class {
|
||||||
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
|
||||||
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ProfileHeaderViewController: UIViewController {
|
||||||
|
|
||||||
|
static let segmentedControlHeight: CGFloat = 32
|
||||||
|
static let segmentedControlMarginHeight: CGFloat = 20
|
||||||
|
static let headerMinHeight: CGFloat = segmentedControlHeight + 2 * segmentedControlMarginHeight
|
||||||
|
|
||||||
|
weak var delegate: ProfileHeaderViewControllerDelegate?
|
||||||
|
|
||||||
|
let profileBannerView = ProfileHeaderView()
|
||||||
|
let pageSegmentedControl: UISegmentedControl = {
|
||||||
|
let segmenetedControl = UISegmentedControl(items: ["A", "B"])
|
||||||
|
segmenetedControl.selectedSegmentIndex = 0
|
||||||
|
return segmenetedControl
|
||||||
|
}()
|
||||||
|
|
||||||
|
private var isBannerPinned = false
|
||||||
|
private var bottomShadowAlpha: CGFloat = 0.0
|
||||||
|
|
||||||
|
private var isAdjustBannerImageViewForSafeAreaInset = false
|
||||||
|
private var containerSafeAreaInset: UIEdgeInsets = .zero
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileHeaderViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
|
profileBannerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(profileBannerView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
profileBannerView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
profileBannerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
profileBannerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
])
|
||||||
|
profileBannerView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
|
pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(pageSegmentedControl)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
pageSegmentedControl.topAnchor.constraint(equalTo: profileBannerView.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight),
|
||||||
|
pageSegmentedControl.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||||
|
pageSegmentedControl.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: pageSegmentedControl.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight),
|
||||||
|
pageSegmentedControl.heightAnchor.constraint(equalToConstant: ProfileHeaderViewController.segmentedControlHeight).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
pageSegmentedControl.addTarget(self, action: #selector(ProfileHeaderViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if !isAdjustBannerImageViewForSafeAreaInset {
|
||||||
|
isAdjustBannerImageViewForSafeAreaInset = true
|
||||||
|
profileBannerView.bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
||||||
|
profileBannerView.bannerImageView.frame.size.height += containerSafeAreaInset.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view)
|
||||||
|
view.layer.setupShadow(color: UIColor.black.withAlphaComponent(0.12), alpha: Float(bottomShadowAlpha), x: 0, y: 2, blur: 2, spread: 0, roundedRect: view.bounds, byRoundingCorners: .allCorners, cornerRadii: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileHeaderViewController {
|
||||||
|
|
||||||
|
@objc private func pageSegmentedControlValueChanged(_ sender: UISegmentedControl) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: selectedSegmentIndex: %ld", ((#file as NSString).lastPathComponent), #line, #function, sender.selectedSegmentIndex)
|
||||||
|
delegate?.profileHeaderViewController(self, pageSegmentedControlValueChanged: sender, selectedSegmentIndex: sender.selectedSegmentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileHeaderViewController {
|
||||||
|
|
||||||
|
func updateHeaderContainerSafeAreaInset(_ inset: UIEdgeInsets) {
|
||||||
|
containerSafeAreaInset = inset
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateHeaderBottomShadow(progress: CGFloat) {
|
||||||
|
let alpha = min(max(0, 10 * progress - 9), 1)
|
||||||
|
if bottomShadowAlpha != alpha {
|
||||||
|
bottomShadowAlpha = alpha
|
||||||
|
view.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateHeaderScrollProgress(_ progress: CGFloat) {
|
||||||
|
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
|
||||||
|
updateHeaderBottomShadow(progress: progress)
|
||||||
|
|
||||||
|
let bannerImageView = profileBannerView.bannerImageView
|
||||||
|
guard bannerImageView.bounds != .zero else {
|
||||||
|
// wait layout finish
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let bannerContainerInWindow = profileBannerView.convert(profileBannerView.bannerContainerView.frame, to: nil)
|
||||||
|
let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
|
||||||
|
|
||||||
|
if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
|
||||||
|
bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
|
||||||
|
bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
|
||||||
|
} else if bannerContainerBottomOffset < containerSafeAreaInset.top {
|
||||||
|
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
||||||
|
let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
|
||||||
|
bannerImageView.frame.size.height = bannerImageHeight
|
||||||
|
} else {
|
||||||
|
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
||||||
|
bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle titleView
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
//
|
||||||
|
// ProfileFieldView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
|
final class ProfileFieldView: UIView {
|
||||||
|
|
||||||
|
let titleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
||||||
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
|
label.text = "Title"
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let valueActiveLabel: ActiveLabel = {
|
||||||
|
let label = ActiveLabel(style: .profileField)
|
||||||
|
label.configure(content: "value")
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let topSeparatorLine = UIView.separatorLine
|
||||||
|
let bottomSeparatorLine = UIView.separatorLine
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileFieldView {
|
||||||
|
private func _init() {
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(titleLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
titleLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
titleLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
|
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
titleLabel.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
|
valueActiveLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(valueActiveLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
valueActiveLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
valueActiveLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
|
||||||
|
valueActiveLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
])
|
||||||
|
valueActiveLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
|
||||||
|
topSeparatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(topSeparatorLine)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
topSeparatorLine.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
topSeparatorLine.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
topSeparatorLine.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
topSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
bottomSeparatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(bottomSeparatorLine)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
bottomSeparatorLine.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
|
bottomSeparatorLine.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ProfileFieldView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let filedView = ProfileFieldView()
|
||||||
|
filedView.valueActiveLabel.configure(field: "https://mastodon.online")
|
||||||
|
return filedView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
//
|
||||||
|
// ProfileFriendshipActionButton.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ProfileFriendshipActionButton: RoundedEdgesButton {
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileFriendshipActionButton {
|
||||||
|
private func _init() {
|
||||||
|
configure(state: .follow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileFriendshipActionButton {
|
||||||
|
enum State {
|
||||||
|
case follow
|
||||||
|
case following
|
||||||
|
case blocked
|
||||||
|
case muted
|
||||||
|
case edit
|
||||||
|
case editing
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .follow: return L10n.Common.Controls.Firendship.follow
|
||||||
|
case .following: return L10n.Common.Controls.Firendship.following
|
||||||
|
case .blocked: return L10n.Common.Controls.Firendship.blocked
|
||||||
|
case .muted: return L10n.Common.Controls.Firendship.muted
|
||||||
|
case .edit: return L10n.Common.Controls.Firendship.editInfo
|
||||||
|
case .editing: return L10n.Common.Controls.Actions.done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var backgroundColor: UIColor {
|
||||||
|
switch self {
|
||||||
|
case .follow: return Asset.Colors.Button.normal.color
|
||||||
|
case .following: return Asset.Colors.Button.normal.color
|
||||||
|
case .blocked: return Asset.Colors.Background.danger.color
|
||||||
|
case .muted: return Asset.Colors.Background.alertYellow.color
|
||||||
|
case .edit: return Asset.Colors.Button.normal.color
|
||||||
|
case .editing: return Asset.Colors.Button.normal.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configure(state: State) {
|
||||||
|
setTitle(state.title, for: .normal)
|
||||||
|
setTitleColor(.white, for: .normal)
|
||||||
|
setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted)
|
||||||
|
setBackgroundImage(.placeholder(color: state.backgroundColor), for: .normal)
|
||||||
|
setBackgroundImage(.placeholder(color: state.backgroundColor.withAlphaComponent(0.5)), for: .highlighted)
|
||||||
|
setBackgroundImage(.placeholder(color: state.backgroundColor.withAlphaComponent(0.5)), for: .disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,247 @@
|
||||||
|
//
|
||||||
|
// ProfileBannerView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
|
protocol ProfileHeaderViewDelegate: class {
|
||||||
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity)
|
||||||
|
|
||||||
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||||
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dwingDashboardMeterView: ProfileStatusDashboardMeterView)
|
||||||
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dwersDashboardMeterView: ProfileStatusDashboardMeterView)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
|
static let avatarImageViewSize = CGSize(width: 56, height: 56)
|
||||||
|
static let avatarImageViewCornerRadius: CGFloat = 6
|
||||||
|
static let friendshipActionButtonSize = CGSize(width: 108, height: 34)
|
||||||
|
|
||||||
|
weak var delegate: ProfileHeaderViewDelegate?
|
||||||
|
|
||||||
|
let bannerContainerView = UIView()
|
||||||
|
let bannerImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.image = .placeholder(color: .systemGray)
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let avatarImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
let placeholderImage = UIImage
|
||||||
|
.placeholder(size: ProfileHeaderView.avatarImageViewSize, color: Asset.Colors.Background.systemGroupedBackground.color)
|
||||||
|
.af.imageRounded(withCornerRadius: ProfileHeaderView.avatarImageViewCornerRadius, divideRadiusByImageScale: false)
|
||||||
|
imageView.image = placeholderImage
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let nameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
||||||
|
label.adjustsFontSizeToFitWidth = true
|
||||||
|
label.minimumScaleFactor = 0.5
|
||||||
|
label.textColor = .white
|
||||||
|
label.text = "Alice"
|
||||||
|
label.applyShadow(color: UIColor.black.withAlphaComponent(0.2), alpha: 0.5, x: 0, y: 2, blur: 2, spread: 0)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let usernameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||||
|
label.adjustsFontSizeToFitWidth = true
|
||||||
|
label.minimumScaleFactor = 0.5
|
||||||
|
label.textColor = .white
|
||||||
|
label.text = "@alice"
|
||||||
|
label.applyShadow(color: UIColor.black.withAlphaComponent(0.2), alpha: 0.5, x: 0, y: 2, blur: 2, spread: 0)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let statusDashboardView = ProfileStatusDashboardView()
|
||||||
|
let friendshipActionButton = ProfileFriendshipActionButton()
|
||||||
|
|
||||||
|
let bioContainerView = UIView()
|
||||||
|
let fieldContainerStackView = UIStackView()
|
||||||
|
|
||||||
|
let bioActiveLabel = ActiveLabel(style: .default)
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileHeaderView {
|
||||||
|
private func _init() {
|
||||||
|
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
|
// banner
|
||||||
|
bannerContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
bannerContainerView.preservesSuperviewLayoutMargins = true
|
||||||
|
addSubview(bannerContainerView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bannerContainerView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
bannerContainerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
trailingAnchor.constraint(equalTo: bannerContainerView.trailingAnchor),
|
||||||
|
readableContentGuide.widthAnchor.constraint(equalTo: bannerContainerView.heightAnchor, multiplier: 3), // set height to 1/3 of readable frame width
|
||||||
|
])
|
||||||
|
|
||||||
|
bannerImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
bannerImageView.frame = bannerContainerView.bounds
|
||||||
|
bannerContainerView.addSubview(bannerImageView)
|
||||||
|
|
||||||
|
// avatar
|
||||||
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
bannerContainerView.addSubview(avatarImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarImageView.leadingAnchor.constraint(equalTo: bannerContainerView.readableContentGuide.leadingAnchor),
|
||||||
|
bannerContainerView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 20),
|
||||||
|
avatarImageView.widthAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.width).priority(.required - 1),
|
||||||
|
avatarImageView.heightAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.height).priority(.required - 1),
|
||||||
|
])
|
||||||
|
|
||||||
|
// name container: [display name | username]
|
||||||
|
let nameContainerStackView = UIStackView()
|
||||||
|
nameContainerStackView.preservesSuperviewLayoutMargins = true
|
||||||
|
nameContainerStackView.axis = .vertical
|
||||||
|
nameContainerStackView.spacing = 0
|
||||||
|
nameContainerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(nameContainerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
nameContainerStackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 12),
|
||||||
|
nameContainerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
nameContainerStackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
||||||
|
])
|
||||||
|
nameContainerStackView.addArrangedSubview(nameLabel)
|
||||||
|
nameContainerStackView.addArrangedSubview(usernameLabel)
|
||||||
|
|
||||||
|
// meta container: [dashboard container | bio container | field container]
|
||||||
|
let metaContainerStackView = UIStackView()
|
||||||
|
metaContainerStackView.spacing = 16
|
||||||
|
metaContainerStackView.axis = .vertical
|
||||||
|
metaContainerStackView.preservesSuperviewLayoutMargins = true
|
||||||
|
metaContainerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(metaContainerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
metaContainerStackView.topAnchor.constraint(equalTo: bannerContainerView.bottomAnchor, constant: 13),
|
||||||
|
metaContainerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
metaContainerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
metaContainerStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
// dashboard container: [dashboard | friendship action button]
|
||||||
|
let dashboardContainerView = UIView()
|
||||||
|
dashboardContainerView.preservesSuperviewLayoutMargins = true
|
||||||
|
metaContainerStackView.addArrangedSubview(dashboardContainerView)
|
||||||
|
|
||||||
|
statusDashboardView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
dashboardContainerView.addSubview(statusDashboardView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
statusDashboardView.topAnchor.constraint(equalTo: dashboardContainerView.topAnchor),
|
||||||
|
statusDashboardView.leadingAnchor.constraint(equalTo: dashboardContainerView.readableContentGuide.leadingAnchor),
|
||||||
|
statusDashboardView.bottomAnchor.constraint(equalTo: dashboardContainerView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
friendshipActionButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
dashboardContainerView.addSubview(friendshipActionButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
friendshipActionButton.topAnchor.constraint(equalTo: dashboardContainerView.topAnchor),
|
||||||
|
friendshipActionButton.leadingAnchor.constraint(greaterThanOrEqualTo: statusDashboardView.trailingAnchor, constant: 8),
|
||||||
|
friendshipActionButton.trailingAnchor.constraint(equalTo: dashboardContainerView.readableContentGuide.trailingAnchor),
|
||||||
|
friendshipActionButton.widthAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.width).priority(.defaultHigh),
|
||||||
|
friendshipActionButton.heightAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.height).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
bioContainerView.preservesSuperviewLayoutMargins = true
|
||||||
|
metaContainerStackView.addArrangedSubview(bioContainerView)
|
||||||
|
bioActiveLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
bioContainerView.addSubview(bioActiveLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bioActiveLabel.topAnchor.constraint(equalTo: bioContainerView.topAnchor),
|
||||||
|
bioActiveLabel.leadingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.leadingAnchor),
|
||||||
|
bioActiveLabel.trailingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.trailingAnchor),
|
||||||
|
bioActiveLabel.bottomAnchor.constraint(equalTo: bioContainerView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
fieldContainerStackView.preservesSuperviewLayoutMargins = true
|
||||||
|
metaContainerStackView.addSubview(fieldContainerStackView)
|
||||||
|
|
||||||
|
bringSubviewToFront(bannerContainerView)
|
||||||
|
bringSubviewToFront(nameContainerStackView)
|
||||||
|
|
||||||
|
bioActiveLabel.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ActiveLabelDelegate
|
||||||
|
extension ProfileHeaderView: ActiveLabelDelegate {
|
||||||
|
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText)
|
||||||
|
delegate?.profileHeaderView(self, activeLabel: activeLabel, entityDidPressed: entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ProfileStatusDashboardViewDelegate
|
||||||
|
extension ProfileHeaderView: ProfileStatusDashboardViewDelegate {
|
||||||
|
|
||||||
|
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||||
|
delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, postDashboardMeterViewDidPressed: dashboardMeterView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||||
|
delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, followingDashboardMeterViewDidPressed: dashboardMeterView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||||
|
delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, followersDashboardMeterViewDidPressed: dashboardMeterView)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AvatarConfigurableView
|
||||||
|
extension ProfileHeaderView: AvatarConfigurableView {
|
||||||
|
static var configurableAvatarImageSize: CGSize { avatarImageViewSize }
|
||||||
|
static var configurableAvatarImageCornerRadius: CGFloat { avatarImageViewCornerRadius }
|
||||||
|
var configurableAvatarImageView: UIImageView? { return avatarImageView }
|
||||||
|
var configurableAvatarButton: UIButton? { return nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ProfileHeaderView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let banner = ProfileHeaderView()
|
||||||
|
banner.bannerImageView.image = UIImage(named: "lucas-ludwig")
|
||||||
|
return banner
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 800))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let banner = ProfileHeaderView()
|
||||||
|
//banner.bannerImageView.image = UIImage(named: "peter-luo")
|
||||||
|
return banner
|
||||||
|
}
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
.previewLayout(.fixed(width: 375, height: 800))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -0,0 +1,79 @@
|
||||||
|
//
|
||||||
|
// ProfileStatusDashboardMeterView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ProfileStatusDashboardMeterView: UIView {
|
||||||
|
|
||||||
|
let numberLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = {
|
||||||
|
let font = UIFont.systemFont(ofSize: 20, weight: .semibold)
|
||||||
|
return font.fontDescriptor.withDesign(.rounded).flatMap {
|
||||||
|
UIFont(descriptor: $0, size: 20)
|
||||||
|
} ?? font
|
||||||
|
}()
|
||||||
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
|
label.text = "999"
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let textLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
|
label.text = L10n.Scene.Profile.Dashboard.posts
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileStatusDashboardMeterView {
|
||||||
|
private func _init() {
|
||||||
|
numberLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(numberLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
numberLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
numberLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
trailingAnchor.constraint(equalTo: numberLabel.trailingAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
textLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(textLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
textLabel.topAnchor.constraint(equalTo: numberLabel.bottomAnchor),
|
||||||
|
textLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
trailingAnchor.constraint(equalTo: textLabel.trailingAnchor),
|
||||||
|
bottomAnchor.constraint(equalTo: textLabel.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ProfileStatusDashboardMeterView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UIViewPreview(width: 54) {
|
||||||
|
ProfileStatusDashboardMeterView()
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 54, height: 41))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -0,0 +1,103 @@
|
||||||
|
//
|
||||||
|
// ProfileStatusDashboardView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ProfileStatusDashboardViewDelegate: class {
|
||||||
|
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||||
|
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||||
|
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ProfileStatusDashboardView: UIView {
|
||||||
|
|
||||||
|
let postDashboardMeterView = ProfileStatusDashboardMeterView()
|
||||||
|
let followingDashboardMeterView = ProfileStatusDashboardMeterView()
|
||||||
|
let followersDashboardMeterView = ProfileStatusDashboardMeterView()
|
||||||
|
|
||||||
|
weak var delegate: ProfileStatusDashboardViewDelegate?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileStatusDashboardView {
|
||||||
|
private func _init() {
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor),
|
||||||
|
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
||||||
|
containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
let spacing: CGFloat = 16
|
||||||
|
containerStackView.spacing = spacing
|
||||||
|
containerStackView.axis = .horizontal
|
||||||
|
containerStackView.distribution = .fillEqually
|
||||||
|
containerStackView.alignment = .top
|
||||||
|
containerStackView.addArrangedSubview(postDashboardMeterView)
|
||||||
|
containerStackView.setCustomSpacing(spacing - 2, after: postDashboardMeterView)
|
||||||
|
containerStackView.addArrangedSubview(followingDashboardMeterView)
|
||||||
|
containerStackView.setCustomSpacing(spacing + 2, after: followingDashboardMeterView)
|
||||||
|
containerStackView.addArrangedSubview(followersDashboardMeterView)
|
||||||
|
|
||||||
|
postDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.posts
|
||||||
|
followingDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.following
|
||||||
|
followersDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.followers
|
||||||
|
|
||||||
|
[postDashboardMeterView, followingDashboardMeterView, followersDashboardMeterView].forEach { meterView in
|
||||||
|
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
|
tapGestureRecognizer.addTarget(self, action: #selector(ProfileStatusDashboardView.tapGestureRecognizerHandler(_:)))
|
||||||
|
meterView.addGestureRecognizer(tapGestureRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileStatusDashboardView {
|
||||||
|
@objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
guard let sourceView = sender.view as? ProfileStatusDashboardMeterView else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sourceView === postDashboardMeterView {
|
||||||
|
delegate?.profileStatusDashboardView(self, postDashboardMeterViewDidPressed: sourceView)
|
||||||
|
} else if sourceView === followingDashboardMeterView {
|
||||||
|
delegate?.profileStatusDashboardView(self, followingDashboardMeterViewDidPressed: sourceView)
|
||||||
|
} else if sourceView === followersDashboardMeterView {
|
||||||
|
delegate?.profileStatusDashboardView(self, followersDashboardMeterViewDidPressed: sourceView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ProfileBannerStatusView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
ProfileStatusDashboardView()
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// MeProfileViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class MeProfileViewModel: ProfileViewModel {
|
||||||
|
|
||||||
|
init(context: AppContext) {
|
||||||
|
super.init(
|
||||||
|
context: context,
|
||||||
|
optionalMastodonUser: context.authenticationService.activeMastodonAuthentication.value?.user
|
||||||
|
)
|
||||||
|
|
||||||
|
self.currentMastodonUser
|
||||||
|
.sink { [weak self] currentMastodonUser in
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: current active twitter user: %s", ((#file as NSString).lastPathComponent), #line, #function, currentMastodonUser?.username ?? "<nil>")
|
||||||
|
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.mastodonUser.value = currentMastodonUser
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,20 +5,672 @@
|
||||||
// Created by MainasuK Cirno on 2021-2-23.
|
// Created by MainasuK Cirno on 2021-2-23.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
final class ProfileViewController: UIViewController, NeedsDependency {
|
final class ProfileViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var viewModel: ProfileViewModel!
|
||||||
|
|
||||||
|
private var preferredStatusBarStyleForBanner: UIStatusBarStyle = .lightContent {
|
||||||
|
didSet {
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshControl: UIRefreshControl = {
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
refreshControl.tintColor = .label
|
||||||
|
return refreshControl
|
||||||
|
}()
|
||||||
|
|
||||||
|
let containerScrollView: UIScrollView = {
|
||||||
|
let scrollView = UIScrollView()
|
||||||
|
scrollView.scrollsToTop = false
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
scrollView.preservesSuperviewLayoutMargins = true
|
||||||
|
scrollView.delaysContentTouches = false
|
||||||
|
return scrollView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let overlayScrollView: UIScrollView = {
|
||||||
|
let scrollView = UIScrollView()
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
scrollView.backgroundColor = .clear
|
||||||
|
scrollView.delaysContentTouches = false
|
||||||
|
return scrollView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private(set) lazy var profileSegmentedViewController = ProfileSegmentedViewController()
|
||||||
|
private(set) lazy var profileHeaderViewController = ProfileHeaderViewController()
|
||||||
|
private var profileBannerImageViewLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
private var contentOffsets: [Int: CGFloat] = [:]
|
||||||
|
var currentPostTimelineTableViewContentSizeObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: deinit", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewController {
|
extension ProfileViewController {
|
||||||
|
|
||||||
|
func observeTableViewContentSize(scrollView: UIScrollView) -> NSKeyValueObservation {
|
||||||
|
updateOverlayScrollViewContentSize(scrollView: scrollView)
|
||||||
|
return scrollView.observe(\.contentSize, options: .new) { scrollView, change in
|
||||||
|
self.updateOverlayScrollViewContentSize(scrollView: scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateOverlayScrollViewContentSize(scrollView: UIScrollView) {
|
||||||
|
let bottomPageHeight = max(scrollView.contentSize.height, self.containerScrollView.frame.height - ProfileHeaderViewController.headerMinHeight - self.containerScrollView.safeAreaInsets.bottom)
|
||||||
|
let headerViewHeight: CGFloat = profileHeaderViewController.view.frame.height
|
||||||
|
let contentSize = CGSize(
|
||||||
|
width: self.containerScrollView.contentSize.width,
|
||||||
|
height: bottomPageHeight + headerViewHeight
|
||||||
|
)
|
||||||
|
self.overlayScrollView.contentSize = contentSize
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: contentSize: %s", ((#file as NSString).lastPathComponent), #line, #function, contentSize.debugDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileViewController {
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return preferredStatusBarStyleForBanner
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewSafeAreaInsetsDidChange() {
|
||||||
|
super.viewSafeAreaInsetsDidChange()
|
||||||
|
|
||||||
|
profileHeaderViewController.updateHeaderContainerSafeAreaInset(view.safeAreaInsets)
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
|
let barAppearance = UINavigationBarAppearance()
|
||||||
|
barAppearance.configureWithTransparentBackground()
|
||||||
|
navigationItem.standardAppearance = barAppearance
|
||||||
|
navigationItem.compactAppearance = barAppearance
|
||||||
|
navigationItem.scrollEdgeAppearance = barAppearance
|
||||||
|
navigationItem.titleView = UIView()
|
||||||
|
|
||||||
|
// if navigationController?.viewControllers.first == self {
|
||||||
|
// navigationItem.leftBarButtonItem = avatarBarButtonItem
|
||||||
|
// }
|
||||||
|
// avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(ProfileViewController.avatarButtonPressed(_:)), for: .touchUpInside)
|
||||||
|
|
||||||
|
// unmuteMenuBarButtonItem.target = self
|
||||||
|
// unmuteMenuBarButtonItem.action = #selector(ProfileViewController.unmuteBarButtonItemPressed(_:))
|
||||||
|
|
||||||
|
// Publishers.CombineLatest4(
|
||||||
|
// viewModel.muted.eraseToAnyPublisher(),
|
||||||
|
// viewModel.blocked.eraseToAnyPublisher(),
|
||||||
|
// viewModel.twitterUser.eraseToAnyPublisher(),
|
||||||
|
// context.authenticationService.activeTwitterAuthenticationBox.eraseToAnyPublisher()
|
||||||
|
// )
|
||||||
|
// .receive(on: DispatchQueue.main)
|
||||||
|
// .sink { [weak self] muted, blocked, twitterUser, activeTwitterAuthenticationBox in
|
||||||
|
// guard let self = self else { return }
|
||||||
|
// guard let twitterUser = twitterUser,
|
||||||
|
// let activeTwitterAuthenticationBox = activeTwitterAuthenticationBox,
|
||||||
|
// twitterUser.id != activeTwitterAuthenticationBox.twitterUserID else {
|
||||||
|
// self.navigationItem.rightBarButtonItems = []
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if #available(iOS 14.0, *) {
|
||||||
|
// self.moreMenuBarButtonItem.target = nil
|
||||||
|
// self.moreMenuBarButtonItem.action = nil
|
||||||
|
// self.moreMenuBarButtonItem.menu = UserProviderFacade.createMenuForUser(
|
||||||
|
// twitterUser: twitterUser,
|
||||||
|
// muted: muted,
|
||||||
|
// blocked: blocked,
|
||||||
|
// dependency: self
|
||||||
|
// )
|
||||||
|
// } else {
|
||||||
|
// // no menu supports for early version
|
||||||
|
// self.moreMenuBarButtonItem.target = self
|
||||||
|
// self.moreMenuBarButtonItem.action = #selector(ProfileViewController.moreMenuBarButtonItemPressed(_:))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var rightBarButtonItems: [UIBarButtonItem] = [self.moreMenuBarButtonItem]
|
||||||
|
// if muted {
|
||||||
|
// rightBarButtonItems.append(self.unmuteMenuBarButtonItem)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// self.navigationItem.rightBarButtonItems = rightBarButtonItems
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
|
||||||
|
overlayScrollView.refreshControl = refreshControl
|
||||||
|
refreshControl.addTarget(self, action: #selector(ProfileViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
|
||||||
|
// drawerSidebarTransitionController = DrawerSidebarTransitionController(drawerSidebarTransitionableViewController: self)
|
||||||
|
|
||||||
|
let postsUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter())
|
||||||
|
viewModel.domain.assign(to: \.value, on: postsUserTimelineViewModel.domain).store(in: &disposeBag)
|
||||||
|
viewModel.userID.assign(to: \.value, on: postsUserTimelineViewModel.userID).store(in: &disposeBag)
|
||||||
|
|
||||||
|
let repliesUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: true))
|
||||||
|
viewModel.domain.assign(to: \.value, on: repliesUserTimelineViewModel.domain).store(in: &disposeBag)
|
||||||
|
viewModel.userID.assign(to: \.value, on: repliesUserTimelineViewModel.userID).store(in: &disposeBag)
|
||||||
|
|
||||||
|
let mediaUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(onlyMedia: true))
|
||||||
|
viewModel.domain.assign(to: \.value, on: mediaUserTimelineViewModel.domain).store(in: &disposeBag)
|
||||||
|
viewModel.userID.assign(to: \.value, on: mediaUserTimelineViewModel.userID).store(in: &disposeBag)
|
||||||
|
|
||||||
|
profileSegmentedViewController.pagingViewController.viewModel = {
|
||||||
|
let profilePagingViewModel = ProfilePagingViewModel(
|
||||||
|
postsUserTimelineViewModel: postsUserTimelineViewModel,
|
||||||
|
repliesUserTimelineViewModel: repliesUserTimelineViewModel,
|
||||||
|
mediaUserTimelineViewModel: mediaUserTimelineViewModel
|
||||||
|
)
|
||||||
|
profilePagingViewModel.viewControllers.forEach { viewController in
|
||||||
|
if let viewController = viewController as? NeedsDependency {
|
||||||
|
viewController.context = context
|
||||||
|
viewController.coordinator = coordinator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profilePagingViewModel
|
||||||
|
}()
|
||||||
|
|
||||||
|
profileHeaderViewController.pageSegmentedControl.removeAllSegments()
|
||||||
|
profileSegmentedViewController.pagingViewController.viewModel.barItems.forEach { item in
|
||||||
|
let index = profileHeaderViewController.pageSegmentedControl.numberOfSegments
|
||||||
|
profileHeaderViewController.pageSegmentedControl.insertSegment(withTitle: item.title, at: index, animated: false)
|
||||||
|
}
|
||||||
|
profileHeaderViewController.pageSegmentedControl.selectedSegmentIndex = 0
|
||||||
|
|
||||||
|
overlayScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(overlayScrollView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
overlayScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
overlayScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
view.trailingAnchor.constraint(equalTo: overlayScrollView.frameLayoutGuide.trailingAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: overlayScrollView.frameLayoutGuide.bottomAnchor),
|
||||||
|
overlayScrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
containerScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(containerScrollView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
containerScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
view.trailingAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.trailingAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.bottomAnchor),
|
||||||
|
containerScrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
// add segmented list
|
||||||
|
addChild(profileSegmentedViewController)
|
||||||
|
profileSegmentedViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerScrollView.addSubview(profileSegmentedViewController.view)
|
||||||
|
profileSegmentedViewController.didMove(toParent: self)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
profileSegmentedViewController.view.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor),
|
||||||
|
profileSegmentedViewController.view.trailingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.trailingAnchor),
|
||||||
|
profileSegmentedViewController.view.bottomAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.bottomAnchor),
|
||||||
|
profileSegmentedViewController.view.heightAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.heightAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
// add header
|
||||||
|
addChild(profileHeaderViewController)
|
||||||
|
profileHeaderViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerScrollView.addSubview(profileHeaderViewController.view)
|
||||||
|
profileHeaderViewController.didMove(toParent: self)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
profileHeaderViewController.view.topAnchor.constraint(equalTo: containerScrollView.topAnchor),
|
||||||
|
profileHeaderViewController.view.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor),
|
||||||
|
containerScrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: profileHeaderViewController.view.trailingAnchor),
|
||||||
|
profileSegmentedViewController.view.topAnchor.constraint(equalTo: profileHeaderViewController.view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
containerScrollView.addGestureRecognizer(overlayScrollView.panGestureRecognizer)
|
||||||
|
overlayScrollView.layer.zPosition = .greatestFiniteMagnitude // make vision top-most
|
||||||
|
overlayScrollView.delegate = self
|
||||||
|
profileHeaderViewController.delegate = self
|
||||||
|
profileSegmentedViewController.pagingViewController.pagingDelegate = self
|
||||||
|
|
||||||
|
// // add segmented bar to header
|
||||||
|
// profileSegmentedViewController.pagingViewController.addBar(
|
||||||
|
// bar,
|
||||||
|
// dataSource: profileSegmentedViewController.pagingViewController.viewModel,
|
||||||
|
// at: .custom(view: profileHeaderViewController.view, layout: { bar in
|
||||||
|
// bar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
// self.profileHeaderViewController.view.addSubview(bar)
|
||||||
|
// NSLayoutConstraint.activate([
|
||||||
|
// bar.leadingAnchor.constraint(equalTo: self.profileHeaderViewController.view.leadingAnchor),
|
||||||
|
// bar.trailingAnchor.constraint(equalTo: self.profileHeaderViewController.view.trailingAnchor),
|
||||||
|
// bar.bottomAnchor.constraint(equalTo: self.profileHeaderViewController.view.bottomAnchor),
|
||||||
|
// bar.heightAnchor.constraint(equalToConstant: ProfileHeaderViewController.headerMinHeight).priority(.defaultHigh),
|
||||||
|
// ])
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
|
||||||
|
// bind view model
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
viewModel.bannerImageURL.eraseToAnyPublisher(),
|
||||||
|
viewModel.viewDidAppear.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] bannerImageURL, _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.profileHeaderViewController.profileBannerView.bannerImageView.af.cancelImageRequest()
|
||||||
|
let placeholder = UIImage.placeholder(color: Asset.Colors.Background.systemGroupedBackground.color)
|
||||||
|
guard let bannerImageURL = bannerImageURL else {
|
||||||
|
self.profileHeaderViewController.profileBannerView.bannerImageView.image = placeholder
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.profileHeaderViewController.profileBannerView.bannerImageView.af.setImage(
|
||||||
|
withURL: bannerImageURL,
|
||||||
|
placeholderImage: placeholder,
|
||||||
|
imageTransition: .crossDissolve(0.3),
|
||||||
|
runImageTransitionIfCached: false,
|
||||||
|
completion: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch response.result {
|
||||||
|
case .success(let image):
|
||||||
|
self.viewModel.headerDomainLumaStyle.value = image.domainLumaCoefficientsStyle ?? .dark
|
||||||
|
case .failure:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
viewModel.headerDomainLumaStyle
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] style in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let textColor: UIColor
|
||||||
|
let shadowColor: UIColor
|
||||||
|
switch style {
|
||||||
|
case .light:
|
||||||
|
self.preferredStatusBarStyleForBanner = .darkContent
|
||||||
|
textColor = .black
|
||||||
|
shadowColor = .white
|
||||||
|
case .dark:
|
||||||
|
self.preferredStatusBarStyleForBanner = .lightContent
|
||||||
|
textColor = .white
|
||||||
|
shadowColor = .black
|
||||||
|
default:
|
||||||
|
self.preferredStatusBarStyleForBanner = .default
|
||||||
|
textColor = .white
|
||||||
|
shadowColor = .black
|
||||||
|
}
|
||||||
|
|
||||||
|
self.profileHeaderViewController.profileBannerView.nameLabel.textColor = textColor
|
||||||
|
self.profileHeaderViewController.profileBannerView.usernameLabel.textColor = textColor
|
||||||
|
self.profileHeaderViewController.profileBannerView.nameLabel.applyShadow(color: shadowColor, alpha: 0.5, x: 0, y: 2, blur: 2)
|
||||||
|
self.profileHeaderViewController.profileBannerView.usernameLabel.applyShadow(color: shadowColor, alpha: 0.5, x: 0, y: 2, blur: 2)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
viewModel.avatarImageURL.eraseToAnyPublisher(),
|
||||||
|
viewModel.viewDidAppear.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] avatarImageURL, _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.profileHeaderViewController.profileBannerView.configure(
|
||||||
|
with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarImageURL)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// viewModel.protected
|
||||||
|
// .map { $0 != true }
|
||||||
|
// .assign(to: \.isHidden, on: profileHeaderViewController.profileBannerView.lockImageView)
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
viewModel.name
|
||||||
|
.map { $0 ?? " " }
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.text, on: profileHeaderViewController.profileBannerView.nameLabel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
viewModel.username
|
||||||
|
.map { username in username.flatMap { "@" + $0 } ?? " " }
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.text, on: profileHeaderViewController.profileBannerView.usernameLabel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// viewModel.friendship
|
||||||
|
// .sink { [weak self] friendship in
|
||||||
|
// guard let self = self else { return }
|
||||||
|
// let followingButton = self.profileHeaderViewController.profileBannerView.profileBannerInfoActionView.followActionButton
|
||||||
|
// followingButton.isHidden = friendship == nil
|
||||||
|
//
|
||||||
|
// if let friendship = friendship {
|
||||||
|
// switch friendship {
|
||||||
|
// case .following: followingButton.style = .following
|
||||||
|
// case .pending: followingButton.style = .pending
|
||||||
|
// case .none: followingButton.style = .follow
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
// viewModel.followedBy
|
||||||
|
// .sink { [weak self] followedBy in
|
||||||
|
// guard let self = self else { return }
|
||||||
|
// let followStatusLabel = self.profileHeaderViewController.profileBannerView.profileBannerInfoActionView.followStatusLabel
|
||||||
|
// followStatusLabel.isHidden = followedBy != true
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
//
|
||||||
|
viewModel.bioDescription
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink(receiveValue: { [weak self] bio in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.profileHeaderViewController.profileBannerView.bioActiveLabel.configure(note: bio ?? "")
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// Publishers.CombineLatest(
|
||||||
|
// viewModel.url.eraseToAnyPublisher(),
|
||||||
|
// viewModel.suspended.eraseToAnyPublisher()
|
||||||
|
// )
|
||||||
|
// .receive(on: DispatchQueue.main)
|
||||||
|
// .sink { [weak self] url, isSuspended in
|
||||||
|
// guard let self = self else { return }
|
||||||
|
// let url = url.flatMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) } ?? " "
|
||||||
|
// self.profileHeaderViewController.profileBannerView.linkButton.setTitle(url, for: .normal)
|
||||||
|
// let isEmpty = url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
// self.profileHeaderViewController.profileBannerView.linkContainer.isHidden = isEmpty || isSuspended
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
// Publishers.CombineLatest(
|
||||||
|
// viewModel.location.eraseToAnyPublisher(),
|
||||||
|
// viewModel.suspended.eraseToAnyPublisher()
|
||||||
|
// )
|
||||||
|
// .receive(on: DispatchQueue.main)
|
||||||
|
// .sink { [weak self] location, isSuspended in
|
||||||
|
// guard let self = self else { return }
|
||||||
|
// let location = location.flatMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) } ?? " "
|
||||||
|
// self.profileHeaderViewController.profileBannerView.geoButton.setTitle(location, for: .normal)
|
||||||
|
// let isEmpty = location.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
// self.profileHeaderViewController.profileBannerView.geoContainer.isHidden = isEmpty || isSuspended
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
viewModel.statusesCount
|
||||||
|
.sink { [weak self] count in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let text = count.flatMap { String($0) } ?? "-"
|
||||||
|
self.profileHeaderViewController.profileBannerView.statusDashboardView.postDashboardMeterView.numberLabel.text = text
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
viewModel.followingCount
|
||||||
|
.sink { [weak self] count in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let text = count.flatMap { String($0) } ?? "-"
|
||||||
|
self.profileHeaderViewController.profileBannerView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
viewModel.followersCount
|
||||||
|
.sink { [weak self] count in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let text = count.flatMap { String($0) } ?? "-"
|
||||||
|
self.profileHeaderViewController.profileBannerView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// viewModel.followersCount
|
||||||
|
// .sink { [weak self] count in
|
||||||
|
// guard let self = self else { return }
|
||||||
|
// self.profileHeaderViewController.profileBannerView.profileBannerStatusView.followersStatusItemView.countLabel.text = count.flatMap { "\($0)" } ?? "-"
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
// viewModel.listedCount
|
||||||
|
// .sink { [weak self] count in
|
||||||
|
// guard let self = self else { return }
|
||||||
|
// self.profileHeaderViewController.profileBannerView.profileBannerStatusView.listedStatusItemView.countLabel.text = count.flatMap { "\($0)" } ?? "-"
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
// viewModel.suspended
|
||||||
|
// .receive(on: DispatchQueue.main)
|
||||||
|
// .sink { [weak self] isSuspended in
|
||||||
|
// guard let self = self else { return }
|
||||||
|
// self.profileHeaderViewController.profileBannerView.profileBannerStatusView.isHidden = isSuspended
|
||||||
|
// self.profileHeaderViewController.profileBannerView.profileBannerInfoActionView.isHidden = isSuspended
|
||||||
|
// if isSuspended {
|
||||||
|
// self.profileSegmentedViewController
|
||||||
|
// .pagingViewController.viewModel
|
||||||
|
// .profileTweetPostTimelineViewController.viewModel
|
||||||
|
// .stateMachine
|
||||||
|
// .enter(UserTimelineViewModel.State.Suspended.self)
|
||||||
|
// self.profileSegmentedViewController
|
||||||
|
// .pagingViewController.viewModel
|
||||||
|
// .profileMediaPostTimelineViewController.viewModel
|
||||||
|
// .stateMachine
|
||||||
|
// .enter(UserMediaTimelineViewModel.State.Suspended.self)
|
||||||
|
// self.profileSegmentedViewController
|
||||||
|
// .pagingViewController.viewModel
|
||||||
|
// .profileLikesPostTimelineViewController.viewModel
|
||||||
|
// .stateMachine
|
||||||
|
// .enter(UserLikeTimelineViewModel.State.Suspended.self)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
|
||||||
|
//
|
||||||
|
profileHeaderViewController.profileBannerView.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
viewModel.viewDidAppear.send()
|
||||||
|
|
||||||
|
// set overlay scroll view initial content size
|
||||||
|
guard let currentViewController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer else { return }
|
||||||
|
currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize(scrollView: currentViewController.scrollView)
|
||||||
|
currentViewController.scrollView.panGestureRecognizer.require(toFail: overlayScrollView.panGestureRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
currentPostTimelineTableViewContentSizeObservation = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ProfileViewController {
|
||||||
|
|
||||||
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
let currentViewController = profileSegmentedViewController.pagingViewController.currentViewController
|
||||||
|
if let currentViewController = currentViewController as? UserTimelineViewController {
|
||||||
|
currentViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
|
sender.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @objc private func avatarButtonPressed(_ sender: UIButton) {
|
||||||
|
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
// coordinator.present(scene: .drawerSidebar, from: self, transition: .custom(transitioningDelegate: drawerSidebarTransitionController))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @objc private func unmuteBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
|
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
// guard let twitterUser = viewModel.twitterUser.value else {
|
||||||
|
// assertionFailure()
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// UserProviderFacade.toggleMuteUser(
|
||||||
|
// context: context,
|
||||||
|
// twitterUser: twitterUser,
|
||||||
|
// muted: viewModel.muted.value
|
||||||
|
// )
|
||||||
|
// .sink { _ in
|
||||||
|
// // do nothing
|
||||||
|
// } receiveValue: { _ in
|
||||||
|
// // do nothing
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @objc private func moreMenuBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
|
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
// guard let twitterUser = viewModel.twitterUser.value else {
|
||||||
|
// assertionFailure()
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let moreMenuAlertController = UserProviderFacade.createMoreMenuAlertControllerForUser(
|
||||||
|
// twitterUser: twitterUser,
|
||||||
|
// muted: viewModel.muted.value,
|
||||||
|
// blocked: viewModel.blocked.value,
|
||||||
|
// sender: sender,
|
||||||
|
// dependency: self
|
||||||
|
// )
|
||||||
|
// present(moreMenuAlertController, animated: true, completion: nil)
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIScrollViewDelegate
|
||||||
|
extension ProfileViewController: UIScrollViewDelegate {
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
contentOffsets[profileSegmentedViewController.pagingViewController.currentIndex!] = scrollView.contentOffset.y
|
||||||
|
let topMaxContentOffsetY = profileSegmentedViewController.view.frame.minY - ProfileHeaderViewController.headerMinHeight - containerScrollView.safeAreaInsets.top
|
||||||
|
if scrollView.contentOffset.y < topMaxContentOffsetY {
|
||||||
|
self.containerScrollView.contentOffset.y = scrollView.contentOffset.y
|
||||||
|
for postTimelineView in profileSegmentedViewController.pagingViewController.viewModel.viewControllers {
|
||||||
|
postTimelineView.scrollView.contentOffset.y = 0
|
||||||
|
}
|
||||||
|
contentOffsets.removeAll()
|
||||||
|
} else {
|
||||||
|
containerScrollView.contentOffset.y = topMaxContentOffsetY
|
||||||
|
if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer {
|
||||||
|
let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y
|
||||||
|
customScrollViewContainerController.scrollView.contentOffset.y = contentOffsetY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// elastically banner image
|
||||||
|
let headerScrollProgress = containerScrollView.contentOffset.y / topMaxContentOffsetY
|
||||||
|
profileHeaderViewController.updateHeaderScrollProgress(headerScrollProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ProfileHeaderViewControllerDelegate
|
||||||
|
extension ProfileViewController: ProfileHeaderViewControllerDelegate {
|
||||||
|
|
||||||
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) {
|
||||||
|
guard let scrollView = (profileSegmentedViewController.pagingViewController.currentViewController as? UserTimelineViewController)?.scrollView else {
|
||||||
|
// assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOverlayScrollViewContentSize(scrollView: scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int) {
|
||||||
|
profileSegmentedViewController.pagingViewController.scrollToPage(
|
||||||
|
.at(index: index),
|
||||||
|
animated: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ProfilePagingViewControllerDelegate
|
||||||
|
extension ProfileViewController: ProfilePagingViewControllerDelegate {
|
||||||
|
|
||||||
|
func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController postTimelineViewController: ScrollViewContainer, atIndex index: Int) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index)
|
||||||
|
|
||||||
|
// save content offset
|
||||||
|
overlayScrollView.contentOffset.y = contentOffsets[index] ?? containerScrollView.contentOffset.y
|
||||||
|
|
||||||
|
// setup observer and gesture fallback
|
||||||
|
currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize(scrollView: postTimelineViewController.scrollView)
|
||||||
|
postTimelineViewController.scrollView.panGestureRecognizer.require(toFail: overlayScrollView.panGestureRecognizer)
|
||||||
|
|
||||||
|
|
||||||
|
// if let userMediaTimelineViewController = postTimelineViewController as? UserMediaTimelineViewController,
|
||||||
|
// let currentState = userMediaTimelineViewController.viewModel.stateMachine.currentState {
|
||||||
|
// switch currentState {
|
||||||
|
// case is UserMediaTimelineViewModel.State.NoMore,
|
||||||
|
// is UserMediaTimelineViewModel.State.NotAuthorized,
|
||||||
|
// is UserMediaTimelineViewModel.State.Blocked:
|
||||||
|
// break
|
||||||
|
// default:
|
||||||
|
// if userMediaTimelineViewController.viewModel.items.value.isEmpty {
|
||||||
|
// userMediaTimelineViewController.viewModel.stateMachine.enter(UserMediaTimelineViewModel.State.Reloading.self)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if let userLikeTimelineViewController = postTimelineViewController as? UserLikeTimelineViewController,
|
||||||
|
// let currentState = userLikeTimelineViewController.viewModel.stateMachine.currentState {
|
||||||
|
// switch currentState {
|
||||||
|
// case is UserLikeTimelineViewModel.State.NoMore,
|
||||||
|
// is UserLikeTimelineViewModel.State.NotAuthorized,
|
||||||
|
// is UserLikeTimelineViewModel.State.Blocked:
|
||||||
|
// break
|
||||||
|
// default:
|
||||||
|
// if userLikeTimelineViewController.viewModel.items.value.isEmpty {
|
||||||
|
// userLikeTimelineViewController.viewModel.stateMachine.enter(UserLikeTimelineViewModel.State.Reloading.self)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ProfileBannerInfoActionViewDelegate
|
||||||
|
//extension ProfileViewController: ProfileBannerInfoActionViewDelegate {
|
||||||
|
//
|
||||||
|
// func profileBannerInfoActionView(_ profileBannerInfoActionView: ProfileBannerInfoActionView, followActionButtonPressed button: FollowActionButton) {
|
||||||
|
// UserProviderFacade
|
||||||
|
// .toggleUserFriendship(provider: self, sender: button)
|
||||||
|
// .sink { _ in
|
||||||
|
// // do nothing
|
||||||
|
// } receiveValue: { _ in
|
||||||
|
// // do nothing
|
||||||
|
// }
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
|
||||||
|
// MARK: - ProfileHeaderViewDelegate
|
||||||
|
extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||||
|
|
||||||
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dwingDashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dwersDashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ScrollViewContainer
|
||||||
|
extension ProfileViewController: ScrollViewContainer {
|
||||||
|
var scrollView: UIScrollView { return overlayScrollView }
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,197 @@
|
||||||
|
//
|
||||||
|
// ProfileViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
// please override this base class
|
||||||
|
class ProfileViewModel: NSObject {
|
||||||
|
|
||||||
|
typealias UserID = String
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
private var mastodonUserObserver: AnyCancellable?
|
||||||
|
private var currentMastodonUserObserver: AnyCancellable?
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let mastodonUser: CurrentValueSubject<MastodonUser?, Never>
|
||||||
|
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
|
||||||
|
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||||
|
let headerDomainLumaStyle = CurrentValueSubject<UIUserInterfaceStyle, Never>(.dark) // default dark for placeholder banner
|
||||||
|
|
||||||
|
// output
|
||||||
|
let domain: CurrentValueSubject<String?, Never>
|
||||||
|
let userID: CurrentValueSubject<UserID?, Never>
|
||||||
|
let bannerImageURL: CurrentValueSubject<URL?, Never>
|
||||||
|
let avatarImageURL: CurrentValueSubject<URL?, Never>
|
||||||
|
// let protected: CurrentValueSubject<Bool?, Never>
|
||||||
|
let name: CurrentValueSubject<String?, Never>
|
||||||
|
let username: CurrentValueSubject<String?, Never>
|
||||||
|
let bioDescription: CurrentValueSubject<String?, Never>
|
||||||
|
let url: CurrentValueSubject<String?, Never>
|
||||||
|
let statusesCount: CurrentValueSubject<Int?, Never>
|
||||||
|
let followingCount: CurrentValueSubject<Int?, Never>
|
||||||
|
let followersCount: CurrentValueSubject<Int?, Never>
|
||||||
|
|
||||||
|
// let friendship: CurrentValueSubject<Friendship?, Never>
|
||||||
|
// let followedBy: CurrentValueSubject<Bool?, Never>
|
||||||
|
// let muted: CurrentValueSubject<Bool, Never>
|
||||||
|
// let blocked: CurrentValueSubject<Bool, Never>
|
||||||
|
//
|
||||||
|
// let suspended = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
//
|
||||||
|
|
||||||
|
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
|
||||||
|
self.context = context
|
||||||
|
self.mastodonUser = CurrentValueSubject(mastodonUser)
|
||||||
|
self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain)
|
||||||
|
self.userID = CurrentValueSubject(mastodonUser?.id)
|
||||||
|
self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL())
|
||||||
|
self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL())
|
||||||
|
// self.protected = CurrentValueSubject(twitterUser?.protected)
|
||||||
|
self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback)
|
||||||
|
self.username = CurrentValueSubject(mastodonUser?.acctWithDomain)
|
||||||
|
self.bioDescription = CurrentValueSubject(mastodonUser?.note)
|
||||||
|
self.url = CurrentValueSubject(mastodonUser?.url)
|
||||||
|
self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.statusesCount) })
|
||||||
|
self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followingCount) })
|
||||||
|
self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followersCount) })
|
||||||
|
// self.friendship = CurrentValueSubject(nil)
|
||||||
|
// self.followedBy = CurrentValueSubject(nil)
|
||||||
|
// self.muted = CurrentValueSubject(false)
|
||||||
|
// self.blocked = CurrentValueSubject(false)
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
// bind active authentication
|
||||||
|
context.authenticationService.activeMastodonAuthentication
|
||||||
|
.sink { [weak self] activeMastodonAuthentication in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let activeMastodonAuthentication = activeMastodonAuthentication else {
|
||||||
|
self.domain.value = nil
|
||||||
|
self.currentMastodonUser.value = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.domain.value = activeMastodonAuthentication.domain
|
||||||
|
self.currentMastodonUser.value = activeMastodonAuthentication.user
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileViewModel {
|
||||||
|
|
||||||
|
enum Friendship: CustomDebugStringConvertible {
|
||||||
|
case following
|
||||||
|
case pending
|
||||||
|
case none
|
||||||
|
|
||||||
|
var debugDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .following: return "following"
|
||||||
|
case .pending: return "pending"
|
||||||
|
case .none: return "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileViewModel {
|
||||||
|
private func setup() {
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
mastodonUser.eraseToAnyPublisher(),
|
||||||
|
currentMastodonUser.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] mastodonUser, currentMastodonUser in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.update(mastodonUser: mastodonUser)
|
||||||
|
self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
|
||||||
|
|
||||||
|
if let mastodonUser = mastodonUser {
|
||||||
|
// setup observer
|
||||||
|
self.mastodonUserObserver = ManagedObjectObserver.observe(object: mastodonUser)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] change in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let changeType = change.changeType else { return }
|
||||||
|
switch changeType {
|
||||||
|
case .update:
|
||||||
|
self.update(mastodonUser: mastodonUser)
|
||||||
|
self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
|
||||||
|
case .delete:
|
||||||
|
// TODO:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self.mastodonUserObserver = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let currentMastodonUser = currentMastodonUser {
|
||||||
|
// setup observer
|
||||||
|
self.currentMastodonUserObserver = ManagedObjectObserver.observe(object: currentMastodonUser)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] change in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let changeType = change.changeType else { return }
|
||||||
|
switch changeType {
|
||||||
|
case .update:
|
||||||
|
self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
|
||||||
|
case .delete:
|
||||||
|
// TODO:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.currentMastodonUserObserver = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(mastodonUser: MastodonUser?) {
|
||||||
|
self.userID.value = mastodonUser?.id
|
||||||
|
self.bannerImageURL.value = mastodonUser?.headerImageURL()
|
||||||
|
self.avatarImageURL.value = mastodonUser?.avatarImageURL()
|
||||||
|
// self.protected.value = twitterUser?.protected
|
||||||
|
self.name.value = mastodonUser?.displayNameWithFallback
|
||||||
|
self.username.value = mastodonUser?.acctWithDomain
|
||||||
|
self.bioDescription.value = mastodonUser?.note
|
||||||
|
self.url.value = mastodonUser?.url
|
||||||
|
self.statusesCount.value = mastodonUser.flatMap { Int(truncating: $0.statusesCount) }
|
||||||
|
self.followingCount.value = mastodonUser.flatMap { Int(truncating: $0.followingCount) }
|
||||||
|
self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
//
|
||||||
|
// ProfilePagingViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Pageboy
|
||||||
|
import Tabman
|
||||||
|
|
||||||
|
protocol ProfilePagingViewControllerDelegate: class {
|
||||||
|
func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ProfilePagingViewController: TabmanViewController {
|
||||||
|
|
||||||
|
weak var pagingDelegate: ProfilePagingViewControllerDelegate?
|
||||||
|
var viewModel: ProfilePagingViewModel!
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - PageboyViewControllerDelegate
|
||||||
|
|
||||||
|
override func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: TabmanViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) {
|
||||||
|
super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated)
|
||||||
|
|
||||||
|
let viewController = viewModel.viewControllers[index]
|
||||||
|
pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfilePagingViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
dataSource = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
//
|
||||||
|
// ProfilePagingViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Pageboy
|
||||||
|
import Tabman
|
||||||
|
|
||||||
|
final class ProfilePagingViewModel: NSObject {
|
||||||
|
|
||||||
|
let postUserTimelineViewController = UserTimelineViewController()
|
||||||
|
let repliesUserTimelineViewController = UserTimelineViewController()
|
||||||
|
let mediaUserTimelineViewController = UserTimelineViewController()
|
||||||
|
|
||||||
|
init(
|
||||||
|
postsUserTimelineViewModel: UserTimelineViewModel,
|
||||||
|
repliesUserTimelineViewModel: UserTimelineViewModel,
|
||||||
|
mediaUserTimelineViewModel: UserTimelineViewModel
|
||||||
|
) {
|
||||||
|
postUserTimelineViewController.viewModel = postsUserTimelineViewModel
|
||||||
|
repliesUserTimelineViewController.viewModel = repliesUserTimelineViewModel
|
||||||
|
mediaUserTimelineViewController.viewModel = mediaUserTimelineViewModel
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewControllers: [ScrollViewContainer] {
|
||||||
|
return [
|
||||||
|
postUserTimelineViewController,
|
||||||
|
repliesUserTimelineViewController,
|
||||||
|
mediaUserTimelineViewController,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
let barItems: [TMBarItemable] = {
|
||||||
|
let items = [
|
||||||
|
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.posts),
|
||||||
|
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.replies),
|
||||||
|
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.media),
|
||||||
|
]
|
||||||
|
return items
|
||||||
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - PageboyViewControllerDataSource
|
||||||
|
extension ProfilePagingViewModel: PageboyViewControllerDataSource {
|
||||||
|
|
||||||
|
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
|
||||||
|
return viewControllers.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
|
||||||
|
return viewControllers[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
|
||||||
|
return .first
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
//
|
||||||
|
// ProfileSegmentedViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ProfileSegmentedViewController: UIViewController {
|
||||||
|
let pagingViewController = ProfilePagingViewController()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileSegmentedViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
addChild(pagingViewController)
|
||||||
|
pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(pagingViewController.view)
|
||||||
|
pagingViewController.didMove(toParent: self)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
pagingViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
pagingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
view.trailingAnchor.constraint(equalTo: pagingViewController.view.trailingAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: pagingViewController.view.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
//
|
||||||
|
// UserTimelineViewController+StatusProvider.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
// MARK: - StatusProvider
|
||||||
|
extension UserTimelineViewController: StatusProvider {
|
||||||
|
|
||||||
|
func status() -> Future<Status?, Never> {
|
||||||
|
return Future { promise in promise(.success(nil)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||||
|
return Future { promise in
|
||||||
|
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||||
|
assertionFailure()
|
||||||
|
promise(.success(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
|
||||||
|
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
promise(.success(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .status(let objectID, _):
|
||||||
|
let managedObjectContext = self.viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext
|
||||||
|
managedObjectContext.perform {
|
||||||
|
let status = managedObjectContext.object(with: objectID) as? Status
|
||||||
|
promise(.success(status))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
promise(.success(nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func status(for cell: UICollectionViewCell) -> Future<Status?, Never> {
|
||||||
|
return Future { promise in promise(.success(nil)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var managedObjectContext: NSManagedObjectContext {
|
||||||
|
return viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
|
||||||
|
return viewModel.diffableDataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? {
|
||||||
|
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||||
|
assertionFailure()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
|
||||||
|
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func items(indexPaths: [IndexPath]) -> [Item] {
|
||||||
|
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||||
|
assertionFailure()
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
var items: [Item] = []
|
||||||
|
for indexPath in indexPaths {
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue }
|
||||||
|
items.append(item)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
//
|
||||||
|
// UserTimelineViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import AVKit
|
||||||
|
import Combine
|
||||||
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
|
|
||||||
|
// TODO: adopt MediaPreviewableViewController
|
||||||
|
final class UserTimelineViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var viewModel: UserTimelineViewModel!
|
||||||
|
|
||||||
|
// let mediaPreviewTransitionController = MediaPreviewTransitionController()
|
||||||
|
|
||||||
|
lazy var tableView: UITableView = {
|
||||||
|
let tableView = UITableView()
|
||||||
|
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
|
||||||
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UserTimelineViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(tableView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
tableView.delegate = self
|
||||||
|
viewModel.setupDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
dependency: self,
|
||||||
|
statusTableViewCellDelegate: self
|
||||||
|
)
|
||||||
|
|
||||||
|
// trigger user timeline loading
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
viewModel.domain.removeDuplicates().eraseToAnyPublisher(),
|
||||||
|
viewModel.userID.removeDuplicates().eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
context.videoPlaybackService.viewDidDisappear(from: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIScrollViewDelegate
|
||||||
|
extension UserTimelineViewController {
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
handleScrollViewDidScroll(scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
extension UserTimelineViewController: UITableViewDelegate {
|
||||||
|
|
||||||
|
// TODO: cache cell height
|
||||||
|
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
return 200
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AVPlayerViewControllerDelegate
|
||||||
|
extension UserTimelineViewController: AVPlayerViewControllerDelegate {
|
||||||
|
|
||||||
|
func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
|
handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
|
handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TimelinePostTableViewCellDelegate
|
||||||
|
extension UserTimelineViewController: StatusTableViewCellDelegate {
|
||||||
|
weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
|
||||||
|
func parent() -> UIViewController { return self }
|
||||||
|
}
|
||||||
|
|
||||||
|
//// MARK: - TimelineHeaderTableViewCellDelegate
|
||||||
|
//extension UserTimelineViewController: TimelineHeaderTableViewCellDelegate { }
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - CustomScrollViewContainerController
|
||||||
|
extension UserTimelineViewController: ScrollViewContainer {
|
||||||
|
var scrollView: UIScrollView { return tableView }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - LoadMoreConfigurableTableViewContainer
|
||||||
|
extension UserTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||||
|
typealias LoadingState = UserTimelineViewModel.State.LoadingMore
|
||||||
|
|
||||||
|
var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||||
|
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
//
|
||||||
|
// UserTimelineViewModel+Diffable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UserTimelineViewModel {
|
||||||
|
|
||||||
|
func setupDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
dependency: NeedsDependency,
|
||||||
|
statusTableViewCellDelegate: StatusTableViewCellDelegate
|
||||||
|
) {
|
||||||
|
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
.share()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
diffableDataSource = StatusSection.tableViewDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
dependency: dependency,
|
||||||
|
managedObjectContext: statusFetchedResultsController.fetchedResultsController.managedObjectContext,
|
||||||
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
|
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// set empty section to make update animation top-to-bottom style
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
diffableDataSource?.apply(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,262 @@
|
||||||
|
//
|
||||||
|
// UserTimelineViewModel+State.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension UserTimelineViewModel {
|
||||||
|
class State: GKState {
|
||||||
|
weak var viewModel: UserTimelineViewModel?
|
||||||
|
|
||||||
|
init(viewModel: UserTimelineViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UserTimelineViewModel.State {
|
||||||
|
class Initial: UserTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
guard let viewModel = viewModel else { return false }
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type:
|
||||||
|
return viewModel.userID.value != nil
|
||||||
|
case is Suspended.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Reloading: UserTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Fail.Type:
|
||||||
|
return true
|
||||||
|
case is Idle.Type:
|
||||||
|
return true
|
||||||
|
case is NoMore.Type:
|
||||||
|
return true
|
||||||
|
case is NotAuthorized.Type, is Blocked.Type:
|
||||||
|
return true
|
||||||
|
case is Suspended.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
// reset
|
||||||
|
viewModel.statusFetchedResultsController.statusIDs.value = []
|
||||||
|
|
||||||
|
guard let userID = viewModel.userID.value, !userID.isEmpty else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let domain = activeMastodonAuthenticationBox.domain
|
||||||
|
let queryFilter = viewModel.queryFilter.value
|
||||||
|
|
||||||
|
viewModel.context.apiService.userTimeline(
|
||||||
|
domain: domain,
|
||||||
|
accountID: userID,
|
||||||
|
maxID: nil,
|
||||||
|
sinceID: nil,
|
||||||
|
excludeReplies: queryFilter.excludeReplies,
|
||||||
|
excludeReblogs: queryFilter.excludeReblogs,
|
||||||
|
onlyMedia: queryFilter.onlyMedia,
|
||||||
|
authorizationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
|
||||||
|
} receiveValue: { response in
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
var hasNewStatusesAppend = false
|
||||||
|
var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value
|
||||||
|
for status in response.value {
|
||||||
|
guard !statusIDs.contains(status.id) else { continue }
|
||||||
|
statusIDs.append(status.id)
|
||||||
|
hasNewStatusesAppend = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasNewStatusesAppend {
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
} else {
|
||||||
|
stateMachine.enter(NoMore.self)
|
||||||
|
}
|
||||||
|
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: UserTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type, is LoadingMore.Type:
|
||||||
|
return true
|
||||||
|
case is Suspended.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: UserTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type, is LoadingMore.Type:
|
||||||
|
return true
|
||||||
|
case is Suspended.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadingMore: UserTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Fail.Type:
|
||||||
|
return true
|
||||||
|
case is Idle.Type:
|
||||||
|
return true
|
||||||
|
case is NoMore.Type:
|
||||||
|
return true
|
||||||
|
case is NotAuthorized.Type, is Blocked.Type:
|
||||||
|
return true
|
||||||
|
case is Suspended.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
guard let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let userID = viewModel.userID.value, !userID.isEmpty else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let domain = activeMastodonAuthenticationBox.domain
|
||||||
|
let queryFilter = viewModel.queryFilter.value
|
||||||
|
|
||||||
|
viewModel.context.apiService.userTimeline(
|
||||||
|
domain: domain,
|
||||||
|
accountID: userID,
|
||||||
|
maxID: maxID,
|
||||||
|
sinceID: nil,
|
||||||
|
excludeReplies: queryFilter.excludeReplies,
|
||||||
|
excludeReblogs: queryFilter.excludeReblogs,
|
||||||
|
onlyMedia: queryFilter.onlyMedia,
|
||||||
|
authorizationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
|
||||||
|
} receiveValue: { response in
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
var hasNewStatusesAppend = false
|
||||||
|
var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value
|
||||||
|
for status in response.value {
|
||||||
|
guard !statusIDs.contains(status.id) else { continue }
|
||||||
|
statusIDs.append(status.id)
|
||||||
|
hasNewStatusesAppend = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasNewStatusesAppend {
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
} else {
|
||||||
|
stateMachine.enter(NoMore.self)
|
||||||
|
}
|
||||||
|
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotAuthorized: UserTimelineViewModel.State {
|
||||||
|
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type:
|
||||||
|
return true
|
||||||
|
case is Suspended.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Blocked: UserTimelineViewModel.State {
|
||||||
|
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type:
|
||||||
|
return true
|
||||||
|
case is Suspended.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Suspended: UserTimelineViewModel.State {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoMore: UserTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type:
|
||||||
|
return true
|
||||||
|
case is NotAuthorized.Type, is Blocked.Type:
|
||||||
|
return true
|
||||||
|
case is Suspended.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
//
|
||||||
|
// UserTimelineViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import GameplayKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
import AlamofireImage
|
||||||
|
|
||||||
|
class UserTimelineViewModel: NSObject {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let domain: CurrentValueSubject<String?, Never>
|
||||||
|
let userID: CurrentValueSubject<String?, Never>
|
||||||
|
let queryFilter: CurrentValueSubject<QueryFilter, Never>
|
||||||
|
let statusFetchedResultsController: StatusFetchedResultsController
|
||||||
|
|
||||||
|
// output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
||||||
|
private(set) lazy var stateMachine: GKStateMachine = {
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
State.Initial(viewModel: self),
|
||||||
|
State.Reloading(viewModel: self),
|
||||||
|
State.Fail(viewModel: self),
|
||||||
|
State.Idle(viewModel: self),
|
||||||
|
State.LoadingMore(viewModel: self),
|
||||||
|
State.NotAuthorized(viewModel: self),
|
||||||
|
State.Blocked(viewModel: self),
|
||||||
|
State.Suspended(viewModel: self),
|
||||||
|
State.NoMore(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(State.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
|
init(context: AppContext, domain: String?, userID: String?, queryFilter: QueryFilter) {
|
||||||
|
self.context = context
|
||||||
|
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
domain: domain,
|
||||||
|
additionalTweetPredicate: Status.notDeleted()
|
||||||
|
)
|
||||||
|
self.domain = CurrentValueSubject(domain)
|
||||||
|
self.userID = CurrentValueSubject(userID)
|
||||||
|
self.queryFilter = CurrentValueSubject(queryFilter)
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.domain
|
||||||
|
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
|
||||||
|
statusFetchedResultsController.objectIDs
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] objectIDs in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
|
// var isPermissionDenied = false
|
||||||
|
|
||||||
|
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
|
||||||
|
let oldSnapshot = diffableDataSource.snapshot()
|
||||||
|
for item in oldSnapshot.itemIdentifiers {
|
||||||
|
guard case let .status(objectID, attribute) = item else { continue }
|
||||||
|
oldSnapshotAttributeDict[objectID] = attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
|
||||||
|
var items: [Item] = []
|
||||||
|
for objectID in objectIDs {
|
||||||
|
let attribute = oldSnapshotAttributeDict[objectID] ?? Item.StatusAttribute()
|
||||||
|
items.append(.status(objectID: objectID, attribute: attribute))
|
||||||
|
}
|
||||||
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
|
||||||
|
if let currentState = self.stateMachine.currentState {
|
||||||
|
switch currentState {
|
||||||
|
case is State.Reloading, is State.LoadingMore, is State.Idle, is State.Fail:
|
||||||
|
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
|
// TODO: handle other states
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not animate when empty items fix loader first appear layout issue
|
||||||
|
diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UserTimelineViewModel {
|
||||||
|
struct QueryFilter {
|
||||||
|
let excludeReplies: Bool?
|
||||||
|
let excludeReblogs: Bool?
|
||||||
|
let onlyMedia: Bool?
|
||||||
|
|
||||||
|
init(
|
||||||
|
excludeReplies: Bool? = nil,
|
||||||
|
excludeReblogs: Bool? = nil,
|
||||||
|
onlyMedia: Bool? = nil
|
||||||
|
) {
|
||||||
|
self.excludeReplies = excludeReplies
|
||||||
|
self.excludeReblogs = excludeReblogs
|
||||||
|
self.onlyMedia = onlyMedia
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,11 +15,11 @@ import MastodonSDK
|
||||||
// MARK: - StatusProvider
|
// MARK: - StatusProvider
|
||||||
extension PublicTimelineViewController: StatusProvider {
|
extension PublicTimelineViewController: StatusProvider {
|
||||||
|
|
||||||
func toot() -> Future<Toot?, Never> {
|
func status() -> Future<Status?, Never> {
|
||||||
return Future { promise in promise(.success(nil)) }
|
return Future { promise in promise(.success(nil)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Toot?, Never> {
|
func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||||
return Future { promise in
|
return Future { promise in
|
||||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
@ -33,11 +33,11 @@ extension PublicTimelineViewController: StatusProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .toot(let objectID, _):
|
case .status(let objectID, _):
|
||||||
let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext
|
let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext
|
||||||
managedObjectContext.perform {
|
managedObjectContext.perform {
|
||||||
let toot = managedObjectContext.object(with: objectID) as? Toot
|
let status = managedObjectContext.object(with: objectID) as? Status
|
||||||
promise(.success(toot))
|
promise(.success(status))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
promise(.success(nil))
|
promise(.success(nil))
|
||||||
|
@ -45,7 +45,7 @@ extension PublicTimelineViewController: StatusProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toot(for cell: UICollectionViewCell) -> Future<Toot?, Never> {
|
func status(for cell: UICollectionViewCell) -> Future<Status?, Never> {
|
||||||
return Future { promise in promise(.success(nil)) }
|
return Future { promise in promise(.success(nil)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -159,13 +159,13 @@ extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
|
||||||
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
||||||
extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
||||||
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String?, timelineIndexobjectID: NSManagedObjectID?) {
|
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineStatusID: String?, timelineIndexobjectID: NSManagedObjectID?) {
|
||||||
guard let upperTimelineTootID = upperTimelineTootID else {return}
|
guard let upperTimelineStatusID = upperTimelineStatusID else {return}
|
||||||
viewModel.loadMiddleSateMachineList
|
viewModel.loadMiddleSateMachineList
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] ids in
|
.sink { [weak self] ids in
|
||||||
guard let _ = self else { return }
|
guard let _ = self else { return }
|
||||||
if let stateMachine = ids[upperTimelineTootID] {
|
if let stateMachine = ids[upperTimelineStatusID] {
|
||||||
guard let state = stateMachine.currentState else {
|
guard let state = stateMachine.currentState else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
|
@ -185,17 +185,17 @@ extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegat
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
var dict = viewModel.loadMiddleSateMachineList.value
|
var dict = viewModel.loadMiddleSateMachineList.value
|
||||||
if let _ = dict[upperTimelineTootID] {
|
if let _ = dict[upperTimelineStatusID] {
|
||||||
// do nothing
|
// do nothing
|
||||||
} else {
|
} else {
|
||||||
let stateMachine = GKStateMachine(states: [
|
let stateMachine = GKStateMachine(states: [
|
||||||
PublicTimelineViewModel.LoadMiddleState.Initial(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
|
PublicTimelineViewModel.LoadMiddleState.Initial(viewModel: viewModel, upperTimelineStatusID: upperTimelineStatusID),
|
||||||
PublicTimelineViewModel.LoadMiddleState.Loading(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
|
PublicTimelineViewModel.LoadMiddleState.Loading(viewModel: viewModel, upperTimelineStatusID: upperTimelineStatusID),
|
||||||
PublicTimelineViewModel.LoadMiddleState.Fail(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
|
PublicTimelineViewModel.LoadMiddleState.Fail(viewModel: viewModel, upperTimelineStatusID: upperTimelineStatusID),
|
||||||
PublicTimelineViewModel.LoadMiddleState.Success(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
|
PublicTimelineViewModel.LoadMiddleState.Success(viewModel: viewModel, upperTimelineStatusID: upperTimelineStatusID),
|
||||||
])
|
])
|
||||||
stateMachine.enter(PublicTimelineViewModel.LoadMiddleState.Initial.self)
|
stateMachine.enter(PublicTimelineViewModel.LoadMiddleState.Initial.self)
|
||||||
dict[upperTimelineTootID] = stateMachine
|
dict[upperTimelineStatusID] = stateMachine
|
||||||
viewModel.loadMiddleSateMachineList.value = dict
|
viewModel.loadMiddleSateMachineList.value = dict
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,32 +41,32 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
|
||||||
let indexes = tootIDs.value
|
let indexes = statusIDs.value
|
||||||
let toots = fetchedResultsController.fetchedObjects ?? []
|
let statuses = fetchedResultsController.fetchedObjects ?? []
|
||||||
guard toots.count == indexes.count else { return }
|
guard statuses.count == indexes.count else { return }
|
||||||
let indexTootTuples: [(Int, Toot)] = toots
|
let indexStatusTuples: [(Int, Status)] = statuses
|
||||||
.compactMap { toot -> (Int, Toot)? in
|
.compactMap { status -> (Int, Status)? in
|
||||||
guard toot.deletedAt == nil else { return nil }
|
guard status.deletedAt == nil else { return nil }
|
||||||
return indexes.firstIndex(of: toot.id).map { index in (index, toot) }
|
return indexes.firstIndex(of: status.id).map { index in (index, status) }
|
||||||
}
|
}
|
||||||
.sorted { $0.0 < $1.0 }
|
.sorted { $0.0 < $1.0 }
|
||||||
var oldSnapshotAttributeDict: [NSManagedObjectID: Item.StatusAttribute] = [:]
|
var oldSnapshotAttributeDict: [NSManagedObjectID: Item.StatusAttribute] = [:]
|
||||||
for item in self.items.value {
|
for item in self.items.value {
|
||||||
guard case let .toot(objectID, attribute) = item else { continue }
|
guard case let .status(objectID, attribute) = item else { continue }
|
||||||
oldSnapshotAttributeDict[objectID] = attribute
|
oldSnapshotAttributeDict[objectID] = attribute
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = [Item]()
|
var items = [Item]()
|
||||||
for (_, toot) in indexTootTuples {
|
for (_, status) in indexStatusTuples {
|
||||||
let targetToot = toot.reblog ?? toot
|
let targetStatus = status.reblog ?? status
|
||||||
let isStatusTextSensitive: Bool = {
|
let isStatusTextSensitive: Bool = {
|
||||||
guard let spoilerText = targetToot.spoilerText, !spoilerText.isEmpty else { return false }
|
guard let spoilerText = targetStatus.spoilerText, !spoilerText.isEmpty else { return false }
|
||||||
return true
|
return true
|
||||||
}()
|
}()
|
||||||
let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: targetToot.sensitive)
|
let attribute = oldSnapshotAttributeDict[status.objectID] ?? Item.StatusAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: targetStatus.sensitive)
|
||||||
items.append(Item.toot(objectID: toot.objectID, attribute: attribute))
|
items.append(Item.status(objectID: status.objectID, attribute: attribute))
|
||||||
if tootIDsWhichHasGap.contains(toot.id) {
|
if statusIDsWhichHasGap.contains(status.id) {
|
||||||
items.append(Item.publicMiddleLoader(tootID: toot.id))
|
items.append(Item.publicMiddleLoader(statusID: status.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,18 +14,18 @@ import os.log
|
||||||
extension PublicTimelineViewModel {
|
extension PublicTimelineViewModel {
|
||||||
class LoadMiddleState: GKState {
|
class LoadMiddleState: GKState {
|
||||||
weak var viewModel: PublicTimelineViewModel?
|
weak var viewModel: PublicTimelineViewModel?
|
||||||
let upperTimelineTootID: String
|
let upperTimelineStatusID: String
|
||||||
|
|
||||||
init(viewModel: PublicTimelineViewModel, upperTimelineTootID: String) {
|
init(viewModel: PublicTimelineViewModel, upperTimelineStatusID: String) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.upperTimelineTootID = upperTimelineTootID
|
self.upperTimelineStatusID = upperTimelineStatusID
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, self.debugDescription, previousState.debugDescription)
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
var dict = viewModel.loadMiddleSateMachineList.value
|
var dict = viewModel.loadMiddleSateMachineList.value
|
||||||
dict[self.upperTimelineTootID] = stateMachine
|
dict[self.upperTimelineStatusID] = stateMachine
|
||||||
viewModel.loadMiddleSateMachineList.value = dict // trigger value change
|
viewModel.loadMiddleSateMachineList.value = dict // trigger value change
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,42 +54,42 @@ extension PublicTimelineViewModel.LoadMiddleState {
|
||||||
}
|
}
|
||||||
viewModel.context.apiService.publicTimeline(
|
viewModel.context.apiService.publicTimeline(
|
||||||
domain: activeMastodonAuthenticationBox.domain,
|
domain: activeMastodonAuthenticationBox.domain,
|
||||||
maxID: upperTimelineTootID
|
maxID: upperTimelineStatusID
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
os_log("%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
let toots = response.value
|
let statuses = response.value
|
||||||
let addedToots = toots.filter { !viewModel.tootIDs.value.contains($0.id) }
|
let addedStatuses = statuses.filter { !viewModel.statusIDs.value.contains($0.id) }
|
||||||
|
|
||||||
guard let gapIndex = viewModel.tootIDs.value.firstIndex(of: self.upperTimelineTootID) else { return }
|
guard let gapIndex = viewModel.statusIDs.value.firstIndex(of: self.upperTimelineStatusID) else { return }
|
||||||
let upToots = Array(viewModel.tootIDs.value[...gapIndex])
|
let upStatuses = Array(viewModel.statusIDs.value[...gapIndex])
|
||||||
let downToots = Array(viewModel.tootIDs.value[(gapIndex + 1)...])
|
let downStatuses = Array(viewModel.statusIDs.value[(gapIndex + 1)...])
|
||||||
|
|
||||||
// construct newTootIDs
|
// construct newStatusIDs
|
||||||
var newTootIDs = upToots
|
var newStatusIDs = upStatuses
|
||||||
newTootIDs.append(contentsOf: addedToots.map { $0.id })
|
newStatusIDs.append(contentsOf: addedStatuses.map { $0.id })
|
||||||
newTootIDs.append(contentsOf: downToots)
|
newStatusIDs.append(contentsOf: downStatuses)
|
||||||
// remove old gap from viewmodel
|
// remove old gap from viewmodel
|
||||||
if let index = viewModel.tootIDsWhichHasGap.firstIndex(of: self.upperTimelineTootID) {
|
if let index = viewModel.statusIDsWhichHasGap.firstIndex(of: self.upperTimelineStatusID) {
|
||||||
viewModel.tootIDsWhichHasGap.remove(at: index)
|
viewModel.statusIDsWhichHasGap.remove(at: index)
|
||||||
}
|
}
|
||||||
// add new gap from viewmodel if need
|
// add new gap from viewmodel if need
|
||||||
let intersection = toots.filter { downToots.contains($0.id) }
|
let intersection = statuses.filter { downStatuses.contains($0.id) }
|
||||||
if intersection.isEmpty {
|
if intersection.isEmpty {
|
||||||
addedToots.last.flatMap { viewModel.tootIDsWhichHasGap.append($0.id) }
|
addedStatuses.last.flatMap { viewModel.statusIDsWhichHasGap.append($0.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.tootIDs.value = newTootIDs
|
viewModel.statusIDs.value = newStatusIDs
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld toots, %{public}%ld new toots", (#file as NSString).lastPathComponent, #line, #function, toots.count, addedToots.count)
|
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld statuses, %{public}%ld new statues", (#file as NSString).lastPathComponent, #line, #function, statuses.count, addedStatuses.count)
|
||||||
if addedToots.isEmpty {
|
if addedStatuses.isEmpty {
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
} else {
|
} else {
|
||||||
stateMachine.enter(Success.self)
|
stateMachine.enter(Success.self)
|
||||||
|
|
|
@ -68,21 +68,21 @@ extension PublicTimelineViewModel.State {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
let resposeTootIDs = response.value.compactMap { $0.id }
|
let resposeStatusIDs = response.value.compactMap { $0.id }
|
||||||
var newTootsIDs = resposeTootIDs
|
var newStatusIDs = resposeStatusIDs
|
||||||
let oldTootsIDs = viewModel.tootIDs.value
|
let oldStatusIDs = viewModel.statusIDs.value
|
||||||
var hasGap = true
|
var hasGap = true
|
||||||
for tootID in oldTootsIDs {
|
for statusID in oldStatusIDs {
|
||||||
if !newTootsIDs.contains(tootID) {
|
if !newStatusIDs.contains(statusID) {
|
||||||
newTootsIDs.append(tootID)
|
newStatusIDs.append(statusID)
|
||||||
} else {
|
} else {
|
||||||
hasGap = false
|
hasGap = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasGap && oldTootsIDs.count > 0 {
|
if hasGap && oldStatusIDs.count > 0 {
|
||||||
resposeTootIDs.last.flatMap { viewModel.tootIDsWhichHasGap.append($0) }
|
resposeStatusIDs.last.flatMap { viewModel.statusIDsWhichHasGap.append($0) }
|
||||||
}
|
}
|
||||||
viewModel.tootIDs.value = newTootsIDs
|
viewModel.statusIDs.value = newStatusIDs
|
||||||
stateMachine.enter(Idle.self)
|
stateMachine.enter(Idle.self)
|
||||||
}
|
}
|
||||||
.store(in: &viewModel.disposeBag)
|
.store(in: &viewModel.disposeBag)
|
||||||
|
@ -138,7 +138,7 @@ extension PublicTimelineViewModel.State {
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let maxID = viewModel.tootIDs.value.last
|
let maxID = viewModel.statusIDs.value.last
|
||||||
viewModel.context.apiService.publicTimeline(
|
viewModel.context.apiService.publicTimeline(
|
||||||
domain: activeMastodonAuthenticationBox.domain,
|
domain: activeMastodonAuthenticationBox.domain,
|
||||||
maxID: maxID
|
maxID: maxID
|
||||||
|
@ -153,14 +153,14 @@ extension PublicTimelineViewModel.State {
|
||||||
}
|
}
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
stateMachine.enter(Idle.self)
|
stateMachine.enter(Idle.self)
|
||||||
var oldTootsIDs = viewModel.tootIDs.value
|
var oldStatusIDs = viewModel.statusIDs.value
|
||||||
for toot in response.value {
|
for status in response.value {
|
||||||
if !oldTootsIDs.contains(toot.id) {
|
if !oldStatusIDs.contains(status.id) {
|
||||||
oldTootsIDs.append(toot.id)
|
oldStatusIDs.append(status.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.tootIDs.value = oldTootsIDs
|
viewModel.statusIDs.value = oldStatusIDs
|
||||||
}
|
}
|
||||||
.store(in: &viewModel.disposeBag)
|
.store(in: &viewModel.disposeBag)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ class PublicTimelineViewModel: NSObject {
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let fetchedResultsController: NSFetchedResultsController<Toot>
|
let fetchedResultsController: NSFetchedResultsController<Status>
|
||||||
|
|
||||||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class PublicTimelineViewModel: NSObject {
|
||||||
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||||
|
|
||||||
//
|
//
|
||||||
var tootIDsWhichHasGap = [String]()
|
var statusIDsWhichHasGap = [String]()
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
||||||
|
|
||||||
|
@ -47,15 +47,15 @@ class PublicTimelineViewModel: NSObject {
|
||||||
return stateMachine
|
return stateMachine
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let tootIDs = CurrentValueSubject<[String], Never>([])
|
let statusIDs = CurrentValueSubject<[String], Never>([])
|
||||||
let items = CurrentValueSubject<[Item], Never>([])
|
let items = CurrentValueSubject<[Item], Never>([])
|
||||||
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||||
|
|
||||||
init(context: AppContext) {
|
init(context: AppContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.fetchedResultsController = {
|
self.fetchedResultsController = {
|
||||||
let fetchRequest = Toot.sortedFetchRequest
|
let fetchRequest = Status.sortedFetchRequest
|
||||||
fetchRequest.predicate = Toot.predicate(domain: "", ids: [])
|
fetchRequest.predicate = Status.predicate(domain: "", ids: [])
|
||||||
fetchRequest.returnsObjectsAsFaults = false
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
fetchRequest.fetchBatchSize = 20
|
fetchRequest.fetchBatchSize = 20
|
||||||
let controller = NSFetchedResultsController(
|
let controller = NSFetchedResultsController(
|
||||||
|
@ -111,12 +111,12 @@ class PublicTimelineViewModel: NSObject {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
tootIDs
|
statusIDs
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] ids in
|
.sink { [weak self] ids in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let domain = self.context.authenticationService.activeMastodonAuthenticationBox.value?.domain ?? ""
|
let domain = self.context.authenticationService.activeMastodonAuthenticationBox.value?.domain ?? ""
|
||||||
self.fetchedResultsController.fetchRequest.predicate = Toot.predicate(domain: domain, ids: ids)
|
self.fetchedResultsController.fetchRequest.predicate = Status.predicate(domain: domain, ids: ids)
|
||||||
do {
|
do {
|
||||||
try self.fetchedResultsController.performFetch()
|
try self.fetchedResultsController.performFetch()
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// AdaptiveStatusBarStyleNavigationController.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-2-26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
// Make status bar style adptive for child view controller
|
||||||
|
// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
|
||||||
|
final class AdaptiveStatusBarStyleNavigationController: UINavigationController {
|
||||||
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
|
return visibleViewController
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
//
|
|
||||||
// DarkContentStatusBarStyleNavigationController.swift
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021-2-26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
final class DarkContentStatusBarStyleNavigationController: UINavigationController {
|
|
||||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
||||||
return .darkContent
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class RoundedEdgesButton: UIButton {
|
class RoundedEdgesButton: UIButton {
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
|
@ -12,6 +12,8 @@ import ActiveLabel
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
|
|
||||||
protocol StatusViewDelegate: class {
|
protocol StatusViewDelegate: class {
|
||||||
|
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
|
||||||
|
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton)
|
||||||
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||||
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||||
|
@ -196,9 +198,10 @@ final class StatusView: UIView {
|
||||||
return actionToolbarContainer
|
return actionToolbarContainer
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
||||||
let activeTextLabel = ActiveLabel(style: .default)
|
let activeTextLabel = ActiveLabel(style: .default)
|
||||||
|
|
||||||
|
private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
|
@ -227,7 +230,7 @@ final class StatusView: UIView {
|
||||||
extension StatusView {
|
extension StatusView {
|
||||||
|
|
||||||
func _init() {
|
func _init() {
|
||||||
// container: [retoot | author | status | action toolbar]
|
// container: [reblog | author | status | action toolbar]
|
||||||
let containerStackView = UIStackView()
|
let containerStackView = UIStackView()
|
||||||
containerStackView.axis = .vertical
|
containerStackView.axis = .vertical
|
||||||
containerStackView.spacing = 10
|
containerStackView.spacing = 10
|
||||||
|
@ -403,6 +406,12 @@ extension StatusView {
|
||||||
playerContainerView.delegate = self
|
playerContainerView.delegate = self
|
||||||
activeTextLabel.delegate = self
|
activeTextLabel.delegate = self
|
||||||
|
|
||||||
|
headerInfoLabelTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerInfoLabelTapGestureRecognizerHandler(_:)))
|
||||||
|
headerInfoLabel.isUserInteractionEnabled = true
|
||||||
|
headerInfoLabel.addGestureRecognizer(headerInfoLabelTapGestureRecognizer)
|
||||||
|
|
||||||
|
avatarButton.addTarget(self, action: #selector(StatusView.avatarButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside)
|
contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside)
|
||||||
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
|
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
@ -441,6 +450,21 @@ extension StatusView {
|
||||||
|
|
||||||
extension StatusView {
|
extension StatusView {
|
||||||
|
|
||||||
|
@objc private func headerInfoLabelTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.statusView(self, headerInfoLabelDidPressed: headerInfoLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func avatarButtonDidPressed(_ sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.statusView(self, avatarButtonDidPressed: sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func avatarStackedContainerButtonDidPressed(_ sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.statusView(self, avatarButtonDidPressed: sender)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func contentWarningActionButtonPressed(_ sender: UIButton) {
|
@objc private func contentWarningActionButtonPressed(_ sender: UIButton) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
delegate?.statusView(self, contentWarningActionButtonPressed: sender)
|
delegate?.statusView(self, contentWarningActionButtonPressed: sender)
|
||||||
|
|
|
@ -21,6 +21,8 @@ protocol StatusTableViewCellDelegate: class {
|
||||||
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get }
|
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get }
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
||||||
|
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarButtonDidPressed button: UIButton)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
||||||
|
@ -197,6 +199,14 @@ extension StatusTableViewCell: UITableViewDelegate {
|
||||||
// MARK: - StatusViewDelegate
|
// MARK: - StatusViewDelegate
|
||||||
extension StatusTableViewCell: StatusViewDelegate {
|
extension StatusTableViewCell: StatusViewDelegate {
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) {
|
||||||
|
delegate?.statusTableViewCell(self, statusView: statusView, headerInfoLabelDidPressed: label)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) {
|
||||||
|
delegate?.statusTableViewCell(self, statusView: statusView, avatarButtonDidPressed: button)
|
||||||
|
}
|
||||||
|
|
||||||
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) {
|
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) {
|
||||||
delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button)
|
delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol TimelineMiddleLoaderTableViewCellDelegate: class {
|
protocol TimelineMiddleLoaderTableViewCellDelegate: class {
|
||||||
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String?, timelineIndexobjectID:NSManagedObjectID?)
|
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineStatusID: String?, timelineIndexobjectID:NSManagedObjectID?)
|
||||||
func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton)
|
func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
//
|
||||||
|
// WebViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by xiaojian sun on 2021/3/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
final class WebViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var viewModel: WebViewModel!
|
||||||
|
|
||||||
|
let webView: WKWebView = {
|
||||||
|
let configuration = WKWebViewConfiguration()
|
||||||
|
configuration.processPool = WKProcessPool()
|
||||||
|
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||||
|
return webView
|
||||||
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
|
||||||
|
// cleanup cookie
|
||||||
|
let httpCookieStore = webView.configuration.websiteDataStore.httpCookieStore
|
||||||
|
httpCookieStore.getAllCookies { cookies in
|
||||||
|
for cookie in cookies {
|
||||||
|
httpCookieStore.delete(cookie, completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WebViewController {
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(WebViewController.cancelBarButtonItemPressed(_:)))
|
||||||
|
|
||||||
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(webView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
webView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let request = URLRequest(url: viewModel.url)
|
||||||
|
webView.load(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WebViewController {
|
||||||
|
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
|
dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
// WebViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by xiaojian sun on 2021/3/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class WebViewModel {
|
||||||
|
public init(url: URL) {
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
// input
|
||||||
|
let url: URL
|
||||||
|
}
|
|
@ -17,29 +17,29 @@ extension APIService {
|
||||||
|
|
||||||
// make local state change only
|
// make local state change only
|
||||||
func like(
|
func like(
|
||||||
tootObjectID: NSManagedObjectID,
|
statusObjectID: NSManagedObjectID,
|
||||||
mastodonUserObjectID: NSManagedObjectID,
|
mastodonUserObjectID: NSManagedObjectID,
|
||||||
favoriteKind: Mastodon.API.Favorites.FavoriteKind
|
favoriteKind: Mastodon.API.Favorites.FavoriteKind
|
||||||
) -> AnyPublisher<Toot.ID, Error> {
|
) -> AnyPublisher<Status.ID, Error> {
|
||||||
var _targetTootID: Toot.ID?
|
var _targetStatusID: Status.ID?
|
||||||
let managedObjectContext = backgroundManagedObjectContext
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
return managedObjectContext.performChanges {
|
return managedObjectContext.performChanges {
|
||||||
let toot = managedObjectContext.object(with: tootObjectID) as! Toot
|
let status = managedObjectContext.object(with: statusObjectID) as! Status
|
||||||
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||||
let targetToot = toot.reblog ?? toot
|
let targetStatus = status.reblog ?? status
|
||||||
let targetTootID = targetToot.id
|
let targetStatusID = targetStatus.id
|
||||||
_targetTootID = targetTootID
|
_targetStatusID = targetStatusID
|
||||||
|
|
||||||
targetToot.update(liked: favoriteKind == .create, mastodonUser: mastodonUser)
|
targetStatus.update(liked: favoriteKind == .create, by: mastodonUser)
|
||||||
|
|
||||||
}
|
}
|
||||||
.tryMap { result in
|
.tryMap { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
guard let targetTootID = _targetTootID else {
|
guard let targetStatusID = _targetStatusID else {
|
||||||
throw APIError.implicit(.badRequest)
|
throw APIError.implicit(.badRequest)
|
||||||
}
|
}
|
||||||
return targetTootID
|
return targetStatusID
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
assertionFailure(error.localizedDescription)
|
assertionFailure(error.localizedDescription)
|
||||||
|
@ -76,12 +76,12 @@ extension APIService {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
let _oldToot: Toot? = {
|
let _oldStatus: Status? = {
|
||||||
let request = Toot.sortedFetchRequest
|
let request = Status.sortedFetchRequest
|
||||||
request.predicate = Toot.predicate(domain: mastodonAuthenticationBox.domain, id: statusID)
|
request.predicate = Status.predicate(domain: mastodonAuthenticationBox.domain, id: statusID)
|
||||||
request.fetchLimit = 1
|
request.fetchLimit = 1
|
||||||
request.returnsObjectsAsFaults = false
|
request.returnsObjectsAsFaults = false
|
||||||
request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)]
|
request.relationshipKeyPathsForPrefetching = [#keyPath(Status.reblog)]
|
||||||
do {
|
do {
|
||||||
return try managedObjectContext.fetch(request).first
|
return try managedObjectContext.fetch(request).first
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -91,15 +91,15 @@ extension APIService {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
guard let requestMastodonUser = _requestMastodonUser,
|
guard let requestMastodonUser = _requestMastodonUser,
|
||||||
let oldToot = _oldToot else {
|
let oldStatus = _oldStatus else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
|
APIService.CoreData.merge(status: oldStatus, entity: entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
|
||||||
if favoriteKind == .destroy {
|
if favoriteKind == .destroy {
|
||||||
oldToot.update(favouritesCount: NSNumber(value: max(0, oldToot.favouritesCount.intValue - 1)))
|
oldStatus.update(favouritesCount: NSNumber(value: max(0, oldStatus.favouritesCount.intValue - 1)))
|
||||||
}
|
}
|
||||||
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "<nil>", entity.favouritesCount )
|
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update status %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "<nil>", entity.favouritesCount )
|
||||||
}
|
}
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
||||||
|
@ -129,7 +129,7 @@ extension APIService {
|
||||||
|
|
||||||
extension APIService {
|
extension APIService {
|
||||||
func likeList(
|
func likeList(
|
||||||
limit: Int = onceRequestTootMaxCount,
|
limit: Int = onceRequestStatusMaxCount,
|
||||||
userID: String,
|
userID: String,
|
||||||
maxID: String? = nil,
|
maxID: String? = nil,
|
||||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
|
|
@ -19,7 +19,7 @@ extension APIService {
|
||||||
domain: String,
|
domain: String,
|
||||||
sinceID: Mastodon.Entity.Status.ID? = nil,
|
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||||
maxID: Mastodon.Entity.Status.ID? = nil,
|
maxID: Mastodon.Entity.Status.ID? = nil,
|
||||||
limit: Int = onceRequestTootMaxCount,
|
limit: Int = onceRequestStatusMaxCount,
|
||||||
local: Bool? = nil,
|
local: Bool? = nil,
|
||||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||||
|
|
|
@ -21,7 +21,7 @@ extension APIService {
|
||||||
domain: String,
|
domain: String,
|
||||||
sinceID: Mastodon.Entity.Status.ID? = nil,
|
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||||
maxID: Mastodon.Entity.Status.ID? = nil,
|
maxID: Mastodon.Entity.Status.ID? = nil,
|
||||||
limit: Int = onceRequestTootMaxCount
|
limit: Int = onceRequestStatusMaxCount
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||||
let query = Mastodon.API.Timeline.PublicTimelineQuery(
|
let query = Mastodon.API.Timeline.PublicTimelineQuery(
|
||||||
local: nil,
|
local: nil,
|
||||||
|
|
|
@ -16,34 +16,34 @@ extension APIService {
|
||||||
|
|
||||||
// make local state change only
|
// make local state change only
|
||||||
func reblog(
|
func reblog(
|
||||||
tootObjectID: NSManagedObjectID,
|
statusObjectID: NSManagedObjectID,
|
||||||
mastodonUserObjectID: NSManagedObjectID,
|
mastodonUserObjectID: NSManagedObjectID,
|
||||||
reblogKind: Mastodon.API.Reblog.ReblogKind
|
reblogKind: Mastodon.API.Reblog.ReblogKind
|
||||||
) -> AnyPublisher<Toot.ID, Error> {
|
) -> AnyPublisher<Status.ID, Error> {
|
||||||
var _targetTootID: Toot.ID?
|
var _targetStatusID: Status.ID?
|
||||||
let managedObjectContext = backgroundManagedObjectContext
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
return managedObjectContext.performChanges {
|
return managedObjectContext.performChanges {
|
||||||
let toot = managedObjectContext.object(with: tootObjectID) as! Toot
|
let status = managedObjectContext.object(with: statusObjectID) as! Status
|
||||||
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||||
let targetToot = toot.reblog ?? toot
|
let targetStatus = status.reblog ?? status
|
||||||
let targetTootID = targetToot.id
|
let targetStatusID = targetStatus.id
|
||||||
_targetTootID = targetTootID
|
_targetStatusID = targetStatusID
|
||||||
|
|
||||||
switch reblogKind {
|
switch reblogKind {
|
||||||
case .reblog:
|
case .reblog:
|
||||||
targetToot.update(reblogged: true, mastodonUser: mastodonUser)
|
targetStatus.update(reblogged: true, by: mastodonUser)
|
||||||
case .undoReblog:
|
case .undoReblog:
|
||||||
targetToot.update(reblogged: false, mastodonUser: mastodonUser)
|
targetStatus.update(reblogged: false, by: mastodonUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.tryMap { result in
|
.tryMap { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
guard let targetTootID = _targetTootID else {
|
guard let targetStatusID = _targetStatusID else {
|
||||||
throw APIError.implicit(.badRequest)
|
throw APIError.implicit(.badRequest)
|
||||||
}
|
}
|
||||||
return targetTootID
|
return targetStatusID
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
assertionFailure(error.localizedDescription)
|
assertionFailure(error.localizedDescription)
|
||||||
|
@ -85,25 +85,25 @@ extension APIService {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let oldToot: Toot = {
|
guard let oldStatus: Status = {
|
||||||
let request = Toot.sortedFetchRequest
|
let request = Status.sortedFetchRequest
|
||||||
request.predicate = Toot.predicate(domain: domain, id: statusID)
|
request.predicate = Status.predicate(domain: domain, id: statusID)
|
||||||
request.fetchLimit = 1
|
request.fetchLimit = 1
|
||||||
request.returnsObjectsAsFaults = false
|
request.returnsObjectsAsFaults = false
|
||||||
request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)]
|
request.relationshipKeyPathsForPrefetching = [#keyPath(Status.reblog)]
|
||||||
return managedObjectContext.safeFetch(request).first
|
return managedObjectContext.safeFetch(request).first
|
||||||
}() else {
|
}() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
APIService.CoreData.merge(toot: oldToot, entity: entity.reblog ?? entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
|
APIService.CoreData.merge(status: oldStatus, entity: entity.reblog ?? entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
|
||||||
switch reblogKind {
|
switch reblogKind {
|
||||||
case .undoReblog:
|
case .undoReblog:
|
||||||
oldToot.update(reblogsCount: NSNumber(value: max(0, oldToot.reblogsCount.intValue - 1)))
|
oldStatus.update(reblogsCount: NSNumber(value: max(0, oldStatus.reblogsCount.intValue - 1)))
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s reblog status to: %{public}s. now %ld reblog", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.reblogged.flatMap { $0 ? "reblog" : "unreblog" } ?? "<nil>", entity.reblogsCount )
|
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update status %{public}s reblog status to: %{public}s. now %ld reblog", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.reblogged.flatMap { $0 ? "reblog" : "unreblog" } ?? "<nil>", entity.reblogsCount )
|
||||||
}
|
}
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
//
|
||||||
|
// APIService+Relationship.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-4-1.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import CommonOSLog
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
func relationship(
|
||||||
|
domain: String,
|
||||||
|
accountIDs: [Mastodon.Entity.Account.ID],
|
||||||
|
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Relationship]>, Error> {
|
||||||
|
fatalError()
|
||||||
|
// let authorization = authorizationBox.userAuthorization
|
||||||
|
// let requestMastodonUserID = authorizationBox.userID
|
||||||
|
// let query = Mastodon.API.Account.AccountStatuseseQuery(
|
||||||
|
// maxID: maxID,
|
||||||
|
// sinceID: sinceID,
|
||||||
|
// excludeReplies: excludeReplies,
|
||||||
|
// excludeReblogs: excludeReblogs,
|
||||||
|
// onlyMedia: onlyMedia,
|
||||||
|
// limit: limit
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// return Mastodon.API.Account.statuses(
|
||||||
|
// session: session,
|
||||||
|
// domain: domain,
|
||||||
|
// accountID: accountID,
|
||||||
|
// query: query,
|
||||||
|
// authorization: authorization
|
||||||
|
// )
|
||||||
|
// .flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> in
|
||||||
|
// return APIService.Persist.persistStatus(
|
||||||
|
// managedObjectContext: self.backgroundManagedObjectContext,
|
||||||
|
// domain: domain,
|
||||||
|
// query: nil,
|
||||||
|
// response: response,
|
||||||
|
// persistType: .user,
|
||||||
|
// requestMastodonUserID: requestMastodonUserID,
|
||||||
|
// log: OSLog.api
|
||||||
|
// )
|
||||||
|
// .setFailureType(to: Error.self)
|
||||||
|
// .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
|
||||||
|
// switch result {
|
||||||
|
// case .success:
|
||||||
|
// return response
|
||||||
|
// case .failure(let error):
|
||||||
|
// throw error
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .eraseToAnyPublisher()
|
||||||
|
// }
|
||||||
|
// .eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
//
|
||||||
|
// APIService+UserTimeline.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import CommonOSLog
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
func userTimeline(
|
||||||
|
domain: String,
|
||||||
|
accountID: String,
|
||||||
|
maxID: Mastodon.Entity.Status.ID? = nil,
|
||||||
|
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||||
|
limit: Int = onceRequestStatusMaxCount,
|
||||||
|
excludeReplies: Bool? = nil,
|
||||||
|
excludeReblogs: Bool? = nil,
|
||||||
|
onlyMedia: Bool? = nil,
|
||||||
|
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||||
|
let authorization = authorizationBox.userAuthorization
|
||||||
|
let requestMastodonUserID = authorizationBox.userID
|
||||||
|
let query = Mastodon.API.Account.AccountStatuseseQuery(
|
||||||
|
maxID: maxID,
|
||||||
|
sinceID: sinceID,
|
||||||
|
excludeReplies: excludeReplies,
|
||||||
|
excludeReblogs: excludeReblogs,
|
||||||
|
onlyMedia: onlyMedia,
|
||||||
|
limit: limit
|
||||||
|
)
|
||||||
|
|
||||||
|
return Mastodon.API.Account.statuses(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
accountID: accountID,
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> in
|
||||||
|
return APIService.Persist.persistStatus(
|
||||||
|
managedObjectContext: self.backgroundManagedObjectContext,
|
||||||
|
domain: domain,
|
||||||
|
query: nil,
|
||||||
|
response: response,
|
||||||
|
persistType: .user,
|
||||||
|
requestMastodonUserID: requestMastodonUserID,
|
||||||
|
log: OSLog.api
|
||||||
|
)
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ final class APIService {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension APIService {
|
extension APIService {
|
||||||
public static let onceRequestTootMaxCount = 100
|
public static let onceRequestStatusMaxCount = 100
|
||||||
public static let onceRequestUserMaxCount = 100
|
public static let onceRequestUserMaxCount = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,11 +48,11 @@ extension APIService.CoreData {
|
||||||
|
|
||||||
if let oldMastodonUser = oldMastodonUser {
|
if let oldMastodonUser = oldMastodonUser {
|
||||||
// merge old mastodon usre
|
// merge old mastodon usre
|
||||||
APIService.CoreData.mergeMastodonUser(
|
APIService.CoreData.merge(
|
||||||
for: requestMastodonUser,
|
user: oldMastodonUser,
|
||||||
old: oldMastodonUser,
|
|
||||||
in: domain,
|
|
||||||
entity: entity,
|
entity: entity,
|
||||||
|
requestMastodonUser: requestMastodonUser,
|
||||||
|
domain: domain,
|
||||||
networkDate: networkDate
|
networkDate: networkDate
|
||||||
)
|
)
|
||||||
return (oldMastodonUser, false)
|
return (oldMastodonUser, false)
|
||||||
|
@ -68,11 +68,15 @@ extension APIService.CoreData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func mergeMastodonUser(
|
}
|
||||||
for requestMastodonUser: MastodonUser?,
|
|
||||||
old user: MastodonUser,
|
extension APIService.CoreData {
|
||||||
in domain: String,
|
|
||||||
|
static func merge(
|
||||||
|
user: MastodonUser,
|
||||||
entity: Mastodon.Entity.Account,
|
entity: Mastodon.Entity.Account,
|
||||||
|
requestMastodonUser: MastodonUser?,
|
||||||
|
domain: String,
|
||||||
networkDate: Date
|
networkDate: Date
|
||||||
) {
|
) {
|
||||||
guard networkDate > user.updatedAt else { return }
|
guard networkDate > user.updatedAt else { return }
|
||||||
|
@ -84,6 +88,38 @@ extension APIService.CoreData {
|
||||||
user.update(displayName: property.displayName)
|
user.update(displayName: property.displayName)
|
||||||
user.update(avatar: property.avatar)
|
user.update(avatar: property.avatar)
|
||||||
user.update(avatarStatic: property.avatarStatic)
|
user.update(avatarStatic: property.avatarStatic)
|
||||||
|
user.update(header: property.header)
|
||||||
|
user.update(headerStatic: property.headerStatic)
|
||||||
|
user.update(note: property.note)
|
||||||
|
user.update(url: property.url)
|
||||||
|
user.update(statusesCount: property.statusesCount)
|
||||||
|
user.update(followingCount: property.followingCount)
|
||||||
|
user.update(followersCount: property.followersCount)
|
||||||
|
|
||||||
|
user.didUpdate(at: networkDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension APIService.CoreData {
|
||||||
|
|
||||||
|
static func update(
|
||||||
|
user: MastodonUser,
|
||||||
|
entity: Mastodon.Entity.Relationship,
|
||||||
|
requestMastodonUser: MastodonUser,
|
||||||
|
domain: String,
|
||||||
|
networkDate: Date
|
||||||
|
) {
|
||||||
|
guard networkDate > user.updatedAt else { return }
|
||||||
|
|
||||||
|
user.update(isFollowing: entity.following, by: requestMastodonUser)
|
||||||
|
entity.requested.flatMap { user.update(isFollowRequested: $0, by: requestMastodonUser) }
|
||||||
|
entity.endorsed.flatMap { user.update(isEndorsed: $0, by: requestMastodonUser) }
|
||||||
|
requestMastodonUser.update(isFollowing: entity.followedBy, by: user)
|
||||||
|
entity.muting.flatMap { user.update(isMuting: $0, by: requestMastodonUser) }
|
||||||
|
user.update(isBlocking: entity.blocking, by: requestMastodonUser)
|
||||||
|
entity.domainBlocking.flatMap { user.update(isDomainBlocking: $0, by: requestMastodonUser) }
|
||||||
|
entity.blockedBy.flatMap { requestMastodonUser.update(isBlocking: $0, by: user) }
|
||||||
|
|
||||||
user.didUpdate(at: networkDate)
|
user.didUpdate(at: networkDate)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,39 +18,39 @@ extension APIService.CoreData {
|
||||||
for requestMastodonUser: MastodonUser?,
|
for requestMastodonUser: MastodonUser?,
|
||||||
domain: String,
|
domain: String,
|
||||||
entity: Mastodon.Entity.Status,
|
entity: Mastodon.Entity.Status,
|
||||||
tootCache: APIService.Persist.PersistCache<Toot>?,
|
statusCache: APIService.Persist.PersistCache<Status>?,
|
||||||
userCache: APIService.Persist.PersistCache<MastodonUser>?,
|
userCache: APIService.Persist.PersistCache<MastodonUser>?,
|
||||||
networkDate: Date,
|
networkDate: Date,
|
||||||
log: OSLog
|
log: OSLog
|
||||||
) -> (Toot: Toot, isTootCreated: Bool, isMastodonUserCreated: Bool) {
|
) -> (status: Status, isStatusCreated: Bool, isMastodonUserCreated: Bool) {
|
||||||
let processEntityTaskSignpostID = OSSignpostID(log: log)
|
let processEntityTaskSignpostID = OSSignpostID(log: log)
|
||||||
os_signpost(.begin, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "process toot %{public}s", entity.id)
|
os_signpost(.begin, log: log, name: "update database - process entity: createOrMergeStatus", signpostID: processEntityTaskSignpostID, "process status %{public}s", entity.id)
|
||||||
defer {
|
defer {
|
||||||
os_signpost(.end, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "process toot %{public}s", entity.id)
|
os_signpost(.end, log: log, name: "update database - process entity: createOrMergeStatus", signpostID: processEntityTaskSignpostID, "process status %{public}s", entity.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// build tree
|
// build tree
|
||||||
let reblog = entity.reblog.flatMap { entity -> Toot in
|
let reblog = entity.reblog.flatMap { entity -> Status in
|
||||||
let (toot, _, _) = createOrMergeStatus(
|
let (status, _, _) = createOrMergeStatus(
|
||||||
into: managedObjectContext,
|
into: managedObjectContext,
|
||||||
for: requestMastodonUser,
|
for: requestMastodonUser,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
entity: entity,
|
entity: entity,
|
||||||
tootCache: tootCache,
|
statusCache: statusCache,
|
||||||
userCache: userCache,
|
userCache: userCache,
|
||||||
networkDate: networkDate,
|
networkDate: networkDate,
|
||||||
log: log
|
log: log
|
||||||
)
|
)
|
||||||
return toot
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch old Toot
|
// fetch old Status
|
||||||
let oldToot: Toot? = {
|
let oldStatus: Status? = {
|
||||||
if let tootCache = tootCache {
|
if let statusCache = statusCache {
|
||||||
return tootCache.dictionary[entity.id]
|
return statusCache.dictionary[entity.id]
|
||||||
} else {
|
} else {
|
||||||
let request = Toot.sortedFetchRequest
|
let request = Status.sortedFetchRequest
|
||||||
request.predicate = Toot.predicate(domain: domain, id: entity.id)
|
request.predicate = Status.predicate(domain: domain, id: entity.id)
|
||||||
request.fetchLimit = 1
|
request.fetchLimit = 1
|
||||||
request.returnsObjectsAsFaults = false
|
request.returnsObjectsAsFaults = false
|
||||||
do {
|
do {
|
||||||
|
@ -62,19 +62,19 @@ extension APIService.CoreData {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if let oldToot = oldToot {
|
if let oldStatus = oldStatus {
|
||||||
// merge old Toot
|
// merge old Status
|
||||||
APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
|
APIService.CoreData.merge(status: oldStatus, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
|
||||||
return (oldToot, false, false)
|
return (oldStatus, false, false)
|
||||||
} else {
|
} else {
|
||||||
let (mastodonUser, isMastodonUserCreated) = createOrMergeMastodonUser(into: managedObjectContext, for: requestMastodonUser,in: domain, entity: entity.account, userCache: userCache, networkDate: networkDate, log: log)
|
let (mastodonUser, isMastodonUserCreated) = createOrMergeMastodonUser(into: managedObjectContext, for: requestMastodonUser,in: domain, entity: entity.account, userCache: userCache, networkDate: networkDate, log: log)
|
||||||
let application = entity.application.flatMap { app -> Application? in
|
let application = entity.application.flatMap { app -> Application? in
|
||||||
Application.insert(into: managedObjectContext, property: Application.Property(name: app.name, website: app.website, vapidKey: app.vapidKey))
|
Application.insert(into: managedObjectContext, property: Application.Property(name: app.name, website: app.website, vapidKey: app.vapidKey))
|
||||||
}
|
}
|
||||||
let replyTo: Toot? = {
|
let replyTo: Status? = {
|
||||||
// could be nil if target replyTo toot's persist task in the queue
|
// could be nil if target replyTo status's persist task in the queue
|
||||||
guard let inReplyToID = entity.inReplyToID,
|
guard let inReplyToID = entity.inReplyToID,
|
||||||
let replyTo = tootCache?.dictionary[inReplyToID] else { return nil }
|
let replyTo = statusCache?.dictionary[inReplyToID] else { return nil }
|
||||||
return replyTo
|
return replyTo
|
||||||
}()
|
}()
|
||||||
let poll = entity.poll.flatMap { poll -> Poll in
|
let poll = entity.poll.flatMap { poll -> Poll in
|
||||||
|
@ -111,10 +111,10 @@ extension APIService.CoreData {
|
||||||
guard !attachments.isEmpty else { return nil }
|
guard !attachments.isEmpty else { return nil }
|
||||||
return attachments
|
return attachments
|
||||||
}()
|
}()
|
||||||
let tootProperty = Toot.Property(entity: entity, domain: domain, networkDate: networkDate)
|
let statusProperty = Status.Property(entity: entity, domain: domain, networkDate: networkDate)
|
||||||
let toot = Toot.insert(
|
let status = Status.insert(
|
||||||
into: managedObjectContext,
|
into: managedObjectContext,
|
||||||
property: tootProperty,
|
property: statusProperty,
|
||||||
author: mastodonUser,
|
author: mastodonUser,
|
||||||
reblog: reblog,
|
reblog: reblog,
|
||||||
application: application,
|
application: application,
|
||||||
|
@ -130,67 +130,81 @@ extension APIService.CoreData {
|
||||||
bookmarkedBy: (entity.bookmarked ?? false) ? requestMastodonUser : nil,
|
bookmarkedBy: (entity.bookmarked ?? false) ? requestMastodonUser : nil,
|
||||||
pinnedBy: (entity.pinned ?? false) ? requestMastodonUser : nil
|
pinnedBy: (entity.pinned ?? false) ? requestMastodonUser : nil
|
||||||
)
|
)
|
||||||
tootCache?.dictionary[entity.id] = toot
|
statusCache?.dictionary[entity.id] = status
|
||||||
os_signpost(.event, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "did insert new tweet %{public}s: %s", mastodonUser.identifier, entity.id)
|
os_signpost(.event, log: log, name: "update database - process entity: createOrMergeStatus", signpostID: processEntityTaskSignpostID, "did insert new tweet %{public}s: %s", mastodonUser.identifier, entity.id)
|
||||||
return (toot, true, isMastodonUserCreated)
|
return (status, true, isMastodonUserCreated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension APIService.CoreData {
|
||||||
static func merge(
|
static func merge(
|
||||||
toot: Toot,
|
status: Status,
|
||||||
entity: Mastodon.Entity.Status,
|
entity: Mastodon.Entity.Status,
|
||||||
requestMastodonUser: MastodonUser?,
|
requestMastodonUser: MastodonUser?,
|
||||||
domain: String,
|
domain: String,
|
||||||
networkDate: Date
|
networkDate: Date
|
||||||
) {
|
) {
|
||||||
guard networkDate > toot.updatedAt else { return }
|
guard networkDate > status.updatedAt else { return }
|
||||||
|
|
||||||
// merge poll
|
// merge poll
|
||||||
if let poll = toot.poll, let entity = entity.poll {
|
if let poll = status.poll, let entity = entity.poll {
|
||||||
merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
|
merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge metrics
|
// merge metrics
|
||||||
if entity.favouritesCount != toot.favouritesCount.intValue {
|
if entity.favouritesCount != status.favouritesCount.intValue {
|
||||||
toot.update(favouritesCount:NSNumber(value: entity.favouritesCount))
|
status.update(favouritesCount:NSNumber(value: entity.favouritesCount))
|
||||||
}
|
}
|
||||||
if let repliesCount = entity.repliesCount {
|
if let repliesCount = entity.repliesCount {
|
||||||
if (repliesCount != toot.repliesCount?.intValue) {
|
if (repliesCount != status.repliesCount?.intValue) {
|
||||||
toot.update(repliesCount:NSNumber(value: repliesCount))
|
status.update(repliesCount:NSNumber(value: repliesCount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if entity.reblogsCount != toot.reblogsCount.intValue {
|
if entity.reblogsCount != status.reblogsCount.intValue {
|
||||||
toot.update(reblogsCount:NSNumber(value: entity.reblogsCount))
|
status.update(reblogsCount:NSNumber(value: entity.reblogsCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge relationship
|
// merge relationship
|
||||||
if let mastodonUser = requestMastodonUser {
|
if let mastodonUser = requestMastodonUser {
|
||||||
if let favourited = entity.favourited {
|
if let favourited = entity.favourited {
|
||||||
toot.update(liked: favourited, mastodonUser: mastodonUser)
|
status.update(liked: favourited, by: mastodonUser)
|
||||||
}
|
}
|
||||||
if let reblogged = entity.reblogged {
|
if let reblogged = entity.reblogged {
|
||||||
toot.update(reblogged: reblogged, mastodonUser: mastodonUser)
|
status.update(reblogged: reblogged, by: mastodonUser)
|
||||||
}
|
}
|
||||||
if let muted = entity.muted {
|
if let muted = entity.muted {
|
||||||
toot.update(muted: muted, mastodonUser: mastodonUser)
|
status.update(muted: muted, by: mastodonUser)
|
||||||
}
|
}
|
||||||
if let bookmarked = entity.bookmarked {
|
if let bookmarked = entity.bookmarked {
|
||||||
toot.update(bookmarked: bookmarked, mastodonUser: mastodonUser)
|
status.update(bookmarked: bookmarked, by: mastodonUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set updateAt
|
// set updateAt
|
||||||
toot.didUpdate(at: networkDate)
|
status.didUpdate(at: networkDate)
|
||||||
|
|
||||||
// merge user
|
// merge user
|
||||||
mergeMastodonUser(for: requestMastodonUser, old: toot.author, in: domain, entity: entity.account, networkDate: networkDate)
|
merge(
|
||||||
|
user: status.author,
|
||||||
|
entity: entity.account,
|
||||||
|
requestMastodonUser: requestMastodonUser,
|
||||||
|
domain: domain,
|
||||||
|
networkDate: networkDate
|
||||||
|
)
|
||||||
|
|
||||||
// merge indirect reblog
|
// merge indirect reblog
|
||||||
if let reblog = toot.reblog, let reblogEntity = entity.reblog {
|
if let reblog = status.reblog, let reblogEntity = entity.reblog {
|
||||||
merge(toot: reblog, entity: reblogEntity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
|
merge(
|
||||||
|
status: reblog,
|
||||||
|
entity: reblogEntity,
|
||||||
|
requestMastodonUser: requestMastodonUser,
|
||||||
|
domain: domain,
|
||||||
|
networkDate: networkDate
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension APIService.CoreData {
|
extension APIService.CoreData {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue