feat: Implement /api/v2/instance to conditionally show Translation option

This commit is contained in:
Marcus Kida 2022-12-12 16:41:13 +01:00
parent 58dcadf642
commit 2d9e980215
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
22 changed files with 722 additions and 42 deletions

View File

@ -27,6 +27,7 @@ extension StatusSection {
static let logger = Logger(subsystem: "StatusSection", category: "logic")
struct Configuration {
let context: AppContext
let authContext: AuthContext
weak var statusTableViewCellDelegate: StatusTableViewCellDelegate?
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
@ -250,6 +251,7 @@ extension StatusSection {
statusView: cell.statusView
)
cell.statusView.viewModel.context = configuration.context
cell.statusView.viewModel.authContext = configuration.authContext
cell.configure(

View File

@ -18,6 +18,7 @@ extension DiscoveryCommunityViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
context: context,
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,

View File

@ -18,6 +18,7 @@ extension DiscoveryPostsViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
context: context,
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,

View File

@ -20,6 +20,7 @@ extension HashtagTimelineViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
context: context,
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,

View File

@ -22,6 +22,7 @@ extension HomeTimelineViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
context: context,
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,

View File

@ -17,6 +17,7 @@ extension BookmarkViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
context: context,
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,

View File

@ -17,6 +17,7 @@ extension FavoriteViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
context: context,
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,

View File

@ -18,6 +18,7 @@ extension UserTimelineViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
context: context,
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,

View File

@ -24,6 +24,7 @@ extension ThreadViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
context: context,
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,

View File

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

View File

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

View File

@ -16,7 +16,8 @@ public final class Instance: NSManagedObject {
@NSManaged public private(set) var updatedAt: Date
@NSManaged public private(set) var configurationRaw: Data?
@NSManaged public private(set) var configurationV2Raw: Data?
// MARK: one-to-many relationships
@NSManaged public var authentications: Set<MastodonAuthentication>
}
@ -44,6 +45,10 @@ extension Instance {
self.configurationRaw = configurationRaw
}
public func update(configurationV2Raw: Data?) {
self.configurationV2Raw = configurationV2Raw
}
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}

View File

@ -25,8 +25,42 @@ extension Instance {
}
extension Instance {
public var canFollowTags: Bool {
guard let majorVersionString = version?.split(separator: ".").first else { return false }
return Int(majorVersionString) == 4 // following Tags is support beginning with Mastodon v4.0.0
public var configurationV2: Mastodon.Entity.V2.Instance.Configuration? {
guard let configurationRaw = configurationV2Raw else { return nil }
guard let configuration = try? JSONDecoder().decode(Mastodon.Entity.V2.Instance.Configuration.self, from: configurationRaw) else {
return nil
}
return configuration
}
static func encodeV2(configuration: Mastodon.Entity.V2.Instance.Configuration) -> Data? {
return try? JSONEncoder().encode(configuration)
}
}
extension Instance {
public var canFollowTags: Bool {
version?.majorServerVersion(greaterThanOrEquals: 4) ?? false // following Tags is support beginning with Mastodon v4.0.0
}
}
extension String {
public func majorServerVersion(greaterThanOrEquals comparedVersion: Int) -> Bool {
guard
let majorVersionString = split(separator: ".").first,
let majorVersionInt = Int(majorVersionString)
else { return false }
return majorVersionInt >= comparedVersion
}
}
extension Instance {
var isTranslationEnabled: Bool {
if let configuration = configurationV2 {
return configuration.translation?.enabled == true
}
return false
}
}

View File

@ -20,4 +20,9 @@ extension APIService {
return Mastodon.API.Instance.instance(session: session, domain: domain)
}
public func instanceV2(
domain: String
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.V2.Instance>, Error> {
return Mastodon.API.V2.Instance.instance(session: session, domain: domain)
}
}

View File

@ -0,0 +1,69 @@
import os.log
import Foundation
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService.CoreData {
static func createOrMergeInstanceV2(
into managedObjectContext: NSManagedObjectContext,
domain: String,
entity: Mastodon.Entity.V2.Instance,
networkDate: Date,
log: Logger
) -> (instance: Instance, isCreated: Bool) {
// fetch old mastodon user
let old: Instance? = {
let request = Instance.sortedFetchRequest
request.predicate = Instance.predicate(domain: domain)
request.fetchLimit = 1
request.returnsObjectsAsFaults = false
do {
return try managedObjectContext.fetch(request).first
} catch {
assertionFailure(error.localizedDescription)
return nil
}
}()
if let old = old {
APIService.CoreData.mergeV2(
instance: old,
entity: entity,
domain: domain,
networkDate: networkDate
)
return (old, false)
} else {
let instance = Instance.insert(
into: managedObjectContext,
property: Instance.Property(domain: domain, version: entity.version)
)
let configurationRaw = entity.configuration.flatMap { Instance.encodeV2(configuration: $0) }
instance.update(configurationV2Raw: configurationRaw)
return (instance, true)
}
}
}
extension APIService.CoreData {
static func mergeV2(
instance: Instance,
entity: Mastodon.Entity.V2.Instance,
domain: String,
networkDate: Date
) {
guard networkDate > instance.updatedAt else { return }
let configurationRaw = entity.configuration.flatMap { Instance.encodeV2(configuration: $0) }
instance.update(configurationV2Raw: configurationRaw)
instance.version = entity.version
instance.didUpdate(at: networkDate)
}
}

View File

@ -50,42 +50,18 @@ extension InstanceService {
func updateInstance(domain: String) {
guard let apiService = self.apiService else { return }
apiService.instance(domain: domain)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Instance>, Error> in
let managedObjectContext = self.backgroundManagedObjectContext
return managedObjectContext.performChanges {
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
into: managedObjectContext,
domain: domain,
entity: response.value,
networkDate: response.networkDate,
log: Logger(subsystem: "Update", category: "InstanceService")
)
// update relationship
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(domain: domain)
request.returnsObjectsAsFaults = false
do {
let authentications = try managedObjectContext.fetch(request)
for authentication in authentications {
authentication.update(instance: instance)
}
} catch {
assertionFailure(error.localizedDescription)
}
.flatMap { [unowned self] response -> AnyPublisher<Void, Error> in
if response.value.version?.majorServerVersion(greaterThanOrEquals: 4) == true {
return apiService.instanceV2(domain: domain)
.flatMap { return self.updateInstanceV2(domain: domain, response: $0) }
.eraseToAnyPublisher()
} else {
return self.updateInstance(domain: domain, response: response)
}
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Instance> in
switch result {
case .success:
return response
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
}
// .flatMap { [unowned self] response -> AnyPublisher<Void, Error> in
// return
// }
.sink { [weak self] completion in
guard let self = self else { return }
switch completion {
@ -100,6 +76,80 @@ extension InstanceService {
}
.store(in: &disposeBag)
}
private func updateInstance(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.Instance>) -> AnyPublisher<Void, Error> {
let managedObjectContext = self.backgroundManagedObjectContext
return managedObjectContext.performChanges {
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
into: managedObjectContext,
domain: domain,
entity: response.value,
networkDate: response.networkDate,
log: Logger(subsystem: "Update", category: "InstanceService")
)
// update relationship
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(domain: domain)
request.returnsObjectsAsFaults = false
do {
let authentications = try managedObjectContext.fetch(request)
for authentication in authentications {
authentication.update(instance: instance)
}
} catch {
assertionFailure(error.localizedDescription)
}
}
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .success:
break
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
}
private func updateInstanceV2(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.V2.Instance>) -> AnyPublisher<Void, Error> {
let managedObjectContext = self.backgroundManagedObjectContext
return managedObjectContext.performChanges {
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstanceV2(
into: managedObjectContext,
domain: domain,
entity: response.value,
networkDate: response.networkDate,
log: Logger(subsystem: "Update", category: "InstanceService")
)
// update relationship
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(domain: domain)
request.returnsObjectsAsFaults = false
do {
let authentications = try managedObjectContext.fetch(request)
for authentication in authentications {
authentication.update(instance: instance)
}
} catch {
assertionFailure(error.localizedDescription)
}
}
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .success:
break
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
}
}
public extension InstanceService {

View File

@ -0,0 +1,50 @@
import Foundation
import Combine
extension Mastodon.API.V2.Instance {
private static func instanceEndpointURL(domain: String) -> URL {
return Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent("instance")
}
/// Information about the server
///
/// - Since: 4.0.0
/// - Version: 4.0.0
/// # Last Update
/// 2022/12/09
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/instance/)
/// - Parameters:
/// - session: `URLSession`
/// - domain: Mastodon instance domain. e.g. "example.com"
/// - Returns: `AnyPublisher` contains `Instance` nested in the response
public static func instance(
session: URLSession,
domain: String
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.V2.Instance>, Error> {
let request = Mastodon.API.get(
url: instanceEndpointURL(domain: domain),
query: nil,
authorization: nil
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value: Mastodon.Entity.V2.Instance
do {
value = try Mastodon.API.decode(type: Mastodon.Entity.V2.Instance.self, from: data, response: response)
} catch {
if let response = response as? HTTPURLResponse, 400 ..< 500 ~= response.statusCode {
// For example, AUTHORIZED_FETCH may result in authentication errors
value = Mastodon.Entity.V2.Instance(domain: domain)
} else {
throw error
}
}
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
}

View File

@ -126,6 +126,7 @@ extension Mastodon.API.V2 {
public enum Search { }
public enum Suggestions { }
public enum Media { }
public enum Instance { }
}
extension Mastodon.API {

View File

@ -0,0 +1,158 @@
import Foundation
extension Mastodon.Entity.V2 {
/// Instance
///
/// - Since: 4.0.0
/// - Version: 4.0.3
/// # Last Update
/// 2022/12/09
/// # Reference
/// [Document](https://docs.joinmastodon.org/entities/instance/)
public struct Instance: Codable {
public let domain: String?
public let title: String
public let description: String
public let shortDescription: String?
public let email: String?
public let version: String?
public let languages: [String]? // (ISO 639 Part 1-5 language codes)
public let registrations: Mastodon.Entity.V2.Instance.Registrations?
public let approvalRequired: Bool?
public let invitesEnabled: Bool?
public let urls: Mastodon.Entity.Instance.InstanceURL?
public let statistics: Mastodon.Entity.Instance.Statistics?
public let thumbnail: Thumbnail?
public let contactAccount: Mastodon.Entity.Account?
public let rules: [Mastodon.Entity.Instance.Rule]?
// https://github.com/mastodon/mastodon/pull/16485
public let configuration: Configuration?
public init(domain: String, approvalRequired: Bool? = nil) {
self.domain = domain
self.title = domain
self.description = ""
self.shortDescription = nil
self.email = ""
self.version = nil
self.languages = nil
self.registrations = nil
self.approvalRequired = approvalRequired
self.invitesEnabled = nil
self.urls = nil
self.statistics = nil
self.thumbnail = nil
self.contactAccount = nil
self.rules = nil
self.configuration = nil
}
enum CodingKeys: String, CodingKey {
case domain
case title
case description
case shortDescription = "short_description"
case email
case version
case languages
case registrations
case approvalRequired = "approval_required"
case invitesEnabled = "invites_enabled"
case urls
case statistics = "stats"
case thumbnail
case contactAccount = "contact_account"
case rules
case configuration
}
}
}
extension Mastodon.Entity.V2.Instance {
public struct Configuration: Codable {
public let statuses: Mastodon.Entity.Instance.Configuration.Statuses?
public let mediaAttachments: Mastodon.Entity.Instance.Configuration.MediaAttachments?
public let polls: Mastodon.Entity.Instance.Configuration.Polls?
public let translation: Mastodon.Entity.V2.Instance.Configuration.Translation?
enum CodingKeys: String, CodingKey {
case statuses
case mediaAttachments = "media_attachments"
case polls
case translation
}
}
}
extension Mastodon.Entity.V2.Instance {
public struct Registrations: Codable {
public let enabled: Bool
}
}
extension Mastodon.Entity.V2.Instance.Configuration {
public struct Translation: Codable {
public let enabled: Bool
}
}
extension Mastodon.Entity.V2.Instance {
public struct Thumbnail: Codable {
public let url: String?
}
}
//extension Mastodon.Entity.V2.Instance {
// public struct Statuses: Codable {
// public let maxCharacters: Int
// public let maxMediaAttachments: Int
// public let charactersReservedPerURL: Int
//
// enum CodingKeys: String, CodingKey {
// case maxCharacters = "max_characters"
// case maxMediaAttachments = "max_media_attachments"
// case charactersReservedPerURL = "characters_reserved_per_url"
// }
// }
//
// public struct MediaAttachments: Codable {
// public let supportedMIMETypes: [String]
// public let imageSizeLimit: Int
// public let imageMatrixLimit: Int
// public let videoSizeLimit: Int
// public let videoFrameRateLimit: Int
// public let videoMatrixLimit: Int
//
// enum CodingKeys: String, CodingKey {
// case supportedMIMETypes = "supported_mime_types"
// case imageSizeLimit = "image_size_limit"
// case imageMatrixLimit = "image_matrix_limit"
// case videoSizeLimit = "video_size_limit"
// case videoFrameRateLimit = "video_frame_rate_limit"
// case videoMatrixLimit = "video_matrix_limit"
// }
// }
//
// public struct Polls: Codable {
// public let maxOptions: Int
// public let maxCharactersPerOption: Int
// public let minExpiration: Int
// public let maxExpiration: Int
//
// enum CodingKeys: String, CodingKey {
// case maxOptions = "max_options"
// case maxCharactersPerOption = "max_characters_per_option"
// case minExpiration = "min_expiration"
// case maxExpiration = "max_expiration"
// }
// }
//
// public struct Translation: Codable {
// public let enabled: Bool
// }
//}

View File

@ -23,7 +23,8 @@ extension NotificationView {
public var objects = Set<NSManagedObject>()
let logger = Logger(subsystem: "NotificationView", category: "ViewModel")
@Published public var context: AppContext?
@Published public var authContext: AuthContext?
@Published public var type: MastodonNotificationType?
@ -57,6 +58,9 @@ extension NotificationView.ViewModel {
bindAuthorMenu(notificationView: notificationView)
bindFollowRequest(notificationView: notificationView)
$context
.assign(to: \.context, on: notificationView.statusView.viewModel)
.store(in: &disposeBag)
$authContext
.assign(to: \.authContext, on: notificationView.statusView.viewModel)
.store(in: &disposeBag)
@ -209,7 +213,7 @@ extension NotificationView.ViewModel {
$isTranslated
)
)
.sink { authorName, isMuting, isBlocking, isMyselfIsTranslated in
.sink { [weak self] authorName, isMuting, isBlocking, isMyselfIsTranslated in
guard let name = authorName?.string else {
notificationView.menuButton.menu = nil
return
@ -217,12 +221,29 @@ extension NotificationView.ViewModel {
let (isMyself, isTranslated) = isMyselfIsTranslated
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = {
guard
let self = self,
let context = self.context,
let authContext = self.authContext
else { return nil }
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)
else { return }
configuration = authentication.instance?.configurationV2
}
return configuration
}()
let menuContext = NotificationView.AuthorMenuContext(
name: name,
isMuting: isMuting,
isBlocking: isBlocking,
isMyself: isMyself,
isBookmarking: false, // no bookmark action display for notification item
isTranslationEnabled: instanceConfigurationV2?.translation?.enabled == true,
isTranslated: isTranslated,
statusLanguage: ""
)

View File

@ -150,6 +150,7 @@ extension StatusAuthorView {
public let isMyself: Bool
public let isBookmarking: Bool
public let isTranslationEnabled: Bool
public let isTranslated: Bool
public let statusLanguage: String?
}
@ -158,7 +159,7 @@ extension StatusAuthorView {
var actions = [MastodonMenu.Action]()
if !menuContext.isMyself {
if let statusLanguage = menuContext.statusLanguage, !menuContext.isTranslated {
if let statusLanguage = menuContext.statusLanguage, menuContext.isTranslationEnabled, !menuContext.isTranslated {
actions.append(
.translateStatus(.init(language: statusLanguage))
)

View File

@ -27,6 +27,7 @@ extension StatusView {
let logger = Logger(subsystem: "StatusView", category: "ViewModel")
public var context: AppContext?
public var authContext: AuthContext?
public var originalStatus: Status?
@ -609,12 +610,28 @@ extension StatusView.ViewModel {
return
}
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = {
guard
let context = self.context,
let authContext = self.authContext
else { return nil }
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)
else { return }
configuration = authentication.instance?.configurationV2
}
return configuration
}()
let menuContext = StatusAuthorView.AuthorMenuContext(
name: name,
isMuting: isMuting,
isBlocking: isBlocking,
isMyself: isMyself,
isBookmarking: isBookmark,
isTranslationEnabled: instanceConfigurationV2?.translation?.enabled == true,
isTranslated: translatedFromLanguage != nil,
statusLanguage: language
)