summaryrefslogtreecommitdiff
path: root/packages/frontend-embed/src/pages
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2024-09-29 11:42:24 +0000
committerGitHub <noreply@github.com>2024-09-29 11:42:24 +0000
commit5fc8b3bc5018a2cb553f114a570cc33ef6831163 (patch)
tree40edc874ae384548fd13e55fff6e317d1ef84fbb /packages/frontend-embed/src/pages
parentMerge pull request #14391 from misskey-dev/develop (diff)
parentRelease: 2024.9.0 (diff)
downloadmisskey-5fc8b3bc5018a2cb553f114a570cc33ef6831163.tar.gz
misskey-5fc8b3bc5018a2cb553f114a570cc33ef6831163.tar.bz2
misskey-5fc8b3bc5018a2cb553f114a570cc33ef6831163.zip
Merge pull request #14580 from misskey-dev/develop
Release: 2024.9.0
Diffstat (limited to 'packages/frontend-embed/src/pages')
-rw-r--r--packages/frontend-embed/src/pages/clip.vue143
-rw-r--r--packages/frontend-embed/src/pages/not-found.vue24
-rw-r--r--packages/frontend-embed/src/pages/note.vue52
-rw-r--r--packages/frontend-embed/src/pages/tag.vue126
-rw-r--r--packages/frontend-embed/src/pages/user-timeline.vue158
5 files changed, 503 insertions, 0 deletions
diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue
new file mode 100644
index 0000000000..2528dc4b80
--- /dev/null
+++ b/packages/frontend-embed/src/pages/clip.vue
@@ -0,0 +1,143 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+ <EmTimelineContainer v-if="clip" :showHeader="embedParams.header">
+ <template #header>
+ <div :class="$style.clipHeader">
+ <div :class="$style.headerClipIconRoot">
+ <i class="ti ti-paperclip"></i>
+ </div>
+ <div :class="$style.headerTitle" @click="top">
+ <div class="_nowrap"><a :href="`/clips/${clip.id}`" target="_blank" rel="noopener">{{ clip.name }}</a></div>
+ <div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
+ </div>
+ <a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
+ <img
+ :class="$style.instanceIcon"
+ :src="serverMetadata.iconUrl || '/favicon.ico'"
+ />
+ </a>
+ </div>
+ </template>
+ <template #body>
+ <EmNotes
+ ref="notesEl"
+ :pagination="pagination"
+ :disableAutoLoad="!embedParams.autoload"
+ :noGap="true"
+ :ad="false"
+ />
+ </template>
+ </EmTimelineContainer>
+ <XNotFound v-else/>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, inject, useTemplateRef } from 'vue';
+import * as Misskey from 'misskey-js';
+import { scrollToTop } from '@@/js/scroll.js';
+import { url, instanceName } from '@@/js/config.js';
+import { isLink } from '@@/js/is-link.js';
+import { defaultEmbedParams } from '@@/js/embed-page.js';
+import type { Paging } from '@/components/EmPagination.vue';
+import EmNotes from '@/components/EmNotes.vue';
+import XNotFound from '@/pages/not-found.vue';
+import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
+import { misskeyApi } from '@/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import { assertServerContext } from '@/server-context.js';
+import { DI } from '@/di.js';
+
+const props = defineProps<{
+ clipId: string;
+}>();
+
+const embedParams = inject(DI.embedParams, defaultEmbedParams);
+
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const serverContext = inject(DI.serverContext)!;
+
+const clip = ref<Misskey.entities.Clip | null>();
+
+if (assertServerContext(serverContext, 'clip')) {
+ clip.value = serverContext.clip;
+} else {
+ clip.value = await misskeyApi('clips/show', {
+ clipId: props.clipId,
+ }).catch(() => {
+ return null;
+ });
+}
+
+const pagination = computed(() => ({
+ endpoint: 'clips/notes',
+ params: {
+ clipId: props.clipId,
+ },
+} as Paging));
+
+const notesEl = useTemplateRef('notesEl');
+
+function top(ev: MouseEvent) {
+ const target = ev.target as HTMLElement | null;
+ if (target && isLink(target)) return;
+
+ if (notesEl.value) {
+ scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
+ }
+}
+</script>
+
+<style lang="scss" module>
+.clipHeader {
+ padding: 8px 16px;
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: var(--margin);
+ overflow: hidden;
+
+ .headerClipIconRoot {
+ flex-shrink: 0;
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+ font-size: 14px;
+ text-align: center;
+ background-color: var(--accentedBg);
+ color: var(--accent);
+ border-radius: 50%;
+ }
+
+ .headerTitle {
+ flex-grow: 1;
+ font-weight: 700;
+ line-height: 1.1;
+ min-width: 0;
+
+ .sub {
+ font-size: 0.8em;
+ font-weight: 400;
+ opacity: 0.7;
+ }
+ }
+
+ .instanceIconLink {
+ flex-shrink: 0;
+ display: block;
+ margin-left: auto;
+ height: 24px;
+ }
+
+ .instanceIcon {
+ height: 24px;
+ border-radius: 3px;
+ }
+}
+</style>
diff --git a/packages/frontend-embed/src/pages/not-found.vue b/packages/frontend-embed/src/pages/not-found.vue
new file mode 100644
index 0000000000..bbb03b4e64
--- /dev/null
+++ b/packages/frontend-embed/src/pages/not-found.vue
@@ -0,0 +1,24 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+ <div class="_fullinfo">
+ <img :src="notFoundImageUrl" class="_ghost"/>
+ <div>{{ i18n.ts.notFoundDescription }}</div>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { inject, computed } from 'vue';
+import { DEFAULT_NOT_FOUND_IMAGE_URL } from '@@/js/const.js';
+import { DI } from '@/di.js';
+import { i18n } from '@/i18n.js';
+
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const notFoundImageUrl = computed(() => serverMetadata?.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
+</script>
diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue
new file mode 100644
index 0000000000..6f6c8c0f63
--- /dev/null
+++ b/packages/frontend-embed/src/pages/note.vue
@@ -0,0 +1,52 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.noteEmbedRoot">
+ <EmNoteDetailed v-if="note && !prohibited" :note="note"/>
+ <XNotFound v-else/>
+</div>
+</template>
+
+<script setup lang="ts">
+import { inject, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import EmNoteDetailed from '@/components/EmNoteDetailed.vue';
+import XNotFound from '@/pages/not-found.vue';
+import { DI } from '@/di.js';
+import { misskeyApi } from '@/misskey-api.js';
+import { assertServerContext } from '@/server-context';
+
+const props = defineProps<{
+ noteId: string;
+}>();
+
+const serverContext = inject(DI.serverContext)!;
+
+const note = ref<Misskey.entities.Note | null>(null);
+
+const prohibited = ref(false);
+
+if (assertServerContext(serverContext, 'note')) {
+ note.value = serverContext.note;
+} else {
+ note.value = await misskeyApi('notes/show', {
+ noteId: props.noteId,
+ }).catch(() => {
+ return null;
+ });
+}
+
+if (note.value?.url != null || note.value?.uri != null) {
+ // リモートサーバーのノートは弾く
+ prohibited.value = true;
+}
+</script>
+
+<style lang="scss" module>
+.noteEmbedRoot {
+ background-color: var(--panel);
+}
+</style>
diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue
new file mode 100644
index 0000000000..b481b3ebe5
--- /dev/null
+++ b/packages/frontend-embed/src/pages/tag.vue
@@ -0,0 +1,126 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+ <EmTimelineContainer v-if="tag" :showHeader="embedParams.header">
+ <template #header>
+ <div :class="$style.clipHeader">
+ <div :class="$style.headerClipIconRoot">
+ <i class="ti ti-hash"></i>
+ </div>
+ <div :class="$style.headerTitle" @click="top">
+ <div class="_nowrap"><a :href="`/tags/${tag}`" target="_blank" rel="noopener">#{{ tag }}</a></div>
+ <div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
+ </div>
+ <a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
+ <img
+ :class="$style.instanceIcon"
+ :src="serverMetadata.iconUrl || '/favicon.ico'"
+ />
+ </a>
+ </div>
+ </template>
+ <template #body>
+ <EmNotes
+ ref="notesEl"
+ :pagination="pagination"
+ :disableAutoLoad="!embedParams.autoload"
+ :noGap="true"
+ :ad="false"
+ />
+ </template>
+ </EmTimelineContainer>
+ <XNotFound v-else/>
+</div>
+</template>
+
+<script setup lang="ts">
+import { computed, inject, useTemplateRef } from 'vue';
+import { scrollToTop } from '@@/js/scroll.js';
+import type { Paging } from '@/components/EmPagination.vue';
+import EmNotes from '@/components/EmNotes.vue';
+import XNotFound from '@/pages/not-found.vue';
+import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
+import { i18n } from '@/i18n.js';
+import { url, instanceName } from '@@/js/config.js';
+import { isLink } from '@@/js/is-link.js';
+import { DI } from '@/di.js';
+import { defaultEmbedParams } from '@@/js/embed-page.js';
+
+const props = defineProps<{
+ tag: string;
+}>();
+
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const embedParams = inject(DI.embedParams, defaultEmbedParams);
+
+const pagination = computed(() => ({
+ endpoint: 'notes/search-by-tag',
+ params: {
+ tag: props.tag,
+ },
+} as Paging));
+
+const notesEl = useTemplateRef('notesEl');
+
+function top(ev: MouseEvent) {
+ const target = ev.target as HTMLElement | null;
+ if (target && isLink(target)) return;
+
+ if (notesEl.value) {
+ scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
+ }
+}
+</script>
+
+<style lang="scss" module>
+.clipHeader {
+ padding: 8px 16px;
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: var(--margin);
+ overflow: hidden;
+
+ .headerClipIconRoot {
+ flex-shrink: 0;
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+ font-size: 14px;
+ text-align: center;
+ background-color: var(--accentedBg);
+ color: var(--accent);
+ border-radius: 50%;
+ }
+
+ .headerTitle {
+ flex-grow: 1;
+ font-weight: 700;
+ line-height: 1.1;
+ min-width: 0;
+
+ .sub {
+ font-size: 0.8em;
+ font-weight: 400;
+ opacity: 0.7;
+ }
+ }
+
+ .instanceIconLink {
+ flex-shrink: 0;
+ display: block;
+ margin-left: auto;
+ height: 24px;
+ }
+
+ .instanceIcon {
+ height: 24px;
+ border-radius: 3px;
+ }
+}
+</style>
diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue
new file mode 100644
index 0000000000..85e6f52d50
--- /dev/null
+++ b/packages/frontend-embed/src/pages/user-timeline.vue
@@ -0,0 +1,158 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+ <EmTimelineContainer v-if="user && !prohibited" :showHeader="embedParams.header">
+ <template #header>
+ <div :class="$style.userHeader">
+ <a :href="`/@${user.username}`" target="_blank" rel="noopener noreferrer" :class="$style.avatarLink">
+ <EmAvatar :class="$style.avatar" :user="user"/>
+ </a>
+ <div :class="$style.headerTitle" @click="top">
+ <I18n :src="i18n.ts.noteOf" tag="div" class="_nowrap">
+ <template #user>
+ <a v-if="user != null" :href="`/@${user.username}`" target="_blank" rel="noopener noreferrer">
+ <EmUserName :user="user"/>
+ </a>
+ <span v-else>{{ i18n.ts.user }}</span>
+ </template>
+ </I18n>
+ <div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
+ </div>
+ <a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
+ <img
+ :class="$style.instanceIcon"
+ :src="serverMetadata.iconUrl || '/favicon.ico'"
+ />
+ </a>
+ </div>
+ </template>
+ <template #body>
+ <EmNotes
+ ref="notesEl"
+ :pagination="pagination"
+ :disableAutoLoad="!embedParams.autoload"
+ :noGap="true"
+ :ad="false"
+ />
+ </template>
+ </EmTimelineContainer>
+ <XNotFound v-else/>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, inject, useTemplateRef } from 'vue';
+import * as Misskey from 'misskey-js';
+import { url, instanceName } from '@@/js/config.js';
+import { defaultEmbedParams } from '@@/js/embed-page.js';
+import type { Paging } from '@/components/EmPagination.vue';
+import EmNotes from '@/components/EmNotes.vue';
+import EmAvatar from '@/components/EmAvatar.vue';
+import EmUserName from '@/components/EmUserName.vue';
+import I18n from '@/components/I18n.vue';
+import XNotFound from '@/pages/not-found.vue';
+import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
+import { scrollToTop } from '@@/js/scroll.js';
+import { isLink } from '@@/js/is-link.js';
+import { misskeyApi } from '@/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import { assertServerContext } from '@/server-context.js';
+import { DI } from '@/di.js';
+
+const props = defineProps<{
+ userId: string;
+}>();
+
+const embedParams = inject(DI.embedParams, defaultEmbedParams);
+
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const serverContext = inject(DI.serverContext)!;
+
+const user = ref<Misskey.entities.UserLite | null>();
+
+const prohibited = ref(false);
+
+if (assertServerContext(serverContext, 'user')) {
+ user.value = serverContext.user;
+} else {
+ user.value = await misskeyApi('users/show', {
+ userId: props.userId,
+ }).catch(() => {
+ return null;
+ });
+}
+
+if (user.value?.host != null) {
+ // リモートサーバーのユーザーは弾く
+ prohibited.value = true;
+}
+
+const pagination = computed(() => ({
+ endpoint: 'users/notes',
+ params: {
+ userId: user.value?.id,
+ },
+} as Paging));
+
+const notesEl = useTemplateRef('notesEl');
+
+function top(ev: MouseEvent) {
+ const target = ev.target as HTMLElement | null;
+ if (target && isLink(target)) return;
+
+ if (notesEl.value) {
+ scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
+ }
+}
+</script>
+
+<style lang="scss" module>
+.userHeader {
+ padding: 8px 16px;
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: var(--margin);
+ overflow: hidden;
+
+ .avatarLink {
+ display: block;
+ }
+
+ .avatar {
+ display: inline-block;
+ width: 32px;
+ height: 32px;
+ }
+
+ .headerTitle {
+ flex-grow: 1;
+ font-weight: 700;
+ line-height: 1.1;
+ min-width: 0;
+
+ .sub {
+ font-size: 0.8em;
+ font-weight: 400;
+ opacity: 0.7;
+ }
+ }
+
+ .instanceIconLink {
+ flex-shrink: 0;
+ display: block;
+ margin-left: auto;
+ height: 24px;
+ }
+
+ .instanceIcon {
+ height: 24px;
+ border-radius: 3px;
+ }
+}
+</style>