Display custom emoji

This commit is contained in:
Justin Mazzocchi 2020-08-08 02:10:05 -07:00
parent 97de884213
commit 998fa8d500
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
8 changed files with 108 additions and 7 deletions

View file

@ -25,6 +25,9 @@
D0159F9324DE743700E78478 /* SecondaryNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159F8E24DE743700E78478 /* SecondaryNavigationView.swift */; }; D0159F9324DE743700E78478 /* SecondaryNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159F8E24DE743700E78478 /* SecondaryNavigationView.swift */; };
D0159F9B24DE748900E78478 /* SidebarNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159F9524DE743E00E78478 /* SidebarNavigationViewModel.swift */; }; D0159F9B24DE748900E78478 /* SidebarNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159F9524DE743E00E78478 /* SidebarNavigationViewModel.swift */; };
D0159F9C24DE748C00E78478 /* SidebarNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159F9824DE744500E78478 /* SidebarNavigationView.swift */; }; D0159F9C24DE748C00E78478 /* SidebarNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159F9824DE744500E78478 /* SidebarNavigationView.swift */; };
D0159FA324DE955900E78478 /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159FA224DE955900E78478 /* CustomEmojiText.swift */; };
D0159FA524DE989700E78478 /* NSMutableAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159FA424DE989700E78478 /* NSMutableAttributedString+Extensions.swift */; };
D0159FA624DE98F600E78478 /* NSMutableAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159FA424DE989700E78478 /* NSMutableAttributedString+Extensions.swift */; };
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; }; D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; }; D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D047FA8724C3E21200AF17C5 /* Assets.xcassets */; }; D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D047FA8724C3E21200AF17C5 /* Assets.xcassets */; };
@ -180,6 +183,8 @@
D0159F8E24DE743700E78478 /* SecondaryNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationView.swift; sourceTree = "<group>"; }; D0159F8E24DE743700E78478 /* SecondaryNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationView.swift; sourceTree = "<group>"; };
D0159F9524DE743E00E78478 /* SidebarNavigationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarNavigationViewModel.swift; sourceTree = "<group>"; }; D0159F9524DE743E00E78478 /* SidebarNavigationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarNavigationViewModel.swift; sourceTree = "<group>"; };
D0159F9824DE744500E78478 /* SidebarNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarNavigationView.swift; sourceTree = "<group>"; }; D0159F9824DE744500E78478 /* SidebarNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarNavigationView.swift; sourceTree = "<group>"; };
D0159FA224DE955900E78478 /* CustomEmojiText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiText.swift; sourceTree = "<group>"; };
D0159FA424DE989700E78478 /* NSMutableAttributedString+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Extensions.swift"; sourceTree = "<group>"; };
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; }; D047FA8524C3E21000AF17C5 /* MetatextApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
D047FA8724C3E21200AF17C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; D047FA8724C3E21200AF17C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; }; D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -286,6 +291,7 @@
D0159F7F24DE739000E78478 /* Views */ = { D0159F7F24DE739000E78478 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D0159FA224DE955900E78478 /* CustomEmojiText.swift */,
D0159F8C24DE743700E78478 /* IdentitiesView.swift */, D0159F8C24DE743700E78478 /* IdentitiesView.swift */,
D0159F8E24DE743700E78478 /* SecondaryNavigationView.swift */, D0159F8E24DE743700E78478 /* SecondaryNavigationView.swift */,
D0159F8D24DE743700E78478 /* TabNavigationView.swift */, D0159F8D24DE743700E78478 /* TabNavigationView.swift */,
@ -470,6 +476,7 @@
children = ( children = (
D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */, D0A1CA7324DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift */,
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */, D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */,
D0159FA424DE989700E78478 /* NSMutableAttributedString+Extensions.swift */,
D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */, D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */,
D081A40424D0F1A8001B016E /* String+Extensions.swift */, D081A40424D0F1A8001B016E /* String+Extensions.swift */,
D065F53A24D3B33A00741304 /* View+Extensions.swift */, D065F53A24D3B33A00741304 /* View+Extensions.swift */,
@ -772,6 +779,7 @@
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */, D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */,
D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */, D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */,
D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */, D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */,
D0159FA524DE989700E78478 /* NSMutableAttributedString+Extensions.swift in Sources */,
D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */, D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */, D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */, D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */,
@ -779,6 +787,7 @@
D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */, D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */,
D0159F9124DE743700E78478 /* TabNavigationView.swift in Sources */, D0159F9124DE743700E78478 /* TabNavigationView.swift in Sources */,
D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */, D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */,
D0159FA324DE955900E78478 /* CustomEmojiText.swift in Sources */,
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */, D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */,
D04FD73C24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */, D04FD73C24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */,
D0DC175B24D0154F00A75C65 /* MastodonAPI.swift in Sources */, D0DC175B24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
@ -802,6 +811,7 @@
D0ED1BD824CF94B200B4899C /* Application.swift in Sources */, D0ED1BD824CF94B200B4899C /* Application.swift in Sources */,
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */, D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */, D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
D0159FA624DE98F600E78478 /* NSMutableAttributedString+Extensions.swift in Sources */,
D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */, D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */,
D0ED1BDB24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */, D0ED1BDB24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */,
D0091B7524DDF4860040E8D2 /* IdentityRepository.swift in Sources */, D0091B7524DDF4860040E8D2 /* IdentityRepository.swift in Sources */,

View file

@ -0,0 +1,37 @@
// Copyright © 2020 Metabolist. All rights reserved.
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
import Kingfisher
extension NSMutableAttributedString {
func insert(emojis: [Emoji], onImageLoad: (() -> Void)?) {
for emoji in emojis {
let token = ":\(emoji.shortcode):"
while let tokenRange = string.range(of: token) {
let attachment = NSTextAttachment()
let attachmentAttributedString = NSAttributedString(attachment: attachment)
replaceCharacters(in: NSRange(tokenRange, in: string), with: attachmentAttributedString)
KingfisherManager.shared.retrieveImage(with: emoji.url) { result in
guard case let .success(value) = result else { return }
attachment.image = value.image
onImageLoad?()
}
}
}
}
func resizeAttachments(toLineHeight lineHeight: CGFloat) {
enumerateAttribute(.attachment, in: NSRange(location: 0, length: length), options: []) { attribute, _, _ in
guard let attachment = attribute as? NSTextAttachment else { return }
attachment.bounds = CGRect(x: 0, y: lineHeight * -0.25, width: lineHeight, height: lineHeight)
}
}
}

View file

@ -3,7 +3,7 @@
"go" = "Go"; "go" = "Go";
"add-identity.instance-url" = "Instance URL"; "add-identity.instance-url" = "Instance URL";
"oauth.error.code-not-found" = "OAuth error: code not found"; "oauth.error.code-not-found" = "OAuth error: code not found";
"secondary-navigation.accounts" = "Accounts"; "secondary-navigation.manage-accounts" = "Manage Accounts";
"secondary-navigation.preferences" = "Preferences"; "secondary-navigation.preferences" = "Preferences";
"identities.add" = "Add"; "identities.add" = "Add";
"preferences" = "Preferences"; "preferences" = "Preferences";

View file

@ -23,11 +23,13 @@ extension Identity {
let id: String let id: String
let identityID: UUID let identityID: UUID
let username: String let username: String
let displayName: String
let url: URL let url: URL
let avatar: URL let avatar: URL
let avatarStatic: URL let avatarStatic: URL
let header: URL let header: URL
let headerStatic: URL let headerStatic: URL
let emojis: [Emoji]
} }
struct Preferences: Codable, Hashable { struct Preferences: Codable, Hashable {

View file

@ -77,11 +77,13 @@ extension IdentityDatabase {
id: account.id, id: account.id,
identityID: identityID, identityID: identityID,
username: account.username, username: account.username,
displayName: account.displayName,
url: account.url, url: account.url,
avatar: account.avatar, avatar: account.avatar,
avatarStatic: account.avatarStatic, avatarStatic: account.avatarStatic,
header: account.header, header: account.header,
headerStatic: account.headerStatic) headerStatic: account.headerStatic,
emojis: account.emojis)
.save) .save)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -178,11 +180,13 @@ private extension IdentityDatabase {
.indexed() .indexed()
.references("storedIdentity", column: "id", onDelete: .cascade) .references("storedIdentity", column: "id", onDelete: .cascade)
t.column("username", .text).notNull() t.column("username", .text).notNull()
t.column("displayName", .text).notNull()
t.column("url", .text).notNull() t.column("url", .text).notNull()
t.column("avatar", .text).notNull() t.column("avatar", .text).notNull()
t.column("avatarStatic", .text).notNull() t.column("avatarStatic", .text).notNull()
t.column("header", .text).notNull() t.column("header", .text).notNull()
t.column("headerStatic", .text).notNull() t.column("headerStatic", .text).notNull()
t.column("emojis", .blob).notNull()
} }
} }

View file

@ -0,0 +1,30 @@
// Copyright © 2020 Metabolist. All rights reserved.
import SwiftUI
struct CustomEmojiText: UIViewRepresentable {
private let attributedText: NSMutableAttributedString
private let emoji: [Emoji]
private let textStyle: UIFont.TextStyle
init(text: String, emoji: [Emoji], textStyle: UIFont.TextStyle) {
attributedText = NSMutableAttributedString(string: text)
self.emoji = emoji
self.textStyle = textStyle
}
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: textStyle)
attributedText.insert(emojis: emoji, onImageLoad: { label.setNeedsDisplay() })
attributedText.resizeAttachments(toLineHeight: label.font.lineHeight)
label.attributedText = attributedText
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
}
}

View file

@ -27,8 +27,20 @@ struct IdentitiesView: View {
} label: { } label: {
HStack { HStack {
KFImage(identity.image, KFImage(identity.image,
options: .downsampled(dimension: 28, scaleFactor: displayScale)) options: .downsampled(dimension: 40, scaleFactor: displayScale))
VStack(alignment: .leading, spacing: 0) {
Spacer()
if let account = identity.account {
CustomEmojiText(
text: account.displayName,
emoji: account.emojis,
textStyle: .headline)
}
Text(identity.handle) Text(identity.handle)
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
}
Spacer() Spacer()
if identity.id == viewModel.identity.id { if identity.id == viewModel.identity.id {
Image(systemName: "checkmark.circle") Image(systemName: "checkmark.circle")

View file

@ -21,14 +21,20 @@ struct SecondaryNavigationView: View {
KFImage(viewModel.identity.image, KFImage(viewModel.identity.image,
options: .downsampled(dimension: 50, scaleFactor: displayScale)) options: .downsampled(dimension: 50, scaleFactor: displayScale))
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let account = viewModel.identity.account {
CustomEmojiText(
text: account.displayName,
emoji: account.emojis,
textStyle: .headline)
}
Text(viewModel.identity.handle) Text(viewModel.identity.handle)
.font(.headline) .font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
.minimumScaleFactor(0.5) .minimumScaleFactor(0.5)
Spacer() Spacer()
Text("secondary-navigation.accounts") Text("secondary-navigation.manage-accounts")
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary)
} }
.padding() .padding()
} }