Support Custom Emojis (#61)

* Support Custom Emojis

* Update EmojiText

* Update EmojiText

* Use EmojiText in StatusEditorAutoCompleteView

* Update EmojiText

* Display Account displayName without emojis in navigation title

Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
David Walter 2023-01-12 06:58:04 +01:00 committed by GitHub
parent 150cb5a8c5
commit 3acd5aced4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 78 additions and 68 deletions

View file

@ -1,5 +1,14 @@
{ {
"pins" : [ "pins" : [
{
"identity" : "emojitext",
"kind" : "remoteSourceControl",
"location" : "https://github.com/divadretlaw/EmojiText",
"state" : {
"revision" : "f349e481499d2c832ab9d2dc28af238e53b1f9b4",
"version" : "1.1.0"
}
},
{ {
"identity" : "html2markdown", "identity" : "html2markdown",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@ -23,8 +32,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke", "location" : "https://github.com/kean/Nuke",
"state" : { "state" : {
"revision" : "81f6a3dea0c8ce3b87389c241c48601be07af0b1", "revision" : "2e9337168d08acccf72c039bf9324be24a1cf7d7",
"version" : "11.5.1" "version" : "11.5.3"
} }
}, },
{ {
@ -41,8 +50,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git", "location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : { "state" : {
"revision" : "6778575285177365cbad3e5b8a72f2a20583cfec", "revision" : "f707b8680cddb96dc1855632340a572ef37bbb98",
"version" : "2.4.3" "version" : "2.5.3"
} }
}, },
{ {

View file

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
import DesignSystem import DesignSystem
import Env import Env
import EmojiText
struct AppAccountView: View { struct AppAccountView: View {
@EnvironmentObject private var routeurPath: RouterPath @EnvironmentObject private var routeurPath: RouterPath
@ -21,7 +22,7 @@ struct AppAccountView: View {
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let account = viewModel.account { if let account = viewModel.account {
account.displayNameWithEmojis EmojiText(account.safeDisplayName, emojis: account.emojis)
Text("\(account.username)@\(viewModel.appAccount.server)") Text("\(account.username)@\(viewModel.appAccount.server)")
.font(.subheadline) .font(.subheadline)
.foregroundColor(.gray) .foregroundColor(.gray)

View file

@ -4,6 +4,7 @@ import DesignSystem
import Env import Env
import Shimmer import Shimmer
import NukeUI import NukeUI
import EmojiText
struct AccountDetailHeaderView: View { struct AccountDetailHeaderView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@ -108,7 +109,7 @@ struct AccountDetailHeaderView: View {
accountAvatarView accountAvatarView
HStack { HStack {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
account.displayNameWithEmojis EmojiText(account.safeDisplayName, emojis: account.emojis)
.font(.headline) .font(.headline)
Text("@\(account.acct)") Text("@\(account.acct)")
.font(.callout) .font(.callout)
@ -120,7 +121,7 @@ struct AccountDetailHeaderView: View {
relationship: relationship)) relationship: relationship))
} }
} }
Text(account.note.asSafeAttributedString) EmojiText(account.note, emojis: account.emojis)
.font(.body) .font(.body)
.padding(.top, 8) .padding(.top, 8)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in

View file

@ -5,6 +5,7 @@ import Status
import Shimmer import Shimmer
import DesignSystem import DesignSystem
import Env import Env
import EmojiText
public struct AccountDetailView: View { public struct AccountDetailView: View {
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
@ -217,7 +218,7 @@ public struct AccountDetailView: View {
Image(systemName: "checkmark.seal") Image(systemName: "checkmark.seal")
.foregroundColor(Color.green.opacity(0.80)) .foregroundColor(Color.green.opacity(0.80))
} }
Text(field.value.asSafeAttributedString) EmojiText(field.value, emojis: viewModel.account?.emojis ?? [])
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
} }
.font(.body) .font(.body)
@ -335,7 +336,8 @@ public struct AccountDetailView: View {
if scrollOffset < -200 { if scrollOffset < -200 {
switch viewModel.accountState { switch viewModel.accountState {
case let .data(account): case let .data(account):
account.displayNameWithEmojis.font(.headline) EmojiText(account.safeDisplayName, emojis: account.emojis)
.font(.headline)
default: default:
EmptyView() EmptyView()
} }

View file

@ -3,6 +3,7 @@ import Models
import Network import Network
import DesignSystem import DesignSystem
import Env import Env
import EmojiText
@MainActor @MainActor
public class AccountsListRowViewModel: ObservableObject { public class AccountsListRowViewModel: ObservableObject {
@ -32,13 +33,13 @@ public struct AccountsListRow: View {
HStack(alignment: .top) { HStack(alignment: .top) {
AvatarView(url: viewModel.account.avatar, size: .status) AvatarView(url: viewModel.account.avatar, size: .status)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
viewModel.account.displayNameWithEmojis EmojiText(viewModel.account.safeDisplayName, emojis: viewModel.account.emojis)
.font(.subheadline) .font(.subheadline)
.fontWeight(.semibold) .fontWeight(.semibold)
Text("@\(viewModel.account.acct)") Text("@\(viewModel.account.acct)")
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)
Text(viewModel.account.note.asSafeAttributedString) EmojiText(viewModel.account.note, emojis: viewModel.account.emojis)
.font(.footnote) .font(.footnote)
.lineLimit(3) .lineLimit(3)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in

View file

@ -17,7 +17,9 @@ let package = Package(
.package(name: "Models", path: "../Models"), .package(name: "Models", path: "../Models"),
.package(name: "Env", path: "../Env"), .package(name: "Env", path: "../Env"),
.package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0"), .package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0"),
.package(url: "https://github.com/kean/Nuke", from: "11.5.0")], .package(url: "https://github.com/kean/Nuke", from: "11.5.0"),
.package(url: "https://github.com/divadretlaw/EmojiText", from: "1.1.0")
],
targets: [ targets: [
.target( .target(
name: "DesignSystem", name: "DesignSystem",
@ -26,7 +28,8 @@ let package = Package(
.product(name: "Env", package: "Env"), .product(name: "Env", package: "Env"),
.product(name: "Shimmer", package: "SwiftUI-Shimmer"), .product(name: "Shimmer", package: "SwiftUI-Shimmer"),
.product(name: "NukeUI", package: "Nuke"), .product(name: "NukeUI", package: "Nuke"),
.product(name: "Nuke", package: "Nuke") .product(name: "Nuke", package: "Nuke"),
.product(name: "EmojiText", package: "EmojiText")
]), ]),
] ]
) )

View file

@ -3,7 +3,6 @@ import SwiftUI
import NukeUI import NukeUI
import Models import Models
@MainActor
extension Account { extension Account {
private struct Part: Identifiable { private struct Part: Identifiable {
let id = UUID().uuidString let id = UUID().uuidString
@ -16,35 +15,12 @@ extension Account {
} }
return displayName return displayName
} }
@ViewBuilder public var displayNameWithoutEmojis: String {
public var displayNameWithEmojis: some View { var name = safeDisplayName
if displayName.isEmpty { for emoji in emojis {
Text(safeDisplayName) name = name.replacingOccurrences(of: ":\(emoji.shortcode):", with: "")
}
let splittedDisplayName = displayName.split(separator: ":").map{ Part(value: $0) }
HStack(spacing: 0) {
if displayName.isEmpty {
Text(" ")
}
ForEach(splittedDisplayName, id: \.id) { part in
if let emoji = emojis.first(where: { $0.shortcode == part.value }) {
LazyImage(url: emoji.url) { state in
if let image = state.image {
image
.resizingMode(.aspectFit)
} else if state.isLoading {
ProgressView()
} else {
ProgressView()
}
}
.processors([.resize(size: .init(width: 20, height: 20))])
.frame(width: 20, height: 20)
} else {
Text(part.value)
}
}
} }
return name.split(separator: " ", omittingEmptySubsequences: true).joined(separator: " ")
} }
} }

View file

@ -0,0 +1,11 @@
import Foundation
import EmojiText
import Models
import HTML2Markdown
public extension EmojiText {
init(_ string: HTMLString, emojis: [Emoji]) {
let markdown = string.asMarkdown
self.init(markdown: markdown, emojis: emojis.map { RemoteEmoji(shortcode: $0.shortcode, url: $0.url) })
}
}

View file

@ -2,6 +2,7 @@ import SwiftUI
import Models import Models
import DesignSystem import DesignSystem
import Network import Network
import EmojiText
public struct ListEditView: View { public struct ListEditView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@ -30,7 +31,7 @@ public struct ListEditView: View {
HStack { HStack {
AvatarView(url: account.avatar, size: .status) AvatarView(url: account.avatar, size: .status)
VStack(alignment: .leading) { VStack(alignment: .leading) {
account.displayNameWithEmojis EmojiText(account.safeDisplayName, emojis: account.emojis)
Text("@\(account.acct)") Text("@\(account.acct)")
.foregroundColor(.gray) .foregroundColor(.gray)
.font(.footnote) .font(.footnote)

View file

@ -10,6 +10,8 @@ extension HTMLString {
do { do {
let dom = try HTMLParser().parse(html: self) let dom = try HTMLParser().parse(html: self)
return dom.toMarkdown() return dom.toMarkdown()
// Add space between hashtags and mentions that follow each other
.replacingOccurrences(of: ")[", with: ") [")
} catch { } catch {
return self return self
} }
@ -44,9 +46,7 @@ extension HTMLString {
public var asSafeAttributedString: AttributedString { public var asSafeAttributedString: AttributedString {
do { do {
// Add space between hashtags and mentions that follow each other
let markdown = asMarkdown let markdown = asMarkdown
.replacingOccurrences(of: ")[", with: ") [")
let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true, let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true,
interpretedSyntax: .inlineOnlyPreservingWhitespace) interpretedSyntax: .inlineOnlyPreservingWhitespace)
return try AttributedString(markdown: markdown, options: options) return try AttributedString(markdown: markdown, options: options)

View file

@ -3,6 +3,7 @@ import Models
import DesignSystem import DesignSystem
import Status import Status
import Env import Env
import EmojiText
struct NotificationRowView: View { struct NotificationRowView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@ -50,18 +51,18 @@ struct NotificationRowView: View {
private func makeMainLabel(type: Models.Notification.NotificationType) -> some View { private func makeMainLabel(type: Models.Notification.NotificationType) -> some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text(notification.account.safeDisplayName) EmojiText(notification.account.safeDisplayName, emojis: notification.account.emojis)
.font(.subheadline) .append {
.fontWeight(.semibold) + Text(" ") +
Text(" ") + Text(type.label())
Text(type.label()) .font(.subheadline) +
.font(.subheadline) + Text("")
Text("") .font(.footnote)
.font(.footnote) .foregroundColor(.gray) +
.foregroundColor(.gray) + Text(notification.createdAt.formatted)
Text(notification.createdAt.formatted) .font(.footnote)
.font(.footnote) .foregroundColor(.gray)
.foregroundColor(.gray) }
Spacer() Spacer()
} }
} }
@ -86,7 +87,7 @@ struct NotificationRowView: View {
.foregroundColor(.gray) .foregroundColor(.gray)
if type == .follow { if type == .follow {
Text(notification.account.note.asSafeAttributedString) EmojiText(notification.account.note, emojis: notification.account.emojis)
.lineLimit(3) .lineLimit(3)
.font(.callout) .font(.callout)
.foregroundColor(.gray) .foregroundColor(.gray)

View file

@ -62,7 +62,7 @@ class StatusDetailViewModel: ObservableObject {
let status: Status = try await client.get(endpoint: Statuses.status(id: statusId)) let status: Status = try await client.get(endpoint: Statuses.status(id: statusId))
let context: StatusContext = try await client.get(endpoint: Statuses.context(id: statusId)) let context: StatusContext = try await client.get(endpoint: Statuses.context(id: statusId))
state = .display(status: status, context: context) state = .display(status: status, context: context)
title = "Post from \(status.account.displayName)" title = "Post from \(status.account.displayNameWithoutEmojis)"
} catch { } catch {
state = .error(error: error) state = .error(error: error)
} }

View file

@ -1,6 +1,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import DesignSystem import DesignSystem
import EmojiText
struct StatusEditorAutoCompleteView: View { struct StatusEditorAutoCompleteView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@ -31,7 +32,7 @@ struct StatusEditorAutoCompleteView: View {
HStack { HStack {
AvatarView(url: account.avatar, size: .badge) AvatarView(url: account.avatar, size: .badge)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(account.displayName) EmojiText(account.safeDisplayName, emojis: account.emojis)
.font(.footnote) .font(.footnote)
.foregroundColor(theme.labelColor) .foregroundColor(theme.labelColor)
Text("@\(account.acct)") Text("@\(account.acct)")

View file

@ -7,6 +7,7 @@ import Models
import Network import Network
import PhotosUI import PhotosUI
import NukeUI import NukeUI
import EmojiText
public struct StatusEditorView: View { public struct StatusEditorView: View {
@EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var preferences: UserPreferences

View file

@ -33,9 +33,9 @@ extension StatusEditorViewModel {
case .edit: case .edit:
return "Editing your post" return "Editing your post"
case let .replyTo(status): case let .replyTo(status):
return "Replying to \(status.reblog?.account.displayName ?? status.account.displayName)" return "Replying to \(status.reblog?.account.displayNameWithoutEmojis ?? status.account.displayNameWithoutEmojis)"
case let .quote(status): case let .quote(status):
return "Quote of \(status.reblog?.account.displayName ?? status.account.displayName)" return "Quote of \(status.reblog?.account.displayNameWithoutEmojis ?? status.account.displayNameWithoutEmojis)"
} }
} }
} }

View file

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
import Models import Models
import DesignSystem import DesignSystem
import EmojiText
@MainActor @MainActor
public struct StatusEmbededView: View { public struct StatusEmbededView: View {
@ -34,7 +35,7 @@ public struct StatusEmbededView: View {
HStack(alignment: .center) { HStack(alignment: .center) {
AvatarView(url: account.avatar, size: .embed) AvatarView(url: account.avatar, size: .embed)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
status.account.displayNameWithEmojis EmojiText(status.account.safeDisplayName, emojis: account.emojis)
.font(.footnote) .font(.footnote)
.fontWeight(.semibold) .fontWeight(.semibold)
Group { Group {

View file

@ -4,6 +4,7 @@ import Env
import DesignSystem import DesignSystem
import Network import Network
import Shimmer import Shimmer
import EmojiText
public struct StatusRowView: View { public struct StatusRowView: View {
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
@ -90,7 +91,7 @@ public struct StatusRowView: View {
HStack(spacing: 2) { HStack(spacing: 2) {
Image(systemName:"arrow.left.arrow.right.circle.fill") Image(systemName:"arrow.left.arrow.right.circle.fill")
AvatarView(url: viewModel.status.account.avatar, size: .boost) AvatarView(url: viewModel.status.account.avatar, size: .boost)
viewModel.status.account.displayNameWithEmojis EmojiText(viewModel.status.account.safeDisplayName, emojis: viewModel.status.account.emojis)
Text("boosted") Text("boosted")
} }
.font(.footnote) .font(.footnote)
@ -164,7 +165,7 @@ public struct StatusRowView: View {
private func makeStatusContentView(status: AnyStatus) -> some View { private func makeStatusContentView(status: AnyStatus) -> some View {
Group { Group {
if !status.spoilerText.isEmpty { if !status.spoilerText.isEmpty {
Text(status.spoilerText) EmojiText(status.spoilerText, emojis: status.emojis)
.font(.body) .font(.body)
Button { Button {
withAnimation { withAnimation {
@ -177,7 +178,7 @@ public struct StatusRowView: View {
} }
if !viewModel.displaySpoiler { if !viewModel.displaySpoiler {
HStack { HStack {
Text(status.content.asSafeAttributedString) EmojiText(status.content, emojis: status.emojis)
.font(.body) .font(.body)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
routeurPath.handleStatus(status: status, url: url) routeurPath.handleStatus(status: status, url: url)
@ -233,7 +234,7 @@ public struct StatusRowView: View {
AvatarView(url: status.account.avatar, size: .status) AvatarView(url: status.account.avatar, size: .status)
} }
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
status.account.displayNameWithEmojis EmojiText(status.account.safeDisplayName, emojis: status.account.emojis)
.font(.headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
Group { Group {