mirror of
https://github.com/defnull/fediwall.git
synced 2024-11-22 07:33:44 +01:00
Replace mastodon emoji shortcodes with images
Replace :emoji: shortcodes with images in status bodys and profile display names. Use non-animated version unless video-autoplay is enabled.
This commit is contained in:
parent
5edb276dfe
commit
c57d9815d3
44
package-lock.json
generated
44
package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.2.0",
|
"@rushstack/eslint-patch": "^1.2.0",
|
||||||
"@tsconfig/node18": "^2.0.1",
|
"@tsconfig/node18": "^2.0.1",
|
||||||
|
"@types/dompurify": "^3.0.2",
|
||||||
"@types/node": "^18.16.17",
|
"@types/node": "^18.16.17",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||||
"@typescript-eslint/parser": "^6.2.0",
|
"@typescript-eslint/parser": "^6.2.0",
|
||||||
@ -29,6 +30,7 @@
|
|||||||
"@vitejs/plugin-vue": "^4.2.3",
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/tsconfig": "^0.4.0",
|
"@vue/tsconfig": "^0.4.0",
|
||||||
|
"dompurify": "^3.0.5",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.45.0",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"git-describe": "^4.1.1",
|
"git-describe": "^4.1.1",
|
||||||
@ -2069,6 +2071,15 @@
|
|||||||
"integrity": "sha512-UqdfvuJK0SArA2CxhKWwwAWfnVSXiYe63bVpMutc27vpngCntGUZQETO24pEJ46zU6XM+7SpqYoMgcO3bM11Ew==",
|
"integrity": "sha512-UqdfvuJK0SArA2CxhKWwwAWfnVSXiYe63bVpMutc27vpngCntGUZQETO24pEJ46zU6XM+7SpqYoMgcO3bM11Ew==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/dompurify": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/trusted-types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.12",
|
"version": "7.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
||||||
@ -2087,6 +2098,12 @@
|
|||||||
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
|
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/web-bluetooth": {
|
"node_modules/@types/web-bluetooth": {
|
||||||
"version": "0.0.17",
|
"version": "0.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz",
|
||||||
@ -3313,9 +3330,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz",
|
||||||
"integrity": "sha512-ae0mA+Qiqp6C29pqZX3fQgK+F91+F7wobM/v8DRzDqJdZJELXiFUx4PP4pK/mzUS0xkiSEx3Ncd9gr69jg3YsQ=="
|
"integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A=="
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.466",
|
"version": "1.4.466",
|
||||||
@ -6820,6 +6837,15 @@
|
|||||||
"integrity": "sha512-UqdfvuJK0SArA2CxhKWwwAWfnVSXiYe63bVpMutc27vpngCntGUZQETO24pEJ46zU6XM+7SpqYoMgcO3bM11Ew==",
|
"integrity": "sha512-UqdfvuJK0SArA2CxhKWwwAWfnVSXiYe63bVpMutc27vpngCntGUZQETO24pEJ46zU6XM+7SpqYoMgcO3bM11Ew==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/dompurify": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/trusted-types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.12",
|
"version": "7.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
||||||
@ -6838,6 +6864,12 @@
|
|||||||
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
|
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/trusted-types": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/web-bluetooth": {
|
"@types/web-bluetooth": {
|
||||||
"version": "0.0.17",
|
"version": "0.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz",
|
||||||
@ -7669,9 +7701,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dompurify": {
|
"dompurify": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz",
|
||||||
"integrity": "sha512-ae0mA+Qiqp6C29pqZX3fQgK+F91+F7wobM/v8DRzDqJdZJELXiFUx4PP4pK/mzUS0xkiSEx3Ncd9gr69jg3YsQ=="
|
"integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A=="
|
||||||
},
|
},
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.4.466",
|
"version": "1.4.466",
|
||||||
|
@ -25,12 +25,14 @@
|
|||||||
"bootstrap": "^5.3.0",
|
"bootstrap": "^5.3.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"dompurify": "^3.0.5",
|
||||||
"vue-dompurify-html": "^4.1.4",
|
"vue-dompurify-html": "^4.1.4",
|
||||||
"vue-masonry": "^0.16.0"
|
"vue-masonry": "^0.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.2.0",
|
"@rushstack/eslint-patch": "^1.2.0",
|
||||||
"@tsconfig/node18": "^2.0.1",
|
"@tsconfig/node18": "^2.0.1",
|
||||||
|
"@types/dompurify": "^3.0.2",
|
||||||
"@types/node": "^18.16.17",
|
"@types/node": "^18.16.17",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||||
"@typescript-eslint/parser": "^6.2.0",
|
"@typescript-eslint/parser": "^6.2.0",
|
||||||
|
@ -32,24 +32,23 @@ const playVideo = computed(() => {
|
|||||||
<div class="card mx-2 my-3" :class="post.pinned ? 'pinned' : ''">
|
<div class="card mx-2 my-3" :class="post.pinned ? 'pinned' : ''">
|
||||||
<div v-if="post.author" class="card-header d-flex align-items-center">
|
<div v-if="post.author" class="card-header d-flex align-items-center">
|
||||||
<div v-if="post.author?.avatar" class="flex-shrink-0">
|
<div v-if="post.author?.avatar" class="flex-shrink-0">
|
||||||
<img :src="post.author.avatar" class="rounded-circle me-2" />
|
<img :src="post.author.avatar" class="me-2 avatar" />
|
||||||
</div>
|
</div>
|
||||||
<p class="flex-grow-1 m-0">
|
<a :href="post.author.url || post.url" target="_blank" v-dompurify-html="post.author.name"
|
||||||
<a v-if="post.author.url" :href="post.author.url" target="_blank" v-dompurify-html="post.author.name" class="text-body"></a>
|
class="flex-grow-1 m-0 text-body"></a>
|
||||||
<span v-else v-dompurify-html="post.author.name"></span></p>
|
|
||||||
<slot name="topleft"></slot>
|
<slot name="topleft"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div v-if="config.showMedia" class="wall-media mb-3">
|
<div v-if="config.showMedia" class="wall-media mb-3">
|
||||||
<img v-if="media?.type === 'image'" :src="media.url" :alt="media.alt">
|
<img v-if="media?.type === 'image'" :src="media.url" :alt="media.alt">
|
||||||
<video v-else-if="media?.type === 'video'" ref="videoElement"
|
<video v-else-if="media?.type === 'video'" ref="videoElement" muted loop :autoplay="playVideo"
|
||||||
muted loop :autoplay="playVideo" :poster="media.preview" :alt="media.alt" >
|
:poster="media.preview" :alt="media.alt">
|
||||||
<source v-if="playVideo" :src="media.url">
|
<source v-if="playVideo" :src="media.url">
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="config.showText" class="card-text" v-dompurify-html="post.content"></p>
|
<p v-if="config.showText" class="card-text" v-dompurify-html="post.content"></p>
|
||||||
<p class="card-text text-end text-break"><a :href="post.url" target="_blank"
|
<p class="card-text text-end text-break"><a :href="post.url" target="_blank" alt="${post.date}"
|
||||||
alt="${post.date}" class="text-decoration-none text-muted"><small>{{ timeAgo }}</small></a></p>
|
class="text-decoration-none text-muted"><small>{{ timeAgo }}</small></a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -68,22 +67,28 @@ const playVideo = computed(() => {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wall-item .card-header img {
|
.wall-item img.avatar {
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wall-media {
|
.wall-item img.emoji {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
object-fit: contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wall-media img, .wall-media video {
|
.wall-media img,
|
||||||
|
.wall-media video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 1wh;
|
max-height: 1wh;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.wall-item .invisible {
|
.wall-item .invisible {
|
||||||
font-size: 0 !important;
|
font-size: 0 !important;
|
||||||
line-height: 0 !important;
|
line-height: 0 !important;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { Config, MastodonAccount, MastodonStatus, Post, PostMedia } from "@/types";
|
import type { Config, MastodonAccount, MastodonStatus, Post, PostMedia } from "@/types";
|
||||||
import { regexEscape } from "@/utils";
|
import { regexEscape } from "@/utils";
|
||||||
|
import { replaceInText } from '@/utils'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch unique posts from all sources (curently only Mastodon is implemented)
|
* Fetch unique posts from all sources (curently only Mastodon is implemented)
|
||||||
@ -83,7 +84,7 @@ export async function fetchPosts(cfg: Config): Promise<Post[]> {
|
|||||||
try {
|
try {
|
||||||
(await task())
|
(await task())
|
||||||
.filter(status => filterStatus(cfg, status))
|
.filter(status => filterStatus(cfg, status))
|
||||||
.map(statusToWallPost)
|
.map(status => statusToWallPost(cfg, status))
|
||||||
.forEach(addOrRepacePost)
|
.forEach(addOrRepacePost)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`Update task failed for domain ${domain}`, err)
|
console.warn(`Update task failed for domain ${domain}`, err)
|
||||||
@ -204,12 +205,41 @@ const filterStatus = (cfg: Config, status: MastodonStatus) => {
|
|||||||
/**
|
/**
|
||||||
* Convert a mastdon status object to a Post.
|
* Convert a mastdon status object to a Post.
|
||||||
*/
|
*/
|
||||||
const statusToWallPost = (status: MastodonStatus): Post => {
|
const statusToWallPost = (cfg: Config, status: MastodonStatus): Post => {
|
||||||
const date = status.created_at
|
const date = status.created_at
|
||||||
|
|
||||||
if (status.reblog)
|
if (status.reblog)
|
||||||
status = status.reblog
|
status = status.reblog
|
||||||
|
|
||||||
|
const animate = cfg.playVideos;
|
||||||
|
const emojiPattern = /(?<=[^a-z0-9:]|^):([a-z0-9_]{2,}):(?=[^a-z0-9:]|$)/igmu
|
||||||
|
const replaceEmojis = (content: string, emojiMeta: Array<any>) => {
|
||||||
|
content = DOMPurify.sanitize(content)
|
||||||
|
|
||||||
|
if (emojiMeta.length) {
|
||||||
|
var tmpNode = document.createElement("div");
|
||||||
|
tmpNode.innerHTML = content
|
||||||
|
replaceInText(tmpNode, emojiPattern, m => {
|
||||||
|
const code = m[1];
|
||||||
|
const hit = emojiMeta.find(e => e.shortcode === code)
|
||||||
|
if (!hit || !hit.url) return m[0]
|
||||||
|
const img = document.createElement("img")
|
||||||
|
img.src = animate ? hit.url : hit.static_url || hit.url
|
||||||
|
img.classList.add("emoji")
|
||||||
|
img.alt = img.title = `:${code}:`
|
||||||
|
return img;
|
||||||
|
})
|
||||||
|
content = tmpNode.innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = status.account.display_name
|
||||||
|
? replaceEmojis(status.account.display_name, status.account.emojis)
|
||||||
|
: status.account.username
|
||||||
|
const content = replaceEmojis(status.content, status.emojis)
|
||||||
|
|
||||||
const media = status.media_attachments?.map((m): PostMedia | undefined => {
|
const media = status.media_attachments?.map((m): PostMedia | undefined => {
|
||||||
switch (m.type) {
|
switch (m.type) {
|
||||||
case "image":
|
case "image":
|
||||||
@ -227,11 +257,11 @@ const statusToWallPost = (status: MastodonStatus): Post => {
|
|||||||
id: status.uri,
|
id: status.uri,
|
||||||
url: status.url || status.uri,
|
url: status.url || status.uri,
|
||||||
author: {
|
author: {
|
||||||
name: status.account.display_name || status.account.username,
|
name,
|
||||||
url: status.account.url,
|
url: status.account.url,
|
||||||
avatar: status.account.avatar,
|
avatar: status.account.avatar,
|
||||||
},
|
},
|
||||||
content: status.content,
|
content,
|
||||||
date,
|
date,
|
||||||
media,
|
media,
|
||||||
}
|
}
|
||||||
|
38
src/utils.ts
38
src/utils.ts
@ -22,3 +22,41 @@ export function deepClone(obj: any) {
|
|||||||
return window.structuredClone(obj)
|
return window.structuredClone(obj)
|
||||||
return JSON.parse(JSON.stringify(obj))
|
return JSON.parse(JSON.stringify(obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all text nodes and replace each occuences of a pattern with either
|
||||||
|
* a string or a new DOM node. Can be used to replace emojis with images or
|
||||||
|
* URLs with links.
|
||||||
|
*
|
||||||
|
* The root node is modifed in-place and then returned.
|
||||||
|
*/
|
||||||
|
export function replaceInText(root: Node, pattern: RegExp, replace: (m: RegExpMatchArray) => string | Node) {
|
||||||
|
const walk = (node: Node) => {
|
||||||
|
for (const child of node.childNodes) {
|
||||||
|
if (child.nodeType === Node.TEXT_NODE) {
|
||||||
|
if (!child.nodeValue) continue;
|
||||||
|
const text = child.nodeValue
|
||||||
|
const matches = Array.from(text.matchAll(pattern))
|
||||||
|
if (!matches.length) continue
|
||||||
|
|
||||||
|
const newChilds: (string | Node)[] = []
|
||||||
|
var lastEnd = 0
|
||||||
|
for (const m of matches) {
|
||||||
|
if (m.index && m.index > lastEnd)
|
||||||
|
newChilds.push(text.substring(lastEnd, m.index))
|
||||||
|
lastEnd = (m.index || 0) + m[0].length
|
||||||
|
newChilds.push(replace(m))
|
||||||
|
}
|
||||||
|
if (lastEnd < text.length)
|
||||||
|
newChilds.push(text.substring(lastEnd))
|
||||||
|
child.replaceWith(...newChilds);
|
||||||
|
} else {
|
||||||
|
walk(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(root)
|
||||||
|
return root;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user