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]
### Added
- Render custom emojis in display names.
### Changed
- 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 { PAGE_SIZE, http } from "./common"
import { CustomEmoji } from "./emojis"
import { defaultProfile, Profile } from "./users"
export interface Attachment {
@ -52,11 +53,6 @@ export interface Tag {
url: string;
}
export interface CustomEmoji {
shortcode: string,
url: string,
}
export interface Post {
id: string;
uri: string;

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
<div class="avatar-row">
<avatar :profile="profile"></avatar>
<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>
</div>
@ -27,6 +27,7 @@ import { $computed } from "vue/macros"
import { Profile, ProfileWrapper } from "@/api/users"
import Avatar from "@/components/Avatar.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import { useInstanceInfo } from "@/store/instance"
/* 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">
<avatar :profile="profile"></avatar>
<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>
</div>
@ -13,6 +13,7 @@ import { $computed } from "vue/macros"
import { Profile, ProfileWrapper } from "@/api/users"
import Avatar from "@/components/Avatar.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import { useInstanceInfo } from "@/store/instance"
const { getActorAddress } = useInstanceInfo()

View file

@ -7,7 +7,7 @@
:to="{ name: 'profile-by-acct', params: { acct: sender.acct }}"
>
<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>
</component>
<div class="separator">
@ -18,7 +18,7 @@
:to="{ name: 'profile-by-acct', params: { acct: recipient.acct }}"
>
<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>
</router-link>
</div>
@ -125,6 +125,7 @@ import {
} from "@/api/subscriptions-ethereum"
import Avatar from "@/components/Avatar.vue"
import Loader from "@/components/Loader.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import { useWallet } from "@/composables/wallet"
import { useInstanceInfo } from "@/store/instance"
import { useCurrentUser } from "@/store/user"

View file

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

View file

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

View file

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

View file

@ -105,7 +105,8 @@
</div>
<div class="name-buttons-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>
<div class="buttons">
@ -280,6 +281,7 @@ import {
import Avatar from "@/components/Avatar.vue"
import Loader from "@/components/Loader.vue"
import PostList from "@/components/PostList.vue"
import ProfileDisplayName from "@/components/ProfileDisplayName.vue"
import ProfileListItem from "@/components/ProfileListItem.vue"
import SidebarLayout from "@/components/SidebarLayout.vue"
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")
})
})