feat: display custom emoji for timeline post
This commit is contained in:
parent
8409331dd8
commit
faeb8d99ef
|
@ -1,5 +1,5 @@
|
|||
<?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">
|
||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
|
@ -45,7 +45,6 @@
|
|||
<attribute name="staticURL" attributeType="String"/>
|
||||
<attribute name="url" attributeType="String"/>
|
||||
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="emojis" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="History" representedClassName=".History" syncable="YES">
|
||||
<attribute name="accounts" optional="YES" attributeType="String"/>
|
||||
|
@ -102,6 +101,7 @@
|
|||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="displayName" attributeType="String"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="emojisData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="header" attributeType="String"/>
|
||||
|
@ -197,6 +197,7 @@
|
|||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="emojisData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
|
@ -216,7 +217,6 @@
|
|||
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="status" inverseEntity="Application"/>
|
||||
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="statuses" inverseEntity="MastodonUser"/>
|
||||
<relationship name="bookmarkedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
|
||||
<relationship name="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="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"/>
|
||||
|
@ -267,12 +267,12 @@
|
|||
<element name="Application" positionX="0" positionY="0" width="128" height="104"/>
|
||||
<element name="Attachment" positionX="0" positionY="0" width="128" height="254"/>
|
||||
<element name="DomainBlock" positionX="45" positionY="162" width="128" height="89"/>
|
||||
<element name="Emoji" positionX="0" positionY="0" width="128" height="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="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="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="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||
|
|
|
@ -25,6 +25,9 @@ final public class MastodonUser: NSManagedObject {
|
|||
@NSManaged public private(set) var headerStatic: String?
|
||||
@NSManaged public private(set) var note: String?
|
||||
@NSManaged public private(set) var url: String?
|
||||
|
||||
@NSManaged public private(set) var emojisData: Data?
|
||||
|
||||
@NSManaged public private(set) var statusesCount: NSNumber
|
||||
@NSManaged public private(set) var followingCount: NSNumber
|
||||
@NSManaged public private(set) var followersCount: NSNumber
|
||||
|
@ -88,6 +91,8 @@ extension MastodonUser {
|
|||
user.headerStatic = property.headerStatic
|
||||
user.note = property.note
|
||||
user.url = property.url
|
||||
user.emojisData = property.emojisData
|
||||
|
||||
user.statusesCount = NSNumber(value: property.statusesCount)
|
||||
user.followingCount = NSNumber(value: property.followingCount)
|
||||
user.followersCount = NSNumber(value: property.followersCount)
|
||||
|
@ -151,6 +156,11 @@ extension MastodonUser {
|
|||
self.url = url
|
||||
}
|
||||
}
|
||||
public func update(emojisData: Data?) {
|
||||
if self.emojisData != emojisData {
|
||||
self.emojisData = emojisData
|
||||
}
|
||||
}
|
||||
public func update(statusesCount: Int) {
|
||||
if self.statusesCount.intValue != statusesCount {
|
||||
self.statusesCount = NSNumber(value: statusesCount)
|
||||
|
@ -270,6 +280,7 @@ extension MastodonUser {
|
|||
public let headerStatic: String?
|
||||
public let note: String?
|
||||
public let url: String?
|
||||
public let emojisData: Data?
|
||||
public let statusesCount: Int
|
||||
public let followingCount: Int
|
||||
public let followersCount: Int
|
||||
|
@ -292,6 +303,7 @@ extension MastodonUser {
|
|||
headerStatic: String?,
|
||||
note: String?,
|
||||
url: String?,
|
||||
emojisData: Data?,
|
||||
statusesCount: Int,
|
||||
followingCount: Int,
|
||||
followersCount: Int,
|
||||
|
@ -313,6 +325,7 @@ extension MastodonUser {
|
|||
self.headerStatic = headerStatic
|
||||
self.note = note
|
||||
self.url = url
|
||||
self.emojisData = emojisData
|
||||
self.statusesCount = statusesCount
|
||||
self.followingCount = followingCount
|
||||
self.followersCount = followersCount
|
||||
|
|
|
@ -24,6 +24,8 @@ public final class Status: NSManagedObject {
|
|||
@NSManaged public private(set) var spoilerText: String?
|
||||
@NSManaged public private(set) var application: Application?
|
||||
|
||||
@NSManaged public private(set) var emojisData: Data?
|
||||
|
||||
// Informational
|
||||
@NSManaged public private(set) var reblogsCount: NSNumber
|
||||
@NSManaged public private(set) var favouritesCount: NSNumber
|
||||
|
@ -54,7 +56,6 @@ public final class Status: NSManagedObject {
|
|||
// one-to-many relationship
|
||||
@NSManaged public private(set) var reblogFrom: Set<Status>?
|
||||
@NSManaged public private(set) var mentions: Set<Mention>?
|
||||
@NSManaged public private(set) var emojis: Set<Emoji>?
|
||||
@NSManaged public private(set) var tags: Set<Tag>?
|
||||
@NSManaged public private(set) var homeTimelineIndexes: Set<HomeTimelineIndex>?
|
||||
@NSManaged public private(set) var mediaAttachments: Set<Attachment>?
|
||||
|
@ -77,7 +78,6 @@ extension Status {
|
|||
replyTo: Status?,
|
||||
poll: Poll?,
|
||||
mentions: [Mention]?,
|
||||
emojis: [Emoji]?,
|
||||
tags: [Tag]?,
|
||||
mediaAttachments: [Attachment]?,
|
||||
favouritedBy: MastodonUser?,
|
||||
|
@ -101,6 +101,8 @@ extension Status {
|
|||
status.spoilerText = property.spoilerText
|
||||
status.application = application
|
||||
|
||||
status.emojisData = property.emojisData
|
||||
|
||||
status.reblogsCount = property.reblogsCount
|
||||
status.favouritesCount = property.favouritesCount
|
||||
status.repliesCount = property.repliesCount
|
||||
|
@ -121,9 +123,6 @@ extension Status {
|
|||
if let mentions = 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 {
|
||||
status.mutableSetValue(forKey: #keyPath(Status.tags)).addObjects(from: tags)
|
||||
}
|
||||
|
@ -148,6 +147,12 @@ extension Status {
|
|||
return status
|
||||
}
|
||||
|
||||
public func update(emojisData: Data?) {
|
||||
if self.emojisData != emojisData {
|
||||
self.emojisData = emojisData
|
||||
}
|
||||
}
|
||||
|
||||
public func update(reblogsCount: NSNumber) {
|
||||
if self.reblogsCount.intValue != reblogsCount.intValue {
|
||||
self.reblogsCount = reblogsCount
|
||||
|
@ -248,6 +253,8 @@ extension Status {
|
|||
public let sensitive: Bool
|
||||
public let spoilerText: String?
|
||||
|
||||
public let emojisData: Data?
|
||||
|
||||
public let reblogsCount: NSNumber
|
||||
public let favouritesCount: NSNumber
|
||||
public let repliesCount: NSNumber?
|
||||
|
@ -269,6 +276,7 @@ extension Status {
|
|||
visibility: String?,
|
||||
sensitive: Bool,
|
||||
spoilerText: String?,
|
||||
emojisData: Data?,
|
||||
reblogsCount: NSNumber,
|
||||
favouritesCount: NSNumber,
|
||||
repliesCount: NSNumber?,
|
||||
|
@ -288,6 +296,7 @@ extension Status {
|
|||
self.visibility = visibility
|
||||
self.sensitive = sensitive
|
||||
self.spoilerText = spoilerText
|
||||
self.emojisData = emojisData
|
||||
self.reblogsCount = reblogsCount
|
||||
self.favouritesCount = favouritesCount
|
||||
self.repliesCount = repliesCount
|
||||
|
|
|
@ -400,6 +400,7 @@
|
|||
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
|
||||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.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 */; };
|
||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1594,6 +1596,7 @@
|
|||
DB6D9F6E2635807F008423CD /* Setting.swift */,
|
||||
DB6D9F4826353FD6008423CD /* Subscription.swift */,
|
||||
DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */,
|
||||
DBAFB7342645463500371D5F /* Emojis.swift */,
|
||||
);
|
||||
path = CoreDataStack;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3199,6 +3202,7 @@
|
|||
DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */,
|
||||
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
|
||||
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */,
|
||||
DBAFB7352645463500371D5F /* Emojis.swift in Sources */,
|
||||
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
|
||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
|
||||
|
@ -3913,8 +3917,8 @@
|
|||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.0.0;
|
||||
kind = exactVersion;
|
||||
version = 5.0.1;
|
||||
};
|
||||
};
|
||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d6cf96e0ca4f2269021bcf8f11381ab57897f84a",
|
||||
"version": "4.0.0"
|
||||
"revision": "40e104063d825d1125ef4b8eeb6460eba8a57483",
|
||||
"version": "5.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -68,7 +68,8 @@ extension ComposeStatusSection {
|
|||
}()
|
||||
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||
// set text
|
||||
cell.statusView.activeTextLabel.configure(content: status.content)
|
||||
//status.emoji
|
||||
cell.statusView.activeTextLabel.configure(content: status.content, emojiDict: [:])
|
||||
// set date
|
||||
cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow
|
||||
|
||||
|
|
|
@ -158,10 +158,11 @@ extension StatusSection {
|
|||
.store(in: &cell.disposeBag)
|
||||
|
||||
// set name username
|
||||
cell.statusView.nameLabel.text = {
|
||||
let nameText: String = {
|
||||
let author = (status.reblog ?? status).author
|
||||
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
|
||||
// set avatar
|
||||
if let reblog = status.reblog {
|
||||
|
@ -176,7 +177,10 @@ extension StatusSection {
|
|||
}
|
||||
|
||||
// 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
|
||||
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
||||
|
@ -569,15 +573,16 @@ extension StatusSection {
|
|||
if status.reblog != nil {
|
||||
cell.statusView.headerContainerView.isHidden = false
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
|
||||
cell.statusView.headerInfoLabel.text = {
|
||||
let headerText: String = {
|
||||
let author = status.author
|
||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Common.Controls.Status.userReblogged(name)
|
||||
}()
|
||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict)
|
||||
} else if status.inReplyToID != nil {
|
||||
cell.statusView.headerContainerView.isHidden = false
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||
cell.statusView.headerInfoLabel.text = {
|
||||
let headerText: String = {
|
||||
guard let replyTo = status.replyTo else {
|
||||
return L10n.Common.Controls.Status.userRepliedTo("-")
|
||||
}
|
||||
|
@ -585,6 +590,7 @@ extension StatusSection {
|
|||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Common.Controls.Status.userRepliedTo(name)
|
||||
}()
|
||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
|
||||
} else {
|
||||
cell.statusView.headerContainerView.isHidden = true
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ extension ActiveLabel {
|
|||
|
||||
enum Style {
|
||||
case `default`
|
||||
case statusHeader
|
||||
case statusName
|
||||
case profileField
|
||||
}
|
||||
|
||||
|
@ -25,6 +27,7 @@ extension ActiveLabel {
|
|||
mentionColor = Asset.Colors.Label.highlight.color
|
||||
hashtagColor = Asset.Colors.Label.highlight.color
|
||||
URLColor = Asset.Colors.Label.highlight.color
|
||||
emojiPlaceholderColor = .systemFill
|
||||
#if DEBUG
|
||||
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
||||
#endif
|
||||
|
@ -33,6 +36,14 @@ extension ActiveLabel {
|
|||
case .default:
|
||||
font = .preferredFont(forTextStyle: .body)
|
||||
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:
|
||||
font = .preferredFont(forTextStyle: .body)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
|
@ -44,9 +55,10 @@ extension ActiveLabel {
|
|||
|
||||
extension ActiveLabel {
|
||||
/// status content
|
||||
func configure(content: String) {
|
||||
func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||
activeEntities.removeAll()
|
||||
if let parseResult = try? MastodonStatusContent.parse(status: content) {
|
||||
|
||||
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
|
||||
text = parseResult.trimmed
|
||||
activeEntities = parseResult.activeEntities
|
||||
} else {
|
||||
|
@ -55,8 +67,8 @@ extension ActiveLabel {
|
|||
}
|
||||
|
||||
/// account note
|
||||
func configure(note: String) {
|
||||
configure(content: note)
|
||||
func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||
configure(content: note, emojiDict: emojiDict)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ extension MastodonUser.Property {
|
|||
headerStatic: entity.headerStatic,
|
||||
note: entity.note,
|
||||
url: entity.url,
|
||||
emojisData: entity.emojis.flatMap { Status.encode(emojis: $0) },
|
||||
statusesCount: entity.statusesCount,
|
||||
followingCount: entity.followingCount,
|
||||
followersCount: entity.followersCount,
|
||||
|
@ -98,3 +99,5 @@ extension MastodonUser {
|
|||
return items
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonUser: EmojiContinaer { }
|
||||
|
|
|
@ -20,6 +20,7 @@ extension Status.Property {
|
|||
visibility: entity.visibility?.rawValue,
|
||||
sensitive: entity.sensitive ?? false,
|
||||
spoilerText: entity.spoilerText,
|
||||
emojisData: entity.emojis.flatMap { Status.encode(emojis: $0) },
|
||||
reblogsCount: NSNumber(value: entity.reblogsCount),
|
||||
favouritesCount: NSNumber(value: entity.favouritesCount),
|
||||
repliesCount: entity.repliesCount.flatMap { NSNumber(value: $0) },
|
||||
|
@ -86,3 +87,5 @@ extension Status {
|
|||
return items
|
||||
}
|
||||
}
|
||||
|
||||
extension Status: EmojiContinaer { }
|
||||
|
|
|
@ -11,9 +11,21 @@ import ActiveLabel
|
|||
|
||||
enum MastodonStatusContent {
|
||||
|
||||
static func parse(status: String) throws -> MastodonStatusContent.ParseResult {
|
||||
let status = status.replacingOccurrences(of: "<br/>", with: "\n")
|
||||
let rootNode = try Node.parse(document: status)
|
||||
typealias EmojiShortcode = String
|
||||
typealias EmojiDict = [EmojiShortcode: URL]
|
||||
|
||||
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)
|
||||
|
||||
var activeEntities: [ActiveEntity] = []
|
||||
|
@ -25,7 +37,7 @@ enum MastodonStatusContent {
|
|||
case .url:
|
||||
guard let href = entity.href else { continue }
|
||||
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:
|
||||
var userInfo: [AnyHashable: Any] = [:]
|
||||
entity.href.flatMap { href in
|
||||
|
@ -40,30 +52,47 @@ enum MastodonStatusContent {
|
|||
}
|
||||
let mention = String(entity.text).deletingPrefix("@")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
var trimmed = text
|
||||
for activeEntity in activeEntities {
|
||||
guard case .url = activeEntity.type else { continue }
|
||||
MastodonStatusContent.trimEntity(status: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities)
|
||||
MastodonStatusContent.trimEntity(toot: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities)
|
||||
}
|
||||
|
||||
return ParseResult(
|
||||
document: status,
|
||||
document: document,
|
||||
original: text,
|
||||
trimmed: trimmed,
|
||||
activeEntities: validate(text: trimmed, activeEntities: activeEntities) ? activeEntities : []
|
||||
activeEntities: activeEntities
|
||||
)
|
||||
}
|
||||
|
||||
static func trimEntity(status: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) {
|
||||
guard case let .url(text, trimmed, _, _) = activeEntity.type else { return }
|
||||
static func trimEntity(toot: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) {
|
||||
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 range = Range(activeEntity.range, in: status) else { return }
|
||||
status.replaceSubrange(range, with: trimmed)
|
||||
guard let range = Range(activeEntity.range, in: toot) else { return }
|
||||
toot.replaceSubrange(range, with: trimmed)
|
||||
|
||||
let offset = trimmed.count - text.count
|
||||
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 {
|
||||
|
@ -106,6 +122,7 @@ extension MastodonStatusContent {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
extension MastodonStatusContent {
|
||||
|
||||
class Node {
|
||||
|
@ -154,6 +171,10 @@ extension MastodonStatusContent {
|
|||
}
|
||||
}
|
||||
|
||||
if _classNames.contains("emoji") {
|
||||
return .emoji
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
self.level = level
|
||||
|
@ -257,6 +278,7 @@ extension MastodonStatusContent.Node {
|
|||
case url
|
||||
case mention
|
||||
case hashtag
|
||||
case emoji
|
||||
}
|
||||
|
||||
static func entities(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
|
||||
|
|
|
@ -175,7 +175,7 @@ extension ProfileHeaderViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isEditing, note, editingNote in
|
||||
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 ?? ""
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -20,7 +20,7 @@ final class ProfileFieldView: UIView {
|
|||
|
||||
let valueActiveLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .profileField)
|
||||
label.configure(content: "value")
|
||||
label.configure(content: "value", emojiDict: [:])
|
||||
return label
|
||||
}()
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
|
||||
let label = ActiveLabel(style: .default)
|
||||
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
|
||||
|
||||
view.addArrangedSubview(label)
|
||||
|
|
|
@ -11,7 +11,7 @@ import AVKit
|
|||
import ActiveLabel
|
||||
import AlamofireImage
|
||||
|
||||
protocol StatusViewDelegate: class {
|
||||
protocol StatusViewDelegate: AnyObject {
|
||||
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
|
||||
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
|
||||
|
@ -69,10 +69,8 @@ final class StatusView: UIView {
|
|||
return label
|
||||
}()
|
||||
|
||||
let headerInfoLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium))
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
let headerInfoLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusHeader)
|
||||
label.text = "Bob reblogged"
|
||||
return label
|
||||
}()
|
||||
|
@ -87,10 +85,8 @@ final class StatusView: UIView {
|
|||
}()
|
||||
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
|
||||
|
||||
let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
let nameLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusName)
|
||||
label.text = "Alice"
|
||||
return label
|
||||
}()
|
||||
|
|
|
@ -89,9 +89,6 @@ extension APIService.CoreData {
|
|||
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)
|
||||
}
|
||||
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 histories = tag.history?.compactMap { history -> History in
|
||||
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,
|
||||
poll: poll,
|
||||
mentions: metions,
|
||||
emojis: emojis,
|
||||
tags: tags,
|
||||
mediaAttachments: mediaAttachments,
|
||||
favouritedBy: (entity.favourited ?? false) ? requestMastodonUser : nil,
|
||||
|
|
Loading…
Reference in New Issue