add embed code

This commit is contained in:
kakkokari-gtyih 2023-04-05 15:48:44 +09:00
parent ecaf152b4a
commit b8f9130386
11 changed files with 383 additions and 32 deletions

View File

@ -476,6 +476,36 @@ export class ClientServerService {
}
});
// Note Embed
fastify.get<{ Params: { note: string; } }>('/notes/:note/embed', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
reply.header("X-Robots-Tag", "noindex");
const note = await this.notesRepository.findOneBy({
id: request.params.note,
visibility: In(['public', 'home']),
});
if (note) {
const _note = await this.noteEntityService.pack(note);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
return await reply.view('note', {
note: _note,
profile,
avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: note.userId })),
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
instanceName: meta.name ?? 'Misskey',
icon: meta.iconUrl,
themeColor: meta.themeColor,
});
} else {
return await renderBase(reply);
}
});
// Page
fastify.get<{ Params: { user: string; page: string; } }>('/@:user/pages/:page', async (request, reply) => {
const { username, host } = Acct.parse(request.params.user);

View File

@ -39,6 +39,7 @@
"eventemitter3": "5.0.0",
"gsap": "3.11.5",
"idb-keyval": "6.2.0",
"iframe-resizer": "^4.3.6",
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
@ -97,6 +98,7 @@
"@types/estree": "^1.0.0",
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@types/iframe-resizer": "^3.5.9",
"@types/matter-js": "0.18.2",
"@types/node": "18.15.11",
"@types/punycode": "2.1.0",

View File

@ -54,6 +54,11 @@
<div class="username"><MkAcct :user="appearNote.user"/></div>
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
</div>
<div v-if="embed" class="instance-info">
<button class="_button" v-click-anime @click="openInstanceMenu">
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" class="icon"/>
</button>
</div>
</header>
<div class="main">
<div class="body">
@ -91,32 +96,54 @@
</MkA>
</div>
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button class="button _button" @click="reply()">
<i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button>
<button
v-if="canRenote"
ref="renoteButton"
class="button _button"
@mousedown="renote()"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
</button>
<button v-else class="button _button" disabled>
<i class="ti ti-ban"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
</button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="button _button" @mousedown="clip()">
<i class="ti ti-paperclip"></i>
</button>
<template v-if="embed">
<MkA class="button _button" :to="notePage(appearNote)">
<i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</MkA>
<MkA v-if="canRenote" class="button _button" :to="notePage(appearNote)">
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
</MkA>
<MkA v-if="appearNote.myReaction == null" class="button _button" :to="notePage(appearNote)">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
</MkA>
<MkA v-if="appearNote.myReaction != null" class="button _button reacted" :to="notePage(appearNote)">
<i class="ti ti-minus"></i>
</MkA>
<MkA v-if="defaultStore.state.showClipButtonInNoteFooter" class="button _button" :to="notePage(appearNote)">
<i class="ti ti-paperclip"></i>
</MkA>
</template>
<template v-else>
<button class="button _button" @click="reply()">
<i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button>
<button
v-if="canRenote"
ref="renoteButton"
class="button _button"
@mousedown="renote()"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
</button>
<button v-else class="button _button" disabled>
<i class="ti ti-ban"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
</button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="button _button" @mousedown="clip()">
<i class="ti ti-paperclip"></i>
</button>
</template>
<button ref="menuButton" class="button _button" @mousedown="menu()">
<i class="ti ti-dots"></i>
</button>
@ -158,6 +185,8 @@ import { defaultStore, noteViewInterruptors } from '@/store';
import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { $i } from '@/account';
import { instance } from '@/instance';
import { openInstanceMenu } from '@/ui/_common_/common';
import { i18n } from '@/i18n';
import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture';
@ -170,6 +199,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
const props = defineProps<{
note: misskey.entities.Note;
pinned?: boolean;
embed?: boolean;
}>();
const inChannel = inject('inChannel', null);
@ -378,12 +408,12 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, embed: props.embed }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, embed: props.embed }), menuButton.value, {
viaKeyboard,
}).then(focus);
}
@ -464,6 +494,10 @@ if (appearNote.replyId) {
&:hover > .article > .main > .footer > .button {
opacity: 1;
&:hover {
text-decoration: none;
}
}
> .reply-to {
@ -578,6 +612,19 @@ if (appearNote.replyId) {
word-wrap: anywhere;
}
}
> .instance-info {
flex-shrink: 0;
padding-left: 16px;
width: 39px;
height: 39px;
img {
width: 100%;
height: auto;
border-radius: 4px;
}
}
}
> .main {
@ -734,6 +781,10 @@ if (appearNote.replyId) {
width: 50px;
height: 50px;
}
> .instance-info {
width: 33px;
height: 33px;
}
}
> .main {

View File

@ -79,6 +79,11 @@ function popout() {
}
function nav(ev: MouseEvent) {
if (router.currentRoute.value.name?.toLowerCase().includes("embed")) {
window.open(props.to, '_blank');
return;
}
if (props.behavior === 'browser') {
location.href = props.to;
return;

View File

@ -187,6 +187,7 @@ try {
} catch (err) {}
const app = createApp(
window.location.href.includes("/embed") ? defineAsyncComponent(() => import('@/ui/embed.vue')) :
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :

View File

@ -0,0 +1,145 @@
<template>
<div class="fcuexfpr">
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="note" class="note">
<div class="main">
<div class="note _gaps_s">
<MkRemoteCaution v-if="note.user.host != null" :href="notePage(note)"/>
<MkNoteDetailed :key="note.id" v-model:note="note" :embed="true" class="note"/>
</div>
</div>
</div>
<MkError v-else-if="error" @retry="fetchNote()"/>
<MkLoading v-else/>
</Transition>
</div>
</template>
<script lang="ts" setup>
import { computed, watch } from 'vue';
import * as misskey from 'misskey-js';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
import * as os from '@/os';
import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
import { dateString } from '@/filters/date';
import MkClipPreview from '@/components/MkClipPreview.vue';
import { defaultStore } from '@/store';
import { notePage } from '@/filters/note';
const props = defineProps<{
noteId: string;
}>();
let note = $ref<null | misskey.entities.Note>();
let error = $ref();
function fetchNote() {
note = null;
os.api('notes/show', {
noteId: props.noteId,
}).then(res => {
note = res;
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;
});
}
function goNotePage() {
window.open(notePage(note), "_blank");
}
watch(() => props.noteId, fetchNote, {
immediate: true,
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => note ? {
title: i18n.ts.note,
subtitle: dateString(note.createdAt),
avatar: note.user,
path: `/notes/${note.id}/embed`,
share: {
title: i18n.t('noteOf', { user: note.user.name }),
text: note.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 {
background: var(--bg);
> .note {
height: 100%;
position: relative;
> a {
z-index: 0;
position: relative;
&:hover {
text-decoration: none;
}
> .main {
z-index: 1;
position: relative;
> .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);
}
}
> .clips {
> .title {
font-weight: bold;
padding: 12px;
}
}
}
}
}
}
</style>

View File

@ -30,6 +30,10 @@ export const routes = [{
name: 'note',
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')),

View File

@ -0,0 +1,29 @@
import { url } from '@/config';
import { v4 as uuid } from 'uuid';
/**
*
*/
export function getEmbedCode(props: {
entityType: 'notes';
id: string;
}): string | null {
let src: string | undefined = "";
switch (props.entityType) {
case 'notes':
src = `${url}/notes/${props.id}/embed`;
break;
default:
src = undefined;
}
if (src !== undefined) {
const id = uuid();
return `<iframe src="${src}" title="Misskey Note" style="border:none; width:100%; max-width: 650px; min-height: 300px;" data-msky-embed="${id}"></iframe>
<script src="https://cdn.jsdelivr.net/npm/iframe-resizer@4.3.6/js/iframeResizer.min.js" integrity="sha256-86F9vrEnnd2apFWVo5sNxArab6T8L048fPPkYONBDHY=" crossorigin="anonymous"></script>
<script>iFrameResize({}, 'iframe[data-msky-embed="${id}"]');</script>`;
}
return null;
}

View File

@ -11,6 +11,7 @@ import { noteActions } from '@/store';
import { miLocalStorage } from '@/local-storage';
import { getUserMenu } from '@/scripts/get-user-menu';
import { clipsCache } from '@/cache';
import { getEmbedCode } from './get-embed-code';
export async function getNoteClipMenu(props: {
note: misskey.entities.Note;
@ -93,6 +94,7 @@ export function getNoteMenu(props: {
translating: Ref<boolean>;
isDeleted: Ref<boolean>;
currentClip?: misskey.entities.Clip;
embed?: boolean;
}) {
const isRenote = (
props.note.renote != null &&
@ -202,7 +204,17 @@ export function getNoteMenu(props: {
}
function openDetail(): void {
os.pageWindow(`/notes/${appearNote.id}`);
if (props.embed) {
window.open(`/notes/${appearNote.id}`, "_blank");
} else {
os.pageWindow(`/notes/${appearNote.id}`);
}
}
function copyEmbedCode(): void {
console.log(getEmbedCode({ entityType: 'notes', id: appearNote.id }));
copyToClipboard(getEmbedCode({entityType: 'notes', id: appearNote.id}));
os.success();
}
function showReactions(): void {
@ -223,7 +235,7 @@ export function getNoteMenu(props: {
}
let menu;
if ($i) {
if ($i && !props.embed) {
const statePromise = os.api('notes/state', {
noteId: appearNote.id,
});
@ -264,6 +276,11 @@ export function getNoteMenu(props: {
text: i18n.ts.share,
action: share,
},
(!props.embed) ? {
icon: 'ti ti-code',
text: "Embed",
action: copyEmbedCode,
} : undefined,
instance.translatorAvailable ? {
icon: 'ti ti-language-hiragana',
text: i18n.ts.translate,
@ -356,7 +373,7 @@ export function getNoteMenu(props: {
} else {
menu = [{
icon: 'ti ti-external-link',
text: i18n.ts.detailed,
text: i18n.ts.details,
action: openDetail,
}, {
icon: 'ti ti-copy',
@ -372,7 +389,11 @@ export function getNoteMenu(props: {
action: () => {
window.open(appearNote.url ?? appearNote.uri, '_blank');
},
} : undefined]
} : undefined, (!props.embed) ? {
icon: 'ti ti-code',
text: "Embed",
action: copyEmbedCode,
} : undefined,]
.filter(x => x !== undefined);
}

View File

@ -0,0 +1,48 @@
<template>
<div class="mk-app" style="container-type: inline-size;">
<component
:is="popup.component"
v-for="popup in popups"
:key="popup.id"
v-bind="popup.props"
v-on="popup.events"
/>
<RouterView/>
</div>
</template>
<script lang="ts" setup>
import { provide, ComputedRef } from 'vue';
import { mainRouter } from '@/router';
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
import { instanceName } from '@/config';
import { popups } from '@/os';
import 'iframe-resizer/js/iframeResizer.contentWindow'
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
provide('router', mainRouter);
provideMetadataReceiver((info) => {
pageMetadata = info;
if (pageMetadata.value) {
document.title = `${pageMetadata.value.title} | ${instanceName}`;
}
});
document.documentElement.style.backgroundColor = "transparent";
document.documentElement.style.maxWidth = "650px";
document.documentElement.style.minHeight = "300px";
</script>
<style lang="scss" scoped>
.mk-app {
max-width: 650px;
min-width: 0;
min-height: 300px;
overflow: hidden;
border-radius: var(--radius);
border: 1px solid var(--divider);
box-sizing: border-box;
background-color: transparent;
}
</style>

View File

@ -663,6 +663,9 @@ importers:
idb-keyval:
specifier: 6.2.0
version: 6.2.0
iframe-resizer:
specifier: ^4.3.6
version: 4.3.6
insert-text-at-cursor:
specifier: 0.3.0
version: 0.3.0
@ -832,6 +835,9 @@ importers:
'@types/gulp-rename':
specifier: 2.0.1
version: 2.0.1
'@types/iframe-resizer':
specifier: ^3.5.9
version: 3.5.9
'@types/matter-js':
specifier: 0.18.2
version: 0.18.2
@ -6614,6 +6620,10 @@ packages:
/@types/http-cache-semantics@4.0.1:
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
/@types/iframe-resizer@3.5.9:
resolution: {integrity: sha512-RQUBI75F+uXruB95BFpC/8V8lPgJg4MQ6HxOCtAZYBB/h0FNCfrFfb4I+u2pZJIV7sKeszZbFqy1UnGeBMrvsA==}
dev: true
/@types/ioredis@4.28.10:
resolution: {integrity: sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==}
dependencies:
@ -12590,6 +12600,11 @@ packages:
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
/iframe-resizer@4.3.6:
resolution: {integrity: sha512-wz0WodRIF6eP0oGQa5NIP1yrITAZ59ZJvVaVJqJRjaeCtfm461vy2C3us6CKx0e7pooqpIGLpVMSTzrfAjX9Sg==}
engines: {node: '>=0.8.0'}
dev: false
/ignore@5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}