From 3acd5aced4ac3fa93e1b90950eb5368cad1fa2ca Mon Sep 17 00:00:00 2001 From: David Walter Date: Thu, 12 Jan 2023 06:58:04 +0100 Subject: [PATCH] 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 --- .../xcshareddata/swiftpm/Package.resolved | 17 ++++++--- .../App/AppAccounts/AppAccountView.swift | 3 +- .../Account/AccountDetailHeaderView.swift | 5 +-- .../Sources/Account/AccountDetailView.swift | 6 ++-- .../AccountsLIst/AccountsListRow.swift | 5 +-- Packages/DesignSystem/Package.swift | 7 ++-- .../Sources/DesignSystem/AccountExt.swift | 36 ++++--------------- .../DesignSystem/Views/EmojiText.swift | 11 ++++++ .../Sources/Lists/Edit/ListEditView.swift | 3 +- .../Sources/Models/Alias/HTMLString.swift | 4 +-- .../Notifications/NotificationRowView.swift | 27 +++++++------- .../Status/Detail/StatusDetailViewModel.swift | 2 +- .../StatusEditorAutoCompleteView.swift | 3 +- .../Status/Editor/StatusEditorView.swift | 1 + .../Editor/StatusEditorViewModelMode.swift | 4 +-- .../Status/Embed/StatusEmbededView.swift | 3 +- .../Sources/Status/Row/StatusRowView.swift | 9 ++--- 17 files changed, 78 insertions(+), 68 deletions(-) create mode 100644 Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift diff --git a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 508c2b4a..60f2a286 100644 --- a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "emojitext", + "kind" : "remoteSourceControl", + "location" : "https://github.com/divadretlaw/EmojiText", + "state" : { + "revision" : "f349e481499d2c832ab9d2dc28af238e53b1f9b4", + "version" : "1.1.0" + } + }, { "identity" : "html2markdown", "kind" : "remoteSourceControl", @@ -23,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kean/Nuke", "state" : { - "revision" : "81f6a3dea0c8ce3b87389c241c48601be07af0b1", - "version" : "11.5.1" + "revision" : "2e9337168d08acccf72c039bf9324be24a1cf7d7", + "version" : "11.5.3" } }, { @@ -41,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup.git", "state" : { - "revision" : "6778575285177365cbad3e5b8a72f2a20583cfec", - "version" : "2.4.3" + "revision" : "f707b8680cddb96dc1855632340a572ef37bbb98", + "version" : "2.5.3" } }, { diff --git a/IceCubesApp/App/AppAccounts/AppAccountView.swift b/IceCubesApp/App/AppAccounts/AppAccountView.swift index 13249ba7..a02b6ff3 100644 --- a/IceCubesApp/App/AppAccounts/AppAccountView.swift +++ b/IceCubesApp/App/AppAccounts/AppAccountView.swift @@ -1,6 +1,7 @@ import SwiftUI import DesignSystem import Env +import EmojiText struct AppAccountView: View { @EnvironmentObject private var routeurPath: RouterPath @@ -21,7 +22,7 @@ struct AppAccountView: View { } VStack(alignment: .leading) { if let account = viewModel.account { - account.displayNameWithEmojis + EmojiText(account.safeDisplayName, emojis: account.emojis) Text("\(account.username)@\(viewModel.appAccount.server)") .font(.subheadline) .foregroundColor(.gray) diff --git a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift index 2e223c62..27994c67 100644 --- a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift +++ b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift @@ -4,6 +4,7 @@ import DesignSystem import Env import Shimmer import NukeUI +import EmojiText struct AccountDetailHeaderView: View { @EnvironmentObject private var theme: Theme @@ -108,7 +109,7 @@ struct AccountDetailHeaderView: View { accountAvatarView HStack { VStack(alignment: .leading, spacing: 0) { - account.displayNameWithEmojis + EmojiText(account.safeDisplayName, emojis: account.emojis) .font(.headline) Text("@\(account.acct)") .font(.callout) @@ -120,7 +121,7 @@ struct AccountDetailHeaderView: View { relationship: relationship)) } } - Text(account.note.asSafeAttributedString) + EmojiText(account.note, emojis: account.emojis) .font(.body) .padding(.top, 8) .environment(\.openURL, OpenURLAction { url in diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index 7023899d..996972f0 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -5,6 +5,7 @@ import Status import Shimmer import DesignSystem import Env +import EmojiText public struct AccountDetailView: View { @Environment(\.redactionReasons) private var reasons @@ -217,7 +218,7 @@ public struct AccountDetailView: View { Image(systemName: "checkmark.seal") .foregroundColor(Color.green.opacity(0.80)) } - Text(field.value.asSafeAttributedString) + EmojiText(field.value, emojis: viewModel.account?.emojis ?? []) .foregroundColor(theme.tintColor) } .font(.body) @@ -335,7 +336,8 @@ public struct AccountDetailView: View { if scrollOffset < -200 { switch viewModel.accountState { case let .data(account): - account.displayNameWithEmojis.font(.headline) + EmojiText(account.safeDisplayName, emojis: account.emojis) + .font(.headline) default: EmptyView() } diff --git a/Packages/Account/Sources/Account/AccountsLIst/AccountsListRow.swift b/Packages/Account/Sources/Account/AccountsLIst/AccountsListRow.swift index b07ea302..80ecc963 100644 --- a/Packages/Account/Sources/Account/AccountsLIst/AccountsListRow.swift +++ b/Packages/Account/Sources/Account/AccountsLIst/AccountsListRow.swift @@ -3,6 +3,7 @@ import Models import Network import DesignSystem import Env +import EmojiText @MainActor public class AccountsListRowViewModel: ObservableObject { @@ -32,13 +33,13 @@ public struct AccountsListRow: View { HStack(alignment: .top) { AvatarView(url: viewModel.account.avatar, size: .status) VStack(alignment: .leading, spacing: 2) { - viewModel.account.displayNameWithEmojis + EmojiText(viewModel.account.safeDisplayName, emojis: viewModel.account.emojis) .font(.subheadline) .fontWeight(.semibold) Text("@\(viewModel.account.acct)") .font(.footnote) .foregroundColor(.gray) - Text(viewModel.account.note.asSafeAttributedString) + EmojiText(viewModel.account.note, emojis: viewModel.account.emojis) .font(.footnote) .lineLimit(3) .environment(\.openURL, OpenURLAction { url in diff --git a/Packages/DesignSystem/Package.swift b/Packages/DesignSystem/Package.swift index af985001..a27deb3e 100644 --- a/Packages/DesignSystem/Package.swift +++ b/Packages/DesignSystem/Package.swift @@ -17,7 +17,9 @@ let package = Package( .package(name: "Models", path: "../Models"), .package(name: "Env", path: "../Env"), .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: [ .target( name: "DesignSystem", @@ -26,7 +28,8 @@ let package = Package( .product(name: "Env", package: "Env"), .product(name: "Shimmer", package: "SwiftUI-Shimmer"), .product(name: "NukeUI", package: "Nuke"), - .product(name: "Nuke", package: "Nuke") + .product(name: "Nuke", package: "Nuke"), + .product(name: "EmojiText", package: "EmojiText") ]), ] ) diff --git a/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift b/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift index 236181d6..8c24ab8b 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/AccountExt.swift @@ -3,7 +3,6 @@ import SwiftUI import NukeUI import Models -@MainActor extension Account { private struct Part: Identifiable { let id = UUID().uuidString @@ -16,35 +15,12 @@ extension Account { } return displayName } - - @ViewBuilder - public var displayNameWithEmojis: some View { - if displayName.isEmpty { - Text(safeDisplayName) - } - 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) - } - } + + public var displayNameWithoutEmojis: String { + var name = safeDisplayName + for emoji in emojis { + name = name.replacingOccurrences(of: ":\(emoji.shortcode):", with: "") } + return name.split(separator: " ", omittingEmptySubsequences: true).joined(separator: " ") } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift new file mode 100644 index 00000000..baa605e2 --- /dev/null +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift @@ -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) }) + } +} diff --git a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift index 3bb7c689..fe89766b 100644 --- a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift +++ b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift @@ -2,6 +2,7 @@ import SwiftUI import Models import DesignSystem import Network +import EmojiText public struct ListEditView: View { @Environment(\.dismiss) private var dismiss @@ -30,7 +31,7 @@ public struct ListEditView: View { HStack { AvatarView(url: account.avatar, size: .status) VStack(alignment: .leading) { - account.displayNameWithEmojis + EmojiText(account.safeDisplayName, emojis: account.emojis) Text("@\(account.acct)") .foregroundColor(.gray) .font(.footnote) diff --git a/Packages/Models/Sources/Models/Alias/HTMLString.swift b/Packages/Models/Sources/Models/Alias/HTMLString.swift index 6109688c..ea954f55 100644 --- a/Packages/Models/Sources/Models/Alias/HTMLString.swift +++ b/Packages/Models/Sources/Models/Alias/HTMLString.swift @@ -10,6 +10,8 @@ extension HTMLString { do { let dom = try HTMLParser().parse(html: self) return dom.toMarkdown() + // Add space between hashtags and mentions that follow each other + .replacingOccurrences(of: ")[", with: ") [") } catch { return self } @@ -44,9 +46,7 @@ extension HTMLString { public var asSafeAttributedString: AttributedString { do { - // Add space between hashtags and mentions that follow each other let markdown = asMarkdown - .replacingOccurrences(of: ")[", with: ") [") let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true, interpretedSyntax: .inlineOnlyPreservingWhitespace) return try AttributedString(markdown: markdown, options: options) diff --git a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift index 90151198..fac25ee1 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift @@ -3,6 +3,7 @@ import Models import DesignSystem import Status import Env +import EmojiText struct NotificationRowView: View { @EnvironmentObject private var theme: Theme @@ -50,18 +51,18 @@ struct NotificationRowView: View { private func makeMainLabel(type: Models.Notification.NotificationType) -> some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 0) { - Text(notification.account.safeDisplayName) - .font(.subheadline) - .fontWeight(.semibold) + - Text(" ") + - Text(type.label()) - .font(.subheadline) + - Text(" ⸱ ") - .font(.footnote) - .foregroundColor(.gray) + - Text(notification.createdAt.formatted) - .font(.footnote) - .foregroundColor(.gray) + EmojiText(notification.account.safeDisplayName, emojis: notification.account.emojis) + .append { + Text(" ") + + Text(type.label()) + .font(.subheadline) + + Text(" ⸱ ") + .font(.footnote) + .foregroundColor(.gray) + + Text(notification.createdAt.formatted) + .font(.footnote) + .foregroundColor(.gray) + } Spacer() } } @@ -86,7 +87,7 @@ struct NotificationRowView: View { .foregroundColor(.gray) if type == .follow { - Text(notification.account.note.asSafeAttributedString) + EmojiText(notification.account.note, emojis: notification.account.emojis) .lineLimit(3) .font(.callout) .foregroundColor(.gray) diff --git a/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift b/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift index 81417b8a..c7ee0f13 100644 --- a/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift +++ b/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift @@ -62,7 +62,7 @@ class StatusDetailViewModel: ObservableObject { let status: Status = try await client.get(endpoint: Statuses.status(id: statusId)) let context: StatusContext = try await client.get(endpoint: Statuses.context(id: statusId)) state = .display(status: status, context: context) - title = "Post from \(status.account.displayName)" + title = "Post from \(status.account.displayNameWithoutEmojis)" } catch { state = .error(error: error) } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift index 12b5ee2f..2a5096bb 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift @@ -1,6 +1,7 @@ import Foundation import SwiftUI import DesignSystem +import EmojiText struct StatusEditorAutoCompleteView: View { @EnvironmentObject private var theme: Theme @@ -31,7 +32,7 @@ struct StatusEditorAutoCompleteView: View { HStack { AvatarView(url: account.avatar, size: .badge) VStack(alignment: .leading) { - Text(account.displayName) + EmojiText(account.safeDisplayName, emojis: account.emojis) .font(.footnote) .foregroundColor(theme.labelColor) Text("@\(account.acct)") diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index 51a26fab..f5bbc699 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -7,6 +7,7 @@ import Models import Network import PhotosUI import NukeUI +import EmojiText public struct StatusEditorView: View { @EnvironmentObject private var preferences: UserPreferences diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift index e145ba56..49482e4b 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift @@ -33,9 +33,9 @@ extension StatusEditorViewModel { case .edit: return "Editing your post" 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): - return "Quote of \(status.reblog?.account.displayName ?? status.account.displayName)" + return "Quote of \(status.reblog?.account.displayNameWithoutEmojis ?? status.account.displayNameWithoutEmojis)" } } } diff --git a/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift b/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift index 0d4fe4e8..83a8ce9e 100644 --- a/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift +++ b/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift @@ -1,6 +1,7 @@ import SwiftUI import Models import DesignSystem +import EmojiText @MainActor public struct StatusEmbededView: View { @@ -34,7 +35,7 @@ public struct StatusEmbededView: View { HStack(alignment: .center) { AvatarView(url: account.avatar, size: .embed) VStack(alignment: .leading, spacing: 0) { - status.account.displayNameWithEmojis + EmojiText(status.account.safeDisplayName, emojis: account.emojis) .font(.footnote) .fontWeight(.semibold) Group { diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 88bc63be..567529e1 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -4,6 +4,7 @@ import Env import DesignSystem import Network import Shimmer +import EmojiText public struct StatusRowView: View { @Environment(\.redactionReasons) private var reasons @@ -90,7 +91,7 @@ public struct StatusRowView: View { HStack(spacing: 2) { Image(systemName:"arrow.left.arrow.right.circle.fill") AvatarView(url: viewModel.status.account.avatar, size: .boost) - viewModel.status.account.displayNameWithEmojis + EmojiText(viewModel.status.account.safeDisplayName, emojis: viewModel.status.account.emojis) Text("boosted") } .font(.footnote) @@ -164,7 +165,7 @@ public struct StatusRowView: View { private func makeStatusContentView(status: AnyStatus) -> some View { Group { if !status.spoilerText.isEmpty { - Text(status.spoilerText) + EmojiText(status.spoilerText, emojis: status.emojis) .font(.body) Button { withAnimation { @@ -177,7 +178,7 @@ public struct StatusRowView: View { } if !viewModel.displaySpoiler { HStack { - Text(status.content.asSafeAttributedString) + EmojiText(status.content, emojis: status.emojis) .font(.body) .environment(\.openURL, OpenURLAction { url in routeurPath.handleStatus(status: status, url: url) @@ -233,7 +234,7 @@ public struct StatusRowView: View { AvatarView(url: status.account.avatar, size: .status) } VStack(alignment: .leading, spacing: 0) { - status.account.displayNameWithEmojis + EmojiText(status.account.safeDisplayName, emojis: status.account.emojis) .font(.headline) .fontWeight(.semibold) Group {