puyoskey/packages/frontend/src/pages/gallery/post.vue
dakkar 43c0ffe7f8 better block display for <bdi> elements
We use MfM in all sorts of places, and only some of them are actual
blocks. We can now tell the `Mfm` component to make the top-level
`<bdi>` a block when we need to (mostly note bodies, user
descriptions, announcements) and leave it inline in all other places.

This should still rendener inline rtl content embedded in ltr text in
a sensible way, while providing right-alignment for fully rtl blocks.
2024-06-07 11:18:25 +01:00

285 lines
7.3 KiB
Vue

<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="1000" :marginMin="16" :marginMax="32">
<div class="_root">
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="post" class="rkxwuolj">
<div class="files">
<div v-for="file in post.files" :key="file.id" class="file">
<img :src="file.url"/>
</div>
</div>
<div class="body">
<div class="title">{{ post.title }}</div>
<div class="description"><Mfm :text="post.description" :isBlock="true"/></div>
<div class="info">
<i class="ph-clock ph-bold ph-lg"></i> <MkTime :time="post.createdAt" mode="detail"/>
</div>
<div class="actions">
<div class="like">
<MkButton v-if="post.isLiked" v-tooltip="i18n.ts._gallery.unlike" class="button" primary @click="unlike()"><i class="ph-heart-break ph-bold ph-lg"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
<MkButton v-else v-tooltip="i18n.ts._gallery.like" class="button" @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
</div>
<div class="other">
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil-simple ph-bold ph-lg ti-fw"></i></button>
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat ph-bold ph-lg ti-fw"></i></button>
<button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
<button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
</div>
</div>
<div class="user">
<MkAvatar :user="post.user" class="avatar" link preview/>
<div class="name">
<MkUserName :user="post.user" style="display: block;"/>
<MkAcct :user="post.user"/>
</div>
<MkFollowButton v-if="!$i || $i.id != post.user.id" v-model:user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
</div>
</div>
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other">
<template #icon><i class="ph-clock ph-bold ph-lg"></i></template>
<template #header>{{ i18n.ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
<div class="sdrarzaf">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
</MkPagination>
</MkContainer>
</div>
<MkError v-else-if="error" @retry="fetchPost()"/>
<MkLoading v-else/>
</Transition>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import { url } from '@/config.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
const router = useRouter();
const props = defineProps<{
postId: string;
}>();
const post = ref<Misskey.entities.GalleryPost | null>(null);
const error = ref<any>(null);
const otherPostsPagination = {
endpoint: 'users/gallery/posts' as const,
limit: 6,
params: computed(() => ({
userId: post.value.user.id,
})),
};
function fetchPost() {
post.value = null;
misskeyApi('gallery/posts/show', {
postId: props.postId,
}).then(_post => {
post.value = _post;
}).catch(_error => {
error.value = _error;
});
}
function copyLink() {
copyToClipboard(`${url}/gallery/${post.value.id}`);
os.success();
}
function share() {
navigator.share({
title: post.value.title,
text: post.value.description,
url: `${url}/gallery/${post.value.id}`,
});
}
function shareWithNote() {
os.post({
initialText: `${post.value.title} ${url}/gallery/${post.value.id}`,
});
}
function like() {
os.apiWithDialog('gallery/posts/like', {
postId: props.postId,
}).then(() => {
post.value.isLiked = true;
post.value.likedCount++;
});
}
async function unlike() {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.unlikeConfirm,
});
if (confirm.canceled) return;
os.apiWithDialog('gallery/posts/unlike', {
postId: props.postId,
}).then(() => {
post.value.isLiked = false;
post.value.likedCount--;
});
}
function edit() {
router.push(`/gallery/${post.value.id}/edit`);
}
watch(() => props.postId, fetchPost, { immediate: true });
const headerActions = computed(() => [{
icon: 'ph-pencil-simple ph-bold ph-lg',
text: i18n.ts.edit,
handler: edit,
}]);
const headerTabs = computed(() => []);
definePageMetadata(() => ({
title: post.value ? post.value.title : i18n.ts.gallery,
...post.value ? {
avatar: post.value.user,
} : {},
}));
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.125s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.rkxwuolj {
> .files {
> .file {
> img {
display: block;
max-width: 100%;
max-height: 500px;
margin: 0 auto;
}
& + .file {
margin-top: 16px;
}
}
}
> .body {
padding: 32px;
> .title {
font-weight: bold;
font-size: 1.2em;
margin-bottom: 16px;
}
> .info {
margin-top: 16px;
font-size: 90%;
opacity: 0.7;
}
> .actions {
display: flex;
align-items: center;
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
> .like {
> .button {
--accent: rgb(241 97 132);
--X8: rgb(241 92 128);
--buttonBg: rgb(216 71 106 / 5%);
--buttonHoverBg: rgb(216 71 106 / 10%);
color: #ff002f;
::v-deep(.count) {
margin-left: 0.5em;
}
}
}
> .other {
margin-left: auto;
> button {
padding: 8px;
margin: 0 8px;
&:hover {
color: var(--fgHighlighted);
}
}
}
}
> .user {
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
display: flex;
align-items: center;
flex-wrap: wrap;
> .avatar {
width: 52px;
height: 52px;
}
> .name {
margin: 0 0 0 12px;
font-size: 90%;
}
> .koudoku {
margin-left: auto;
}
}
}
}
.sdrarzaf {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
margin: var(--margin);
> .post {
}
}
</style>