feat: display custom emoji for timeline post

This commit is contained in:
CMK 2021-05-07 18:25:57 +08:00
parent 8409331dd8
commit faeb8d99ef
17 changed files with 166 additions and 65 deletions

View File

@ -1,5 +1,5 @@
<?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="18154" systemVersion="20D75" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20E232" 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"/>
@ -45,7 +45,6 @@
<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="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"/>
@ -102,6 +101,7 @@
<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="emojisData" optional="YES" attributeType="Binary"/>
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="header" attributeType="String"/> <attribute name="header" attributeType="String"/>
@ -197,6 +197,7 @@
<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"/>
<attribute name="domain" attributeType="String"/> <attribute name="domain" attributeType="String"/>
<attribute name="emojisData" optional="YES" attributeType="Binary"/>
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/> <attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="String"/> <attribute name="identifier" attributeType="String"/>
@ -216,7 +217,6 @@
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="status" 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="statuses" 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="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="status" 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="status" inverseEntity="Attachment"/> <relationship name="mediaAttachments" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Attachment" inverseName="status" inverseEntity="Attachment"/>
@ -267,12 +267,12 @@
<element name="Application" positionX="0" positionY="0" width="128" height="104"/> <element name="Application" positionX="0" positionY="0" width="128" height="104"/>
<element name="Attachment" positionX="0" positionY="0" width="128" height="254"/> <element name="Attachment" positionX="0" positionY="0" width="128" height="254"/>
<element name="DomainBlock" positionX="45" positionY="162" width="128" height="89"/> <element name="DomainBlock" positionX="45" positionY="162" width="128" height="89"/>
<element name="Emoji" positionX="0" positionY="0" width="128" height="149"/> <element name="Emoji" positionX="0" positionY="0" width="128" height="134"/>
<element name="History" positionX="0" positionY="0" width="128" height="119"/> <element name="History" positionX="0" positionY="0" width="128" height="119"/>
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/> <element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/> <element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/> <element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/> <element name="MastodonUser" positionX="0" positionY="0" width="128" height="689"/>
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/> <element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/> <element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/> <element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>

View File

@ -25,6 +25,9 @@ final public class MastodonUser: NSManagedObject {
@NSManaged public private(set) var headerStatic: String? @NSManaged public private(set) var headerStatic: String?
@NSManaged public private(set) var note: String? @NSManaged public private(set) var note: String?
@NSManaged public private(set) var url: String? @NSManaged public private(set) var url: String?
@NSManaged public private(set) var emojisData: Data?
@NSManaged public private(set) var statusesCount: NSNumber @NSManaged public private(set) var statusesCount: NSNumber
@NSManaged public private(set) var followingCount: NSNumber @NSManaged public private(set) var followingCount: NSNumber
@NSManaged public private(set) var followersCount: NSNumber @NSManaged public private(set) var followersCount: NSNumber
@ -88,6 +91,8 @@ extension MastodonUser {
user.headerStatic = property.headerStatic user.headerStatic = property.headerStatic
user.note = property.note user.note = property.note
user.url = property.url user.url = property.url
user.emojisData = property.emojisData
user.statusesCount = NSNumber(value: property.statusesCount) user.statusesCount = NSNumber(value: property.statusesCount)
user.followingCount = NSNumber(value: property.followingCount) user.followingCount = NSNumber(value: property.followingCount)
user.followersCount = NSNumber(value: property.followersCount) user.followersCount = NSNumber(value: property.followersCount)
@ -151,6 +156,11 @@ extension MastodonUser {
self.url = url self.url = url
} }
} }
public func update(emojisData: Data?) {
if self.emojisData != emojisData {
self.emojisData = emojisData
}
}
public func update(statusesCount: Int) { public func update(statusesCount: Int) {
if self.statusesCount.intValue != statusesCount { if self.statusesCount.intValue != statusesCount {
self.statusesCount = NSNumber(value: statusesCount) self.statusesCount = NSNumber(value: statusesCount)
@ -270,6 +280,7 @@ extension MastodonUser {
public let headerStatic: String? public let headerStatic: String?
public let note: String? public let note: String?
public let url: String? public let url: String?
public let emojisData: Data?
public let statusesCount: Int public let statusesCount: Int
public let followingCount: Int public let followingCount: Int
public let followersCount: Int public let followersCount: Int
@ -292,6 +303,7 @@ extension MastodonUser {
headerStatic: String?, headerStatic: String?,
note: String?, note: String?,
url: String?, url: String?,
emojisData: Data?,
statusesCount: Int, statusesCount: Int,
followingCount: Int, followingCount: Int,
followersCount: Int, followersCount: Int,
@ -313,6 +325,7 @@ extension MastodonUser {
self.headerStatic = headerStatic self.headerStatic = headerStatic
self.note = note self.note = note
self.url = url self.url = url
self.emojisData = emojisData
self.statusesCount = statusesCount self.statusesCount = statusesCount
self.followingCount = followingCount self.followingCount = followingCount
self.followersCount = followersCount self.followersCount = followersCount

View File

@ -24,6 +24,8 @@ public final class Status: NSManagedObject {
@NSManaged public private(set) var spoilerText: String? @NSManaged public private(set) var spoilerText: String?
@NSManaged public private(set) var application: Application? @NSManaged public private(set) var application: Application?
@NSManaged public private(set) var emojisData: Data?
// Informational // Informational
@NSManaged public private(set) var reblogsCount: NSNumber @NSManaged public private(set) var reblogsCount: NSNumber
@NSManaged public private(set) var favouritesCount: NSNumber @NSManaged public private(set) var favouritesCount: NSNumber
@ -54,7 +56,6 @@ public final class Status: NSManagedObject {
// one-to-many relationship // one-to-many relationship
@NSManaged public private(set) var reblogFrom: Set<Status>? @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 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>?
@ -77,7 +78,6 @@ extension Status {
replyTo: Status?, replyTo: Status?,
poll: Poll?, poll: Poll?,
mentions: [Mention]?, mentions: [Mention]?,
emojis: [Emoji]?,
tags: [Tag]?, tags: [Tag]?,
mediaAttachments: [Attachment]?, mediaAttachments: [Attachment]?,
favouritedBy: MastodonUser?, favouritedBy: MastodonUser?,
@ -101,6 +101,8 @@ extension Status {
status.spoilerText = property.spoilerText status.spoilerText = property.spoilerText
status.application = application status.application = application
status.emojisData = property.emojisData
status.reblogsCount = property.reblogsCount status.reblogsCount = property.reblogsCount
status.favouritesCount = property.favouritesCount status.favouritesCount = property.favouritesCount
status.repliesCount = property.repliesCount status.repliesCount = property.repliesCount
@ -121,9 +123,6 @@ extension Status {
if let mentions = mentions { if let mentions = mentions {
status.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions) status.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions)
} }
if let emojis = emojis {
status.mutableSetValue(forKey: #keyPath(Status.emojis)).addObjects(from: emojis)
}
if let tags = tags { if let tags = tags {
status.mutableSetValue(forKey: #keyPath(Status.tags)).addObjects(from: tags) status.mutableSetValue(forKey: #keyPath(Status.tags)).addObjects(from: tags)
} }
@ -148,6 +147,12 @@ extension Status {
return status return status
} }
public func update(emojisData: Data?) {
if self.emojisData != emojisData {
self.emojisData = emojisData
}
}
public func update(reblogsCount: NSNumber) { public func update(reblogsCount: NSNumber) {
if self.reblogsCount.intValue != reblogsCount.intValue { if self.reblogsCount.intValue != reblogsCount.intValue {
self.reblogsCount = reblogsCount self.reblogsCount = reblogsCount
@ -248,6 +253,8 @@ extension Status {
public let sensitive: Bool public let sensitive: Bool
public let spoilerText: String? public let spoilerText: String?
public let emojisData: Data?
public let reblogsCount: NSNumber public let reblogsCount: NSNumber
public let favouritesCount: NSNumber public let favouritesCount: NSNumber
public let repliesCount: NSNumber? public let repliesCount: NSNumber?
@ -269,6 +276,7 @@ extension Status {
visibility: String?, visibility: String?,
sensitive: Bool, sensitive: Bool,
spoilerText: String?, spoilerText: String?,
emojisData: Data?,
reblogsCount: NSNumber, reblogsCount: NSNumber,
favouritesCount: NSNumber, favouritesCount: NSNumber,
repliesCount: NSNumber?, repliesCount: NSNumber?,
@ -288,6 +296,7 @@ extension Status {
self.visibility = visibility self.visibility = visibility
self.sensitive = sensitive self.sensitive = sensitive
self.spoilerText = spoilerText self.spoilerText = spoilerText
self.emojisData = emojisData
self.reblogsCount = reblogsCount self.reblogsCount = reblogsCount
self.favouritesCount = favouritesCount self.favouritesCount = favouritesCount
self.repliesCount = repliesCount self.repliesCount = repliesCount

View File

@ -400,6 +400,7 @@
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; }; DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; }; DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; };
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
@ -962,6 +963,7 @@
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; }; DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; }; DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; };
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.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>"; }; 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>"; }; DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; };
@ -1594,6 +1596,7 @@
DB6D9F6E2635807F008423CD /* Setting.swift */, DB6D9F6E2635807F008423CD /* Setting.swift */,
DB6D9F4826353FD6008423CD /* Subscription.swift */, DB6D9F4826353FD6008423CD /* Subscription.swift */,
DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */, DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */,
DBAFB7342645463500371D5F /* Emojis.swift */,
); );
path = CoreDataStack; path = CoreDataStack;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3199,6 +3202,7 @@
DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */, DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */,
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */, 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */, DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */,
DBAFB7352645463500371D5F /* Emojis.swift in Sources */,
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */, DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */, DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
@ -3913,8 +3917,8 @@
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = exactVersion;
minimumVersion = 4.0.0; version = 5.0.1;
}; };
}; };
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = {

View File

@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift", "repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
"state": { "state": {
"branch": null, "branch": null,
"revision": "d6cf96e0ca4f2269021bcf8f11381ab57897f84a", "revision": "40e104063d825d1125ef4b8eeb6460eba8a57483",
"version": "4.0.0" "version": "5.0.1"
} }
}, },
{ {

View File

@ -68,7 +68,8 @@ extension ComposeStatusSection {
}() }()
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
// set text // set text
cell.statusView.activeTextLabel.configure(content: status.content) //status.emoji
cell.statusView.activeTextLabel.configure(content: status.content, emojiDict: [:])
// set date // set date
cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow

View File

@ -158,10 +158,11 @@ extension StatusSection {
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
// set name username // set name username
cell.statusView.nameLabel.text = { let nameText: String = {
let author = (status.reblog ?? status).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.nameLabel.configure(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict)
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
// set avatar // set avatar
if let reblog = status.reblog { if let reblog = status.reblog {
@ -176,7 +177,10 @@ extension StatusSection {
} }
// set text // set text
cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content) cell.statusView.activeTextLabel.configure(
content: (status.reblog ?? status).content,
emojiDict: (status.reblog ?? status).emojiDict
)
// prepare media attachments // prepare media attachments
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
@ -569,15 +573,16 @@ extension StatusSection {
if status.reblog != nil { if status.reblog != nil {
cell.statusView.headerContainerView.isHidden = false cell.statusView.headerContainerView.isHidden = false
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage) cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
cell.statusView.headerInfoLabel.text = { let headerText: String = {
let author = status.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)
}() }()
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict)
} else if status.inReplyToID != nil { } else if status.inReplyToID != nil {
cell.statusView.headerContainerView.isHidden = false cell.statusView.headerContainerView.isHidden = false
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
cell.statusView.headerInfoLabel.text = { let headerText: String = {
guard let replyTo = status.replyTo else { guard let replyTo = status.replyTo else {
return L10n.Common.Controls.Status.userRepliedTo("-") return L10n.Common.Controls.Status.userRepliedTo("-")
} }
@ -585,6 +590,7 @@ extension StatusSection {
let name = author.displayName.isEmpty ? author.username : author.displayName let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Common.Controls.Status.userRepliedTo(name) return L10n.Common.Controls.Status.userRepliedTo(name)
}() }()
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
} else { } else {
cell.statusView.headerContainerView.isHidden = true cell.statusView.headerContainerView.isHidden = true
} }

View File

@ -14,6 +14,8 @@ extension ActiveLabel {
enum Style { enum Style {
case `default` case `default`
case statusHeader
case statusName
case profileField case profileField
} }
@ -25,6 +27,7 @@ extension ActiveLabel {
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
emojiPlaceholderColor = .systemFill
#if DEBUG #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 #endif
@ -33,6 +36,14 @@ extension ActiveLabel {
case .default: case .default:
font = .preferredFont(forTextStyle: .body) font = .preferredFont(forTextStyle: .body)
textColor = Asset.Colors.Label.primary.color textColor = Asset.Colors.Label.primary.color
case .statusHeader:
font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium))
textColor = Asset.Colors.Label.secondary.color
numberOfLines = 1
case .statusName:
font = .systemFont(ofSize: 17, weight: .semibold)
textColor = Asset.Colors.Label.primary.color
numberOfLines = 1
case .profileField: case .profileField:
font = .preferredFont(forTextStyle: .body) font = .preferredFont(forTextStyle: .body)
textColor = Asset.Colors.Label.primary.color textColor = Asset.Colors.Label.primary.color
@ -44,9 +55,10 @@ extension ActiveLabel {
extension ActiveLabel { extension ActiveLabel {
/// status content /// status content
func configure(content: String) { func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
activeEntities.removeAll() activeEntities.removeAll()
if let parseResult = try? MastodonStatusContent.parse(status: content) {
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
text = parseResult.trimmed text = parseResult.trimmed
activeEntities = parseResult.activeEntities activeEntities = parseResult.activeEntities
} else { } else {
@ -55,8 +67,8 @@ extension ActiveLabel {
} }
/// account note /// account note
func configure(note: String) { func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) {
configure(content: note) configure(content: note, emojiDict: emojiDict)
} }
} }

View File

@ -0,0 +1,36 @@
//
// Emojis.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-5-7.
//
import Foundation
import MastodonSDK
protocol EmojiContinaer {
var emojisData: Data? { get }
}
extension EmojiContinaer {
static func encode(emojis: [Mastodon.Entity.Emoji]) -> Data? {
return try? JSONEncoder().encode(emojis)
}
var emojis: [Mastodon.Entity.Emoji]? {
let decoder = JSONDecoder()
return emojisData.flatMap { try? decoder.decode([Mastodon.Entity.Emoji].self, from: $0) }
}
var emojiDict: MastodonStatusContent.EmojiDict {
var dict = MastodonStatusContent.EmojiDict()
for emoji in emojis ?? [] {
guard let url = URL(string: emoji.url) else { continue }
dict[emoji.shortcode] = url
}
return dict
}
}

View File

@ -23,6 +23,7 @@ extension MastodonUser.Property {
headerStatic: entity.headerStatic, headerStatic: entity.headerStatic,
note: entity.note, note: entity.note,
url: entity.url, url: entity.url,
emojisData: entity.emojis.flatMap { Status.encode(emojis: $0) },
statusesCount: entity.statusesCount, statusesCount: entity.statusesCount,
followingCount: entity.followingCount, followingCount: entity.followingCount,
followersCount: entity.followersCount, followersCount: entity.followersCount,
@ -98,3 +99,5 @@ extension MastodonUser {
return items return items
} }
} }
extension MastodonUser: EmojiContinaer { }

View File

@ -20,6 +20,7 @@ extension Status.Property {
visibility: entity.visibility?.rawValue, visibility: entity.visibility?.rawValue,
sensitive: entity.sensitive ?? false, sensitive: entity.sensitive ?? false,
spoilerText: entity.spoilerText, spoilerText: entity.spoilerText,
emojisData: entity.emojis.flatMap { Status.encode(emojis: $0) },
reblogsCount: NSNumber(value: entity.reblogsCount), reblogsCount: NSNumber(value: entity.reblogsCount),
favouritesCount: NSNumber(value: entity.favouritesCount), favouritesCount: NSNumber(value: entity.favouritesCount),
repliesCount: entity.repliesCount.flatMap { NSNumber(value: $0) }, repliesCount: entity.repliesCount.flatMap { NSNumber(value: $0) },
@ -86,3 +87,5 @@ extension Status {
return items return items
} }
} }
extension Status: EmojiContinaer { }

View File

@ -11,9 +11,21 @@ import ActiveLabel
enum MastodonStatusContent { enum MastodonStatusContent {
static func parse(status: String) throws -> MastodonStatusContent.ParseResult { typealias EmojiShortcode = String
let status = status.replacingOccurrences(of: "<br/>", with: "\n") typealias EmojiDict = [EmojiShortcode: URL]
let rootNode = try Node.parse(document: status)
static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult {
let document: String = {
var content = content
content = content.replacingOccurrences(of: "<br/>", with: "\n")
for (shortcode, url) in emojiDict {
let emojiNode = "<span class=\"emoji\" href=\"\(url.absoluteString)\">\(shortcode)</span>"
let pattern = ":\(shortcode):"
content = content.replacingOccurrences(of: pattern, with: emojiNode)
}
return content
}()
let rootNode = try Node.parse(document: document)
let text = String(rootNode.text) let text = String(rootNode.text)
var activeEntities: [ActiveEntity] = [] var activeEntities: [ActiveEntity] = []
@ -25,7 +37,7 @@ enum MastodonStatusContent {
case .url: case .url:
guard let href = entity.href else { continue } guard let href = entity.href else { continue }
let text = String(entity.text) let text = String(entity.text)
activeEntities.append(ActiveEntity(range: range, type: .url(text, trimmed: entity.hrefEllipsis ?? text, url: href))) activeEntities.append(ActiveEntity(range: range, type: .url(text, trimmed: entity.hrefEllipsis ?? text, url: href, userInfo: nil)))
case .hashtag: case .hashtag:
var userInfo: [AnyHashable: Any] = [:] var userInfo: [AnyHashable: Any] = [:]
entity.href.flatMap { href in entity.href.flatMap { href in
@ -40,30 +52,47 @@ enum MastodonStatusContent {
} }
let mention = String(entity.text).deletingPrefix("@") let mention = String(entity.text).deletingPrefix("@")
activeEntities.append(ActiveEntity(range: range, type: .mention(mention, userInfo: userInfo))) activeEntities.append(ActiveEntity(range: range, type: .mention(mention, userInfo: userInfo)))
default: case .emoji:
var userInfo: [AnyHashable: Any] = [:]
guard let href = entity.href else { continue }
userInfo["href"] = href
let emoji = String(entity.text)
activeEntities.append(ActiveEntity(range: range, type: .emoji(emoji, url: href, userInfo: userInfo)))
case .none:
continue continue
} }
} }
var trimmed = text var trimmed = text
for activeEntity in activeEntities { for activeEntity in activeEntities {
guard case .url = activeEntity.type else { continue } MastodonStatusContent.trimEntity(toot: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities)
MastodonStatusContent.trimEntity(status: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities)
} }
return ParseResult( return ParseResult(
document: status, document: document,
original: text, original: text,
trimmed: trimmed, trimmed: trimmed,
activeEntities: validate(text: trimmed, activeEntities: activeEntities) ? activeEntities : [] activeEntities: activeEntities
) )
} }
static func trimEntity(status: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) { static func trimEntity(toot: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) {
guard case let .url(text, trimmed, _, _) = activeEntity.type else { return } let text: String
let trimmed: String
switch activeEntity.type {
case .url(let _text, let _trimmed, _, _):
text = _text
trimmed = _trimmed
case .emoji(let _text, _, _):
text = _text
trimmed = " "
default:
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: status) else { return } guard let range = Range(activeEntity.range, in: toot) else { return }
status.replaceSubrange(range, with: trimmed) toot.replaceSubrange(range, with: trimmed)
let offset = trimmed.count - text.count let offset = trimmed.count - text.count
activeEntity.range.length += offset activeEntity.range.length += offset
@ -74,19 +103,6 @@ enum MastodonStatusContent {
} }
} }
private static func validate(text: String, activeEntities: [ActiveEntity]) -> Bool {
for activeEntity in activeEntities {
let count = text.utf16.count
let endIndex = activeEntity.range.location + activeEntity.range.length
guard endIndex <= count else {
assertionFailure("Please file issue")
return false
}
}
return true
}
} }
extension String { extension String {
@ -106,6 +122,7 @@ extension MastodonStatusContent {
} }
} }
extension MastodonStatusContent { extension MastodonStatusContent {
class Node { class Node {
@ -154,6 +171,10 @@ extension MastodonStatusContent {
} }
} }
if _classNames.contains("emoji") {
return .emoji
}
return nil return nil
}() }()
self.level = level self.level = level
@ -257,6 +278,7 @@ extension MastodonStatusContent.Node {
case url case url
case mention case mention
case hashtag case hashtag
case emoji
} }
static func entities(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] { static func entities(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {

View File

@ -175,7 +175,7 @@ extension ProfileHeaderViewController {
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] isEditing, note, editingNote in .sink { [weak self] isEditing, note, editingNote in
guard let self = self else { return } guard let self = self else { return }
self.profileHeaderView.bioActiveLabel.configure(note: note ?? "") self.profileHeaderView.bioActiveLabel.configure(note: note ?? "", emojiDict: [:]) // FIXME: custom emoji
self.profileHeaderView.bioTextEditorView.text = editingNote ?? "" self.profileHeaderView.bioTextEditorView.text = editingNote ?? ""
} }
.store(in: &disposeBag) .store(in: &disposeBag)

View File

@ -20,7 +20,7 @@ final class ProfileFieldView: UIView {
let valueActiveLabel: ActiveLabel = { let valueActiveLabel: ActiveLabel = {
let label = ActiveLabel(style: .profileField) let label = ActiveLabel(style: .profileField)
label.configure(content: "value") label.configure(content: "value", emojiDict: [:])
return label return label
}() }()

View File

@ -108,7 +108,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
let label = ActiveLabel(style: .default) let label = ActiveLabel(style: .default)
label.textAlignment = .center label.textAlignment = .center
label.configure(content: "Mastodon is open source software. You can contribute or report issues on GitHub at <a href=\"https://github.com/tootsuite/mastodon\">tootsuite/mastodon</a> (v3.3.0).") label.configure(content: "Mastodon is open source software. You can contribute or report issues on GitHub at <a href=\"https://github.com/tootsuite/mastodon\">tootsuite/mastodon</a> (v3.3.0).", emojiDict: [:])
label.delegate = self label.delegate = self
view.addArrangedSubview(label) view.addArrangedSubview(label)

View File

@ -11,7 +11,7 @@ import AVKit
import ActiveLabel import ActiveLabel
import AlamofireImage import AlamofireImage
protocol StatusViewDelegate: class { protocol StatusViewDelegate: AnyObject {
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton)
func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
@ -69,10 +69,8 @@ final class StatusView: UIView {
return label return label
}() }()
let headerInfoLabel: UILabel = { let headerInfoLabel: ActiveLabel = {
let label = UILabel() let label = ActiveLabel(style: .statusHeader)
label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium))
label.textColor = Asset.Colors.Label.secondary.color
label.text = "Bob reblogged" label.text = "Bob reblogged"
return label return label
}() }()
@ -87,10 +85,8 @@ final class StatusView: UIView {
}() }()
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton() let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
let nameLabel: UILabel = { let nameLabel: ActiveLabel = {
let label = UILabel() let label = ActiveLabel(style: .statusName)
label.font = .systemFont(ofSize: 17, weight: .semibold)
label.textColor = Asset.Colors.Label.primary.color
label.text = "Alice" label.text = "Alice"
return label return label
}() }()

View File

@ -89,9 +89,6 @@ extension APIService.CoreData {
let metions = entity.mentions?.enumerated().compactMap { index, mention -> Mention in let metions = entity.mentions?.enumerated().compactMap { index, mention -> Mention in
Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url), index: index) Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url), index: index)
} }
let emojis = entity.emojis?.compactMap { emoji -> Emoji in
Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category))
}
let tags = entity.tags?.compactMap { tag -> Tag in let tags = entity.tags?.compactMap { tag -> Tag in
let histories = tag.history?.compactMap { history -> History in let histories = tag.history?.compactMap { history -> History in
History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts)) History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts))
@ -121,7 +118,6 @@ extension APIService.CoreData {
replyTo: replyTo, replyTo: replyTo,
poll: poll, poll: poll,
mentions: metions, mentions: metions,
emojis: emojis,
tags: tags, tags: tags,
mediaAttachments: mediaAttachments, mediaAttachments: mediaAttachments,
favouritedBy: (entity.favourited ?? false) ? requestMastodonUser : nil, favouritedBy: (entity.favourited ?? false) ? requestMastodonUser : nil,