Render custom emojis in display names

This commit is contained in:
silverpill 2023-03-05 00:48:33 +00:00
parent 0b99590069
commit 8321999d2b
16 changed files with 118 additions and 42 deletions

View file

@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added
- Render custom emojis in display names.
### Changed ### Changed
- Use `/@username` routes by default. - Use `/@username` routes by default.

17
src/api/emojis.ts Normal file
View file

@ -0,0 +1,17 @@
export interface CustomEmoji {
shortcode: string,
url: string,
}
export function replaceShortcodes(text: string, emojis: CustomEmoji[]): string {
return text.replace(/:([\w.]+):/g, (match, shortcode) => {
const emoji = emojis.find((emoji) => {
return emoji.shortcode === shortcode
})
if (emoji) {
return `<img class="emoji" title=":${emoji.shortcode}:" alt=":${emoji.shortcode}:" src="${emoji.url}">`
} else {
return match
}
})
}

View file

@ -1,5 +1,6 @@
import { BACKEND_URL } from "@/constants" import { BACKEND_URL } from "@/constants"
import { PAGE_SIZE, http } from "./common" import { PAGE_SIZE, http } from "./common"
import { CustomEmoji } from "./emojis"
import { defaultProfile, Profile } from "./users" import { defaultProfile, Profile } from "./users"
export interface Attachment { export interface Attachment {
@ -52,11 +53,6 @@ export interface Tag {
url: string; url: string;
} }
export interface CustomEmoji {
shortcode: string,
url: string,
}
export interface Post { export interface Post {
id: string; id: string;
uri: string; uri: string;

View file

@ -3,6 +3,7 @@ import { RouteLocationRaw } from "vue-router"
import { BACKEND_URL } from "@/constants" import { BACKEND_URL } from "@/constants"
import { createDidFromEthereumAddress } from "@/utils/did" import { createDidFromEthereumAddress } from "@/utils/did"
import { PAGE_SIZE, http } from "./common" import { PAGE_SIZE, http } from "./common"
import { CustomEmoji } from "./emojis"
export const EXTRA_FIELD_COUNT_MAX = 10 export const EXTRA_FIELD_COUNT_MAX = 10
@ -48,6 +49,7 @@ export interface Profile {
identity_proofs: ProfileField[]; identity_proofs: ProfileField[];
payment_options: ProfilePaymentOption[]; payment_options: ProfilePaymentOption[];
fields: ProfileField[]; fields: ProfileField[];
emojis: CustomEmoji[],
followers_count: number; followers_count: number;
following_count: number; following_count: number;
@ -69,6 +71,7 @@ export function defaultProfile(): Profile {
identity_proofs: [], identity_proofs: [],
payment_options: [], payment_options: [],
fields: [], fields: [],
emojis: [],
followers_count: 0, followers_count: 0,
following_count: 0, following_count: 0,
subscribers_count: 0, subscribers_count: 0,

View file

@ -15,12 +15,12 @@
<avatar :profile="post.account"></avatar> <avatar :profile="post.account"></avatar>
</a> </a>
<a <a
class="display-name" class="display-name-link"
:href="post.account.url" :href="post.account.url"
:title="author.getDisplayName()" :title="author.getDisplayName()"
@click="openProfile($event, post.account)" @click="openProfile($event, post.account)"
> >
{{ author.getDisplayName() }} <profile-display-name :profile="author"></profile-display-name>
</a> </a>
<div class="actor-address" :title="'@' + getActorAddress(post.account)"> <div class="actor-address" :title="'@' + getActorAddress(post.account)">
@{{ getActorAddress(post.account) }} @{{ getActorAddress(post.account) }}
@ -80,9 +80,8 @@
> >
<div class="quote-header"> <div class="quote-header">
<avatar :profile="linkedPost.account"></avatar> <avatar :profile="linkedPost.account"></avatar>
<span class="display-name"> <profile-display-name :profile="getQuoteAuthor(linkedPost)">
{{ getQuoteAuthorDisplayName(linkedPost) }} </profile-display-name>
</span>
<span class="actor-address"> <span class="actor-address">
@{{ getActorAddress(linkedPost.account) }} @{{ getActorAddress(linkedPost.account) }}
</span> </span>
@ -270,6 +269,7 @@ import CryptoAddress from "@/components/CryptoAddress.vue"
import PostAttachment from "@/components/PostAttachment.vue" import PostAttachment from "@/components/PostAttachment.vue"
import PostContent from "@/components/PostContent.vue" import PostContent from "@/components/PostContent.vue"
import PostEditor from "@/components/PostEditor.vue" import PostEditor from "@/components/PostEditor.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import VisibilityIcon from "@/components/VisibilityIcon.vue" import VisibilityIcon from "@/components/VisibilityIcon.vue"
import { useInstanceInfo } from "@/store/instance" import { useInstanceInfo } from "@/store/instance"
import { useCurrentUser } from "@/store/user" import { useCurrentUser } from "@/store/user"
@ -351,9 +351,8 @@ function getReplyMentions(): Mention[] {
return props.post.mentions return props.post.mentions
} }
function getQuoteAuthorDisplayName(post: Post): string | null { function getQuoteAuthor(post: Post): ProfileWrapper {
const profile = new ProfileWrapper(post.account) return new ProfileWrapper(post.account)
return profile.getDisplayName()
} }
function canReply(): boolean { function canReply(): boolean {
@ -607,7 +606,7 @@ async function onMintToken() {
} }
} }
.display-name { .display-name-link {
color: $text-color; color: $text-color;
font-weight: bold; font-weight: bold;
overflow: hidden; overflow: hidden;

View file

@ -7,6 +7,7 @@ import { onMounted } from "vue"
import { $, $ref } from "vue/macros" import { $, $ref } from "vue/macros"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
import { replaceShortcodes } from "@/api/emojis"
import { Post } from "@/api/posts" import { Post } from "@/api/posts"
import { useCurrentUser } from "@/store/user" import { useCurrentUser } from "@/store/user"
import { addGreentext } from "@/utils/greentext" import { addGreentext } from "@/utils/greentext"
@ -72,16 +73,7 @@ function configureInlineLinks() {
function getContent(): string { function getContent(): string {
let content = addGreentext(props.post.content) let content = addGreentext(props.post.content)
// Replace emoji shortcodes // Replace emoji shortcodes
content = content.replace(/:([\w.]+):/g, (match, shortcode) => { content = replaceShortcodes(content, props.post.emojis)
const emoji = props.post.emojis.find((emoji) => {
return emoji.shortcode === shortcode
})
if (emoji) {
return `<img class="emoji" title=":${emoji.shortcode}:" alt=":${emoji.shortcode}:" src="${emoji.url}">`
} else {
return match
}
})
return content return content
} }
</script> </script>
@ -92,6 +84,8 @@ function getContent(): string {
@import "../styles/mixins"; @import "../styles/mixins";
.post-content { .post-content {
@include ugc-emoji;
color: $text-color; color: $text-color;
line-height: 1.5; line-height: 1.5;
padding: $block-inner-padding; padding: $block-inner-padding;
@ -168,10 +162,6 @@ function getContent(): string {
} }
:deep(.emoji) { :deep(.emoji) {
height: 24px;
vertical-align: text-bottom;
width: 24px;
&:hover { &:hover {
height: 48px; height: 48px;
transition: 100ms linear; transition: 100ms linear;

View file

@ -5,8 +5,9 @@
<router-link <router-link
:to="{ name: 'profile-by-acct', params: { acct: post.account.acct }}" :to="{ name: 'profile-by-acct', params: { acct: post.account.acct }}"
:title="getActorAddress(post.account)" :title="getActorAddress(post.account)"
class="display-name-link"
> >
{{ author.getDisplayName() }} <profile-display-name :profile="author"></profile-display-name>
</router-link> </router-link>
<span>reposted</span> <span>reposted</span>
</div> </div>
@ -32,6 +33,7 @@ import { $, $computed } from "vue/macros"
import type { Post as PostObject } from "@/api/posts" import type { Post as PostObject } from "@/api/posts"
import { ProfileWrapper } from "@/api/users" import { ProfileWrapper } from "@/api/users"
import Post from "@/components/Post.vue" import Post from "@/components/Post.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import { useInstanceInfo } from "@/store/instance" import { useInstanceInfo } from "@/store/instance"
/* eslint-disable-next-line no-undef */ /* eslint-disable-next-line no-undef */

View file

@ -7,7 +7,7 @@
<div class="avatar-row"> <div class="avatar-row">
<avatar :profile="profile"></avatar> <avatar :profile="profile"></avatar>
<div class="name-group"> <div class="name-group">
<div class="display-name">{{ profile.getDisplayName() }}</div> <profile-display-name :profile="profile"></profile-display-name>
<div class="actor-address">@{{ getActorAddress(profile) }}</div> <div class="actor-address">@{{ getActorAddress(profile) }}</div>
</div> </div>
</div> </div>
@ -27,6 +27,7 @@ import { $computed } from "vue/macros"
import { Profile, ProfileWrapper } from "@/api/users" import { Profile, ProfileWrapper } from "@/api/users"
import Avatar from "@/components/Avatar.vue" import Avatar from "@/components/Avatar.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import { useInstanceInfo } from "@/store/instance" import { useInstanceInfo } from "@/store/instance"
/* eslint-disable-next-line no-undef */ /* eslint-disable-next-line no-undef */

View file

@ -0,0 +1,32 @@
<template>
<span
class="display-name"
v-html="getDisplayNameHtml()"
>
</span>
</template>
<script setup lang="ts">
import { replaceShortcodes } from "@/api/emojis"
import { ProfileWrapper } from "@/api/users"
/* eslint-disable-next-line no-undef */
const props = defineProps<{
profile: ProfileWrapper,
}>()
function getDisplayNameHtml(): string {
const profile = props.profile
const escaped = new Option(profile.getDisplayName()).innerHTML
return replaceShortcodes(escaped, profile.emojis)
}
</script>
<style scoped lang="scss">
@import "../styles/mixins";
@import "../styles/theme";
.display-name {
@include ugc-emoji;
}
</style>

View file

@ -2,7 +2,7 @@
<div class="profile"> <div class="profile">
<avatar :profile="profile"></avatar> <avatar :profile="profile"></avatar>
<div class="name"> <div class="name">
<div class="display-name">{{ profile.getDisplayName() }}</div> <profile-display-name :profile="profile"></profile-display-name>
<div class="actor-address">@{{ getActorAddress(profile) }}</div> <div class="actor-address">@{{ getActorAddress(profile) }}</div>
</div> </div>
</div> </div>
@ -13,6 +13,7 @@ import { $computed } from "vue/macros"
import { Profile, ProfileWrapper } from "@/api/users" import { Profile, ProfileWrapper } from "@/api/users"
import Avatar from "@/components/Avatar.vue" import Avatar from "@/components/Avatar.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import { useInstanceInfo } from "@/store/instance" import { useInstanceInfo } from "@/store/instance"
const { getActorAddress } = useInstanceInfo() const { getActorAddress } = useInstanceInfo()

View file

@ -7,7 +7,7 @@
:to="{ name: 'profile-by-acct', params: { acct: sender.acct }}" :to="{ name: 'profile-by-acct', params: { acct: sender.acct }}"
> >
<avatar :profile="sender"></avatar> <avatar :profile="sender"></avatar>
<div class="display-name">{{ sender.getDisplayName() }}</div> <profile-display-name :profile="sender"></profile-display-name>
<div class="wallet-address">{{ walletAddress ? walletAddress.toLowerCase() : '?' }}</div> <div class="wallet-address">{{ walletAddress ? walletAddress.toLowerCase() : '?' }}</div>
</component> </component>
<div class="separator"> <div class="separator">
@ -18,7 +18,7 @@
:to="{ name: 'profile-by-acct', params: { acct: recipient.acct }}" :to="{ name: 'profile-by-acct', params: { acct: recipient.acct }}"
> >
<avatar :profile="recipient"></avatar> <avatar :profile="recipient"></avatar>
<div class="display-name">{{ recipient.getDisplayName() }}</div> <profile-display-name :profile="recipient"></profile-display-name>
<div class="wallet-address">{{ recipientEthereumAddress }}</div> <div class="wallet-address">{{ recipientEthereumAddress }}</div>
</router-link> </router-link>
</div> </div>
@ -125,6 +125,7 @@ import {
} from "@/api/subscriptions-ethereum" } from "@/api/subscriptions-ethereum"
import Avatar from "@/components/Avatar.vue" import Avatar from "@/components/Avatar.vue"
import Loader from "@/components/Loader.vue" import Loader from "@/components/Loader.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import { useWallet } from "@/composables/wallet" import { useWallet } from "@/composables/wallet"
import { useInstanceInfo } from "@/store/instance" import { useInstanceInfo } from "@/store/instance"
import { useCurrentUser } from "@/store/user" import { useCurrentUser } from "@/store/user"

View file

@ -7,7 +7,7 @@
:to="{ name: 'profile-by-acct', params: { acct: sender.acct } }" :to="{ name: 'profile-by-acct', params: { acct: sender.acct } }"
> >
<avatar :profile="sender"></avatar> <avatar :profile="sender"></avatar>
<div class="display-name">{{ sender.getDisplayName() }}</div> <profile-display-name :profile="sender"></profile-display-name>
</component> </component>
<div class="separator"> <div class="separator">
<img :src="require('@/assets/feather/arrow-right.svg')"> <img :src="require('@/assets/feather/arrow-right.svg')">
@ -17,7 +17,7 @@
:to="{ name: 'profile-by-acct', params: { acct: recipient.acct }}" :to="{ name: 'profile-by-acct', params: { acct: recipient.acct }}"
> >
<avatar :profile="recipient"></avatar> <avatar :profile="recipient"></avatar>
<div class="display-name">{{ recipient.getDisplayName() }}</div> <profile-display-name :profile="recipient"></profile-display-name>
</router-link> </router-link>
</div> </div>
<form class="sender" v-if="sender.id === ''"> <form class="sender" v-if="sender.id === ''">
@ -131,6 +131,7 @@ import { defaultProfile, Profile, ProfilePaymentOption, ProfileWrapper } from "@
import Avatar from "@/components/Avatar.vue" import Avatar from "@/components/Avatar.vue"
import Loader from "@/components/Loader.vue" import Loader from "@/components/Loader.vue"
import QrCode from "@/components/QrCode.vue" import QrCode from "@/components/QrCode.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import { useCurrentUser } from "@/store/user" import { useCurrentUser } from "@/store/user"
import { formatDate } from "@/utils/dates" import { formatDate } from "@/utils/dates"
import { createMoneroPaymentUri } from "@/utils/monero" import { createMoneroPaymentUri } from "@/utils/monero"

View file

@ -158,3 +158,11 @@
color: $error-color; color: $error-color;
} }
} }
@mixin ugc-emoji {
:deep(.emoji) {
height: 24px;
vertical-align: text-bottom;
width: 24px;
}
}

View file

@ -20,8 +20,10 @@
<router-link <router-link
:title="getSenderInfo(notification)" :title="getSenderInfo(notification)"
:to="{ name: 'profile-by-acct', params: { acct: notification.account.acct } }" :to="{ name: 'profile-by-acct', params: { acct: notification.account.acct } }"
class="display-name-link"
> >
{{ getSenderName(notification) }} <profile-display-name :profile="getSender(notification)">
</profile-display-name>
</router-link> </router-link>
<span v-if="notification.type === 'follow'">followed you</span> <span v-if="notification.type === 'follow'">followed you</span>
<span v-else-if="notification.type === 'reply'">replied to your post</span> <span v-else-if="notification.type === 'reply'">replied to your post</span>
@ -47,7 +49,8 @@
<div class="floating-avatar"> <div class="floating-avatar">
<avatar :profile="notification.account"></avatar> <avatar :profile="notification.account"></avatar>
</div> </div>
<div class="display-name">{{ getSenderName(notification) }}</div> <profile-display-name :profile="getSender(notification)">
</profile-display-name>
<div class="actor-address">@{{ getActorAddress(notification.account) }}</div> <div class="actor-address">@{{ getActorAddress(notification.account) }}</div>
<div class="timestamp">{{ humanizeDate(notification.created_at) }}</div> <div class="timestamp">{{ humanizeDate(notification.created_at) }}</div>
</router-link> </router-link>
@ -73,6 +76,7 @@ import { getNotifications, Notification } from "@/api/notifications"
import { ProfileWrapper } from "@/api/users" import { ProfileWrapper } from "@/api/users"
import Avatar from "@/components/Avatar.vue" import Avatar from "@/components/Avatar.vue"
import Post from "@/components/Post.vue" import Post from "@/components/Post.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import SidebarLayout from "@/components/SidebarLayout.vue" import SidebarLayout from "@/components/SidebarLayout.vue"
import { useInstanceInfo } from "@/store/instance" import { useInstanceInfo } from "@/store/instance"
import { useNotifications } from "@/store/notifications" import { useNotifications } from "@/store/notifications"
@ -94,13 +98,13 @@ onMounted(async () => {
} }
}) })
function getSenderName(notification: Notification): string { function getSender(notification: Notification): ProfileWrapper {
const sender = new ProfileWrapper(notification.account) return new ProfileWrapper(notification.account)
return sender.getDisplayName()
} }
function getSenderInfo(notification: Notification): string { function getSenderInfo(notification: Notification): string {
return `${getSenderName(notification)} (${getActorAddress(notification.account)})` const senderName = getSender(notification).getDisplayName()
return `${senderName} (${getActorAddress(notification.account)})`
} }
function onPostDeleted(notificationIndex: number) { function onPostDeleted(notificationIndex: number) {

View file

@ -105,7 +105,8 @@
</div> </div>
<div class="name-buttons-group"> <div class="name-buttons-group">
<div class="name-group"> <div class="name-group">
<div class="display-name">{{ profile.getDisplayName() }}</div> <profile-display-name :profile="profile">
</profile-display-name>
<div class="actor-address">@{{ actorAddress }}</div> <div class="actor-address">@{{ actorAddress }}</div>
</div> </div>
<div class="buttons"> <div class="buttons">
@ -280,6 +281,7 @@ import {
import Avatar from "@/components/Avatar.vue" import Avatar from "@/components/Avatar.vue"
import Loader from "@/components/Loader.vue" import Loader from "@/components/Loader.vue"
import PostList from "@/components/PostList.vue" import PostList from "@/components/PostList.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import ProfileListItem from "@/components/ProfileListItem.vue" import ProfileListItem from "@/components/ProfileListItem.vue"
import SidebarLayout from "@/components/SidebarLayout.vue" import SidebarLayout from "@/components/SidebarLayout.vue"
import { useEthereumAddressVerification } from "@/composables/ethereum-address-verification" import { useEthereumAddressVerification } from "@/composables/ethereum-address-verification"

View file

@ -0,0 +1,15 @@
import { expect } from "chai"
import { defaultProfile, ProfileWrapper } from "@/api/users"
describe("ProfileWrapper", () => {
it("Replace invisible characters", () => {
const hidden = ""
expect(hidden.length).to.equal(1)
const profile = new ProfileWrapper({
...defaultProfile(),
username: "test",
display_name: hidden,
})
expect(profile.getDisplayName()).to.equal("test")
})
})