feat: ✨ embed route for iframes
This commit is contained in:
parent
c5ad1d9580
commit
ec1843803e
5 changed files with 280 additions and 0 deletions
85
packages/backend/assets/embed.js
Normal file
85
packages/backend/assets/embed.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
// @ts-check
|
||||
|
||||
/**
|
||||
* This embed script is from Mastodon. Thank you, website boy! :)
|
||||
*
|
||||
* License: AGPLv3, Mastodon gGmbH
|
||||
* Original source: https://github.com/mastodon/mastodon/blob/main/public/embed.js
|
||||
* Current source: https://codeberg.org/calckey/calckey/src/branch/develop/packages/backend/assets/embed.js
|
||||
* From: Eugen Rochko <eugen@zeonfederated.com>
|
||||
* Co-authored-by: Rohan Sharma <i.am.lone.survivor@protonmail.com>
|
||||
* Co-authored-by: rinsuki <428rinsuki+git@gmail.com>
|
||||
* Co-authored-by: Matt Hodges <hodgesmr1@gmail.com>
|
||||
* Co-authored-by: Claire <claire.github-309c@sitedethib.com>
|
||||
* Co-authored-by: Kainoa Kanter <kainoa@t1c.dev>
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @param {() => void} loaded
|
||||
*/
|
||||
const ready = function (loaded) {
|
||||
if (document.readyState === 'complete') {
|
||||
loaded();
|
||||
} else {
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (document.readyState === 'complete') {
|
||||
loaded();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ready(function () {
|
||||
/** @type {Map<number, HTMLIFrameElement>} */
|
||||
const iframes = new Map();
|
||||
|
||||
window.addEventListener('message', function (e) {
|
||||
const data = e.data || {};
|
||||
|
||||
if (typeof data !== 'object' || data.type !== 'setHeight' || !iframes.has(data.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iframe = iframes.get(data.id);
|
||||
|
||||
if ('source' in e && iframe.contentWindow !== e.source) {
|
||||
return;
|
||||
}
|
||||
|
||||
iframe.height = data.height;
|
||||
});
|
||||
|
||||
[].forEach.call(document.querySelectorAll('iframe.mastodon-embed'), function (iframe) {
|
||||
// select unique id for each iframe
|
||||
let id = 0;
|
||||
let failCount = 0;
|
||||
const idBuffer = new Uint32Array(1);
|
||||
while (id === 0 || iframes.has(id)) {
|
||||
id = crypto.getRandomValues(idBuffer)[0];
|
||||
failCount++;
|
||||
if (failCount > 100) {
|
||||
// give up and assign (easily guessable) unique number if getRandomValues is broken or no luck
|
||||
id = -(iframes.size + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
iframes.set(id, iframe);
|
||||
|
||||
iframe.scrolling = 'no';
|
||||
iframe.style.overflow = 'hidden';
|
||||
|
||||
iframe.onload = function () {
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'setHeight',
|
||||
id: id,
|
||||
}, '*');
|
||||
};
|
||||
|
||||
iframe.onload();
|
||||
});
|
||||
});
|
||||
})();
|
|
@ -408,6 +408,42 @@ router.get("/notes/:note", async (ctx, next) => {
|
|||
...metaToPugArgs(meta),
|
||||
note: _note,
|
||||
profile,
|
||||
embed: false,
|
||||
avatarUrl: await Users.getAvatarUrl(
|
||||
await Users.findOneByOrFail({ id: note.userId }),
|
||||
),
|
||||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
});
|
||||
|
||||
ctx.set("Cache-Control", "public, max-age=15");
|
||||
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
router.get("/notes/:note/embed", async (ctx, next) => {
|
||||
const note = await Notes.findOneBy({
|
||||
id: ctx.params.note,
|
||||
visibility: In(["public", "home"]),
|
||||
});
|
||||
|
||||
try {
|
||||
if (note) {
|
||||
const _note = await Notes.pack(note);
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({
|
||||
userId: note.userId,
|
||||
});
|
||||
const meta = await fetchMeta();
|
||||
await ctx.render("note", {
|
||||
...metaToPugArgs(meta),
|
||||
note: _note,
|
||||
profile,
|
||||
embed: true,
|
||||
avatarUrl: await Users.getAvatarUrl(
|
||||
await Users.findOneByOrFail({ id: note.userId }),
|
||||
),
|
||||
|
|
|
@ -8,6 +8,7 @@ block vars
|
|||
- const isImage = note.files.length !== 0 && note.files[0].type.startsWith('image');
|
||||
- const isVideo = note.files.length !== 0 && note.files[0].type.startsWith('video');
|
||||
- const imageUrl = isImage ? note.files[0].url : isVideo ? note.files[0].thumbnailUrl : avatarUrl;
|
||||
- const isEmbed = embed;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
|
153
packages/client/src/pages/note.embed.vue
Normal file
153
packages/client/src/pages/note.embed.vue
Normal file
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header
|
||||
><MkPageHeader
|
||||
:actions="headerActions"
|
||||
:tabs="headerTabs"
|
||||
:display-back-button="true"
|
||||
:to="`#${noteId}`"
|
||||
/></template>
|
||||
<MkSpacer :content-max="800" :marginMin="6">
|
||||
<div class="fcuexfpr">
|
||||
<div v-if="appearNote" class="note">
|
||||
<div class="main _gap">
|
||||
<div class="note _gap">
|
||||
<XNoteDetailed
|
||||
:key="appearNote.id"
|
||||
v-model:note="appearNote"
|
||||
class="note"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetch()" />
|
||||
<MkLoading v-else />
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from "vue";
|
||||
import * as misskey from "calckey-js";
|
||||
import XNoteDetailed from "@/components/MkNoteDetailed.vue";
|
||||
import * as os from "@/os";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: string;
|
||||
}>();
|
||||
|
||||
let note = $ref<null | misskey.entities.Note>();
|
||||
let error = $ref();
|
||||
let isRenote = $ref(false);
|
||||
let appearNote = $ref<null | misskey.entities.Note>();
|
||||
|
||||
function fetchNote() {
|
||||
note = null;
|
||||
os.api("notes/show", {
|
||||
noteId: props.noteId,
|
||||
})
|
||||
.then((res) => {
|
||||
note = res;
|
||||
isRenote =
|
||||
note.renote != null &&
|
||||
note.text == null &&
|
||||
note.fileIds.length === 0 &&
|
||||
note.poll == null;
|
||||
appearNote = isRenote
|
||||
? (note.renote as misskey.entities.Note)
|
||||
: note;
|
||||
|
||||
Promise.all([
|
||||
os.api("users/notes", {
|
||||
userId: note.userId,
|
||||
untilId: note.id,
|
||||
limit: 1,
|
||||
}),
|
||||
os.api("users/notes", {
|
||||
userId: note.userId,
|
||||
sinceId: note.id,
|
||||
limit: 1,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => props.noteId, fetchNote, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(
|
||||
computed(() =>
|
||||
appearNote
|
||||
? {
|
||||
title: i18n.t("noteOf", {
|
||||
user: appearNote.user.name || appearNote.user.username,
|
||||
}),
|
||||
subtitle: new Date(appearNote.createdAt).toLocaleString(),
|
||||
avatar: appearNote.user,
|
||||
path: `/notes/${appearNote.id}`,
|
||||
share: {
|
||||
title: i18n.t("noteOf", {
|
||||
user:
|
||||
appearNote.user.name ||
|
||||
appearNote.user.username,
|
||||
}),
|
||||
text: appearNote.text,
|
||||
},
|
||||
}
|
||||
: null
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.125s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fcuexfpr {
|
||||
#calckey_app > :not(.wallpaper) & {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
> .note {
|
||||
> .main {
|
||||
> .load {
|
||||
min-width: 0;
|
||||
margin: 0 auto;
|
||||
border-radius: 999px;
|
||||
|
||||
&.next {
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
|
||||
&.prev {
|
||||
margin-top: var(--margin);
|
||||
}
|
||||
}
|
||||
|
||||
> .note {
|
||||
> .note {
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -50,6 +50,11 @@ export const routes = [
|
|||
path: "/notes/:noteId",
|
||||
component: page(() => import("./pages/note.vue")),
|
||||
},
|
||||
{
|
||||
name: "noteEmbed",
|
||||
path: "/notes/:noteId/embed",
|
||||
component: page(() => import("./pages/note.embed.vue")),
|
||||
},
|
||||
{
|
||||
path: "/clips/:clipId",
|
||||
component: page(() => import("./pages/clip.vue")),
|
||||
|
|
Loading…
Reference in a new issue