Merge branch Sharkey:develop into trackeropt
This commit is contained in:
commit
9dbd2a6bb4
292 changed files with 5783 additions and 3323 deletions
|
|
@ -63,7 +63,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.details }}</template>
|
||||
<div class="_gaps_s">
|
||||
<Mfm :text="report.comment" :parsedNodes="parsedComment" :isBlock="true" :linkNavigationBehavior="'window'" :author="report.reporter" :nyaize="false" :isAnim="false"/>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsedComment" :compact="false" :detail="false" :showAsQuote="true"/>
|
||||
<div class="_gaps_s" @click.stop>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsedComment" :compact="false" :detail="false" :showAsQuote="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, provide, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" :zPriority="'middle'" @closed="$emit('closed')" @click="onBgClick">
|
||||
<MkModal ref="modal" :zPriority="'middle'" :preferType="'dialog'" @closed="$emit('closed')" @click="onBgClick">
|
||||
<div ref="rootEl" :class="$style.root">
|
||||
<div :class="$style.header">
|
||||
<span :class="$style.icon">
|
||||
|
|
@ -16,13 +16,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span :class="$style.title">{{ announcement.title }}</span>
|
||||
</div>
|
||||
<div :class="$style.text"><Mfm :text="announcement.text" :isBlock="true" /></div>
|
||||
<MkButton primary full @click="ok">{{ i18n.ts.ok }}</MkButton>
|
||||
<div ref="bottomEl"></div>
|
||||
<div :class="$style.footer">
|
||||
<MkButton
|
||||
primary
|
||||
full
|
||||
:disabled="!hasReachedBottom"
|
||||
@click="ok"
|
||||
>{{ hasReachedBottom ? i18n.ts.close : i18n.ts.scrollToClose }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { onMounted, ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
|
@ -32,12 +40,12 @@ import { i18n } from '@/i18n.js';
|
|||
import { $i } from '@/i.js';
|
||||
import { updateCurrentAccountPartial } from '@/accounts.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
announcement: Misskey.entities.Announcement;
|
||||
}>(), {
|
||||
});
|
||||
}>();
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const bottomEl = useTemplateRef('bottomEl');
|
||||
const modal = useTemplateRef('modal');
|
||||
|
||||
async function ok() {
|
||||
|
|
@ -72,7 +80,34 @@ function onBgClick() {
|
|||
});
|
||||
}
|
||||
|
||||
const hasReachedBottom = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
if (bottomEl.value && rootEl.value) {
|
||||
const bottomElRect = bottomEl.value.getBoundingClientRect();
|
||||
const rootElRect = rootEl.value.getBoundingClientRect();
|
||||
if (
|
||||
bottomElRect.top >= rootElRect.top &&
|
||||
bottomElRect.top <= (rootElRect.bottom - 66) // 66 ≒ 75 * 0.9 (modalのアニメーション分)
|
||||
) {
|
||||
hasReachedBottom.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
hasReachedBottom.value = true;
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
root: rootEl.value,
|
||||
rootMargin: '0px 0px -75px 0px',
|
||||
});
|
||||
|
||||
observer.observe(bottomEl.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -80,9 +115,12 @@ onMounted(() => {
|
|||
.root {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
padding: 32px 32px 0;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
background: var(--MI_THEME-panel);
|
||||
border-radius: var(--MI-radius);
|
||||
|
|
@ -103,4 +141,14 @@ onMounted(() => {
|
|||
.text {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: -32px;
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
margin: 0 -32px;
|
||||
padding: 24px 32px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkPagination :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.notFound }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty><MkResult type="empty"/></template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<MkChannelPreview v-for="item in items" :key="item.id" class="_margin" :channel="extractor(item)"/>
|
||||
|
|
@ -23,7 +18,6 @@ import type { Paging } from '@/components/MkPagination.vue';
|
|||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
|
|
|
|||
|
|
@ -28,9 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<div v-if="!initializing && history.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noHistory }}</div>
|
||||
</div>
|
||||
<MkResult v-if="!initializing && history.length == 0" type="empty" :text="i18n.ts._chat.noHistory"/>
|
||||
<MkLoading v-if="initializing"/>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,18 +11,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div
|
||||
v-else-if="!input && !select"
|
||||
:class="[$style.icon, {
|
||||
[$style.type_success]: type === 'success',
|
||||
[$style.type_error]: type === 'error',
|
||||
[$style.type_warning]: type === 'warning',
|
||||
[$style.type_info]: type === 'info',
|
||||
}]"
|
||||
:class="[$style.icon]"
|
||||
>
|
||||
<i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i>
|
||||
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i>
|
||||
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i>
|
||||
<i v-else-if="type === 'info'" :class="$style.iconInner" class="ti ti-info-circle"></i>
|
||||
<i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i>
|
||||
<MkSystemIcon v-if="type === 'success'" :class="$style.iconInner" style="width: 45px;" type="success"/>
|
||||
<MkSystemIcon v-else-if="type === 'error'" :class="$style.iconInner" style="width: 45px;" type="error"/>
|
||||
<MkSystemIcon v-else-if="type === 'warning'" :class="$style.iconInner" style="width: 45px;" type="warn"/>
|
||||
<MkSystemIcon v-else-if="type === 'info'" :class="$style.iconInner" style="width: 45px;" type="info"/>
|
||||
<MkSystemIcon v-else-if="type === 'question'" :class="$style.iconInner" style="width: 45px;" type="question"/>
|
||||
<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/>
|
||||
</div>
|
||||
<header v-if="title" :class="$style.title" class="_selectable"><Mfm :text="title"/></header>
|
||||
|
|
@ -203,22 +198,6 @@ function onInputKeydown(evt: KeyboardEvent) {
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.type_info {
|
||||
color: #55c4dd;
|
||||
}
|
||||
|
||||
.type_success {
|
||||
color: var(--MI_THEME-success);
|
||||
}
|
||||
|
||||
.type_error {
|
||||
color: var(--MI_THEME-error);
|
||||
}
|
||||
|
||||
.type_warning {
|
||||
color: var(--MI_THEME-warn);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: bold;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
@enter="enter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
@afterLeave="afterLeave"
|
||||
>
|
||||
<KeepAlive>
|
||||
<div v-show="opened">
|
||||
|
|
@ -88,6 +92,42 @@ const bgSame = ref(false);
|
|||
const opened = ref(props.defaultOpen);
|
||||
const openedAtLeastOnce = ref(props.defaultOpen);
|
||||
|
||||
//#region interpolate-sizeに対応していないブラウザ向け(TODO: 主要ブラウザが対応したら消す)
|
||||
function enter(el: Element) {
|
||||
if (CSS.supports('interpolate-size', 'allow-keywords')) return;
|
||||
if (!(el instanceof HTMLElement)) return;
|
||||
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = '0';
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
|
||||
}
|
||||
|
||||
function afterEnter(el: Element) {
|
||||
if (CSS.supports('interpolate-size', 'allow-keywords')) return;
|
||||
if (!(el instanceof HTMLElement)) return;
|
||||
|
||||
el.style.height = '';
|
||||
}
|
||||
|
||||
function leave(el: Element) {
|
||||
if (CSS.supports('interpolate-size', 'allow-keywords')) return;
|
||||
if (!(el instanceof HTMLElement)) return;
|
||||
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = `${elementHeight}px`;
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = '0';
|
||||
}
|
||||
|
||||
function afterLeave(el: Element) {
|
||||
if (CSS.supports('interpolate-size', 'allow-keywords')) return;
|
||||
if (!(el instanceof HTMLElement)) return;
|
||||
|
||||
el.style.height = '';
|
||||
}
|
||||
//#endregion
|
||||
|
||||
function toggle() {
|
||||
if (!opened.value) {
|
||||
openedAtLeastOnce.value = true;
|
||||
|
|
@ -110,17 +150,27 @@ onMounted(() => {
|
|||
.transition_toggle_enterActive,
|
||||
.transition_toggle_leaveActive {
|
||||
overflow-y: hidden; // 子要素のmarginが突き出るため clip を使ってはいけない
|
||||
transition: opacity 0.3s, height 0.3s !important;
|
||||
transition: opacity 0.3s, height 0.3s;
|
||||
}
|
||||
|
||||
@supports (interpolate-size: allow-keywords) {
|
||||
.transition_toggle_enterFrom,
|
||||
.transition_toggle_leaveTo {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.root {
|
||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||
}
|
||||
}
|
||||
|
||||
.transition_toggle_enterFrom,
|
||||
.transition_toggle_leaveTo {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: block;
|
||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
|
|||
|
|
@ -62,10 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
<MkResult v-else type="empty"/>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
|
@ -83,7 +80,6 @@ import XFile from './MkFormDialog.file.vue';
|
|||
import type { Form } from '@/utility/form.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { instanceName as localInstanceName } from '@@/js/config.js';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { instance as localInstance } from '@/instance.js';
|
||||
import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
|
||||
|
||||
|
|
@ -61,19 +61,9 @@ $height: 2ex;
|
|||
border-radius: var(--MI-radius-xs) 0 0 var(--MI-radius-xs);
|
||||
overflow: clip;
|
||||
color: #fff;
|
||||
text-shadow: /* .866 ≈ sin(60deg) */
|
||||
1px 0 1px #000,
|
||||
.866px .5px 1px #000,
|
||||
.5px .866px 1px #000,
|
||||
0 1px 1px #000,
|
||||
-.5px .866px 1px #000,
|
||||
-.866px .5px 1px #000,
|
||||
-1px 0 1px #000,
|
||||
-.866px -.5px 1px #000,
|
||||
-.5px -.866px 1px #000,
|
||||
0 -1px 1px #000,
|
||||
.5px -.866px 1px #000,
|
||||
.866px -.5px 1px #000;
|
||||
|
||||
// text-shadowは重いから使うな
|
||||
|
||||
mask-image: linear-gradient(90deg,
|
||||
rgb(0,0,0),
|
||||
rgb(0,0,0) calc(100% - 16px),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="!hardMuted && muted === false"
|
||||
v-if="!hardMuted && muted === false && !threadMuted && !noteMuted"
|
||||
v-show="!isDeleted"
|
||||
ref="rootEl"
|
||||
v-hotkey="keymap"
|
||||
|
|
@ -72,20 +72,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-show="mergedCW == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
|
||||
<div :class="$style.text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
|
||||
<Mfm
|
||||
v-if="appearNote.text"
|
||||
:parsedNodes="parsed"
|
||||
:text="appearNote.text"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'respect'"
|
||||
:emojiUrls="appearNote.emojis"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
:isAnim="allowAnim"
|
||||
:isBlock="true"
|
||||
class="_selectable"
|
||||
/>
|
||||
<div>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
|
||||
<Mfm
|
||||
v-if="appearNote.text"
|
||||
:parsedNodes="parsed"
|
||||
:text="appearNote.text"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'respect'"
|
||||
:emojiUrls="appearNote.emojis"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
:isAnim="allowAnim"
|
||||
class="_selectable"
|
||||
/>
|
||||
</div>
|
||||
<SkNoteTranslation :note="note" :translation="translation" :translating="translating"></SkNoteTranslation>
|
||||
<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
|
||||
<MkButton v-else-if="!prefer.s.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
|
||||
|
|
@ -94,8 +95,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files" @click.stop/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<SkUrlPreviewGroup :sourceUrls="urls" :sourceNote="appearNote" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" :class="$style.urlPreview" @click.stop/>
|
||||
<div v-if="isEnabledUrlPreview" :class="[$style.urlPreview, '_gaps_s']" @click.stop>
|
||||
<SkUrlPreviewGroup :sourceUrls="urls" :sourceNote="appearNote" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds"/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
||||
|
|
@ -124,9 +125,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-tooltip="renoteTooltip"
|
||||
:class="$style.footerButton"
|
||||
class="_button"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@click.stop
|
||||
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
|
||||
@mousedown.prevent="appearNote.isRenoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
|
|
@ -167,8 +168,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click.stop="muted = false">
|
||||
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!--
|
||||
|
|
@ -180,7 +181,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, ref, useTemplateRef, watch, provide } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||
|
|
@ -305,13 +306,12 @@ const galleryEl = useTemplateRef('galleryEl');
|
|||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(prefer.s.uncollapseCW);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : []);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(appearNote.value, parsed.value) : []);
|
||||
const selfNoteIds = computed(() => getSelfNoteIds(props.note));
|
||||
const isLong = shouldCollapsed(appearNote.value, urls.value);
|
||||
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const { muted, hardMuted } = checkMutes(appearNote.value, props.withHardMute);
|
||||
const { muted, hardMuted, threadMuted, noteMuted } = checkMutes(appearNote, computed(() => props.withHardMute));
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||
|
|
@ -319,7 +319,9 @@ const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.vi
|
|||
const renoteCollapsed = ref(
|
||||
prefer.s.collapseRenotes && isRenote && (
|
||||
($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
|
||||
(appearNote.value.myReaction != null)
|
||||
(appearNote.value.myReaction != null) ||
|
||||
(appearNote.value.isFavorited) ||
|
||||
(appearNote.value.isRenoted)
|
||||
),
|
||||
);
|
||||
const inReplyToCollapsed = ref(prefer.s.collapseNotesRepliedTo);
|
||||
|
|
@ -334,7 +336,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
|||
|
||||
const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
const renoteTooltip = computeRenoteTooltip(appearNote);
|
||||
|
||||
let renoting = false;
|
||||
|
||||
|
|
@ -349,7 +351,7 @@ const keymap = {
|
|||
},
|
||||
'q': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
if (canRenote.value && !renoted.value && !renoting) renote(prefer.s.visibilityOnBoost);
|
||||
if (canRenote.value && !appearNote.value.isRenoted && !renoting) renote(prefer.s.visibilityOnBoost);
|
||||
},
|
||||
'm': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
|
|
@ -459,16 +461,6 @@ if (!props.mock) {
|
|||
});
|
||||
});
|
||||
|
||||
if ($i) {
|
||||
misskeyApi('notes/renotes', {
|
||||
noteId: appearNote.value.id,
|
||||
userId: $i.id,
|
||||
limit: 1,
|
||||
}).then((res) => {
|
||||
renoted.value = res.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
useTooltip(reactButton, async (showing) => {
|
||||
const reactions = await misskeyApiGet('notes/reactions', {
|
||||
|
|
@ -527,7 +519,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
channelId: appearNote.value.channelId,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
}).finally(() => { renoting = false; });
|
||||
}
|
||||
} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
|
||||
|
|
@ -548,7 +540,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
renoteId: appearNote.value.id,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
}).finally(() => { renoting = false; });
|
||||
}
|
||||
}
|
||||
|
|
@ -727,7 +719,7 @@ function undoRenote(note) : void {
|
|||
noteId: note.id,
|
||||
});
|
||||
os.toast(i18n.ts.rmboost);
|
||||
renoted.value = false;
|
||||
appearNote.value.isRenoted = false;
|
||||
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="!muted"
|
||||
v-if="!muted && !threadMuted && !noteMuted"
|
||||
v-show="!isDeleted"
|
||||
ref="rootEl"
|
||||
v-hotkey="keymap"
|
||||
|
|
@ -89,21 +89,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</p>
|
||||
<div v-show="mergedCW == null || showContent">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
|
||||
<Mfm
|
||||
v-if="appearNote.text"
|
||||
:parsedNodes="parsed"
|
||||
:text="appearNote.text"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'respect'"
|
||||
:emojiUrls="appearNote.emojis"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
:isAnim="allowAnim"
|
||||
:isBlock="true"
|
||||
class="_selectable"
|
||||
/>
|
||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||
<div>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
|
||||
<Mfm
|
||||
v-if="appearNote.text"
|
||||
:parsedNodes="parsed"
|
||||
:text="appearNote.text"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'respect'"
|
||||
:emojiUrls="appearNote.emojis"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
:isAnim="allowAnim"
|
||||
class="_selectable"
|
||||
/>
|
||||
</div>
|
||||
<SkNoteTranslation :note="note" :translation="translation" :translating="translating"></SkNoteTranslation>
|
||||
<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
|
||||
<MkButton v-else-if="!prefer.s.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
|
||||
|
|
@ -111,8 +111,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<SkUrlPreviewGroup :sourceNodes="nodes" :sourceNote="appearNote" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;" @click.stop/>
|
||||
<div v-if="isEnabledUrlPreview" class="_gaps_s" style="margin-top: 6px;" @click.stop>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsed" :sourceNote="appearNote" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds"/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
||||
</div>
|
||||
|
|
@ -138,8 +138,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-tooltip="renoteTooltip"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@mousedown.prevent="appearNote.isRenoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
|
|
@ -226,14 +226,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
<div v-else class="_panel" :class="$style.muted" @click.stop="muted = false">
|
||||
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, provide, ref, useTemplateRef, watch } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import * as config from '@@/js/config.js';
|
||||
|
|
@ -336,7 +336,6 @@ const galleryEl = useTemplateRef('galleryEl');
|
|||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(prefer.s.uncollapseCW);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
|
||||
const translating = ref(false);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : []);
|
||||
|
|
@ -352,24 +351,14 @@ const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null);
|
|||
|
||||
const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
const renoteTooltip = computeRenoteTooltip(appearNote);
|
||||
|
||||
const { muted } = checkMutes(appearNote.value);
|
||||
const { muted, threadMuted, noteMuted } = checkMutes(appearNote);
|
||||
|
||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||
});
|
||||
|
||||
if ($i) {
|
||||
misskeyApi('notes/renotes', {
|
||||
noteId: appearNote.value.id,
|
||||
userId: $i.id,
|
||||
limit: 1,
|
||||
}).then((res) => {
|
||||
renoted.value = res.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
let renoting = false;
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
|
|
@ -380,7 +369,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
|||
const keymap = {
|
||||
'r': () => reply(),
|
||||
'e|a|plus': () => react(),
|
||||
'q': () => { if (canRenote.value && !renoted.value && !renoting) renote(prefer.s.visibilityOnBoost); },
|
||||
'q': () => { if (canRenote.value && !appearNote.value.isRenoted && !renoting) renote(prefer.s.visibilityOnBoost); },
|
||||
'm': () => showMenu(),
|
||||
'c': () => {
|
||||
if (!prefer.s.showClipButtonInNoteFooter) return;
|
||||
|
|
@ -414,6 +403,11 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
|
|||
const tab = ref(props.initialTab);
|
||||
const reactionTabType = ref<string | null>(null);
|
||||
|
||||
// Auto-select the first page of reactions
|
||||
watch(appearNote, n => {
|
||||
reactionTabType.value ??= Object.keys(n.reactions)[0] ?? null;
|
||||
}, { immediate: true });
|
||||
|
||||
const renotesPagination = computed<Paging>(() => ({
|
||||
endpoint: 'notes/renotes',
|
||||
limit: 10,
|
||||
|
|
@ -549,7 +543,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
channelId: appearNote.value.channelId,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
}).finally(() => { renoting = false; });
|
||||
} else {
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
|
|
@ -568,7 +562,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
renoteId: appearNote.value.id,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
}).finally(() => { renoting = false; });
|
||||
}
|
||||
}
|
||||
|
|
@ -716,12 +710,12 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
|||
}
|
||||
|
||||
function undoRenote() : void {
|
||||
if (!renoted.value) return;
|
||||
if (!appearNote.value.isRenoted) return;
|
||||
misskeyApi('notes/unrenote', {
|
||||
noteId: appearNote.value.id,
|
||||
});
|
||||
os.toast(i18n.ts.rmboost);
|
||||
renoted.value = false;
|
||||
appearNote.value.isRenoted = false;
|
||||
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-show="!isDeleted" v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
|
||||
<div v-show="!isDeleted" v-if="!muted && !noteMuted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
|
||||
<div :class="$style.main">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
|
||||
|
|
@ -31,8 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-tooltip="renoteTooltip"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@click.stop="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@click.stop="appearNote.isRenoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ph-rocket-launch ph-bold ph-lg"></i>
|
||||
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
|
||||
|
|
@ -78,8 +78,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.muted" @click="muted = false">
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
<div v-else :class="$style.muted" @click.stop="muted = false">
|
||||
<SkMutedNote :muted="muted" :threadMuted="false" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -114,6 +114,7 @@ import { prefer } from '@/preferences.js';
|
|||
import { useNoteCapture } from '@/use/use-note-capture.js';
|
||||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
import { instance, policies } from '@/instance';
|
||||
import { getAppearNote } from '@/utility/get-appear-note';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -128,13 +129,14 @@ const props = withDefaults(defineProps<{
|
|||
onDeleteCallback: undefined,
|
||||
});
|
||||
|
||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i?.id);
|
||||
const appearNote = computed(() => getAppearNote(props.note));
|
||||
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
|
||||
const translating = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
const clipButton = useTemplateRef('clipButton');
|
||||
const renoteButton = shallowRef<HTMLElement>();
|
||||
|
|
@ -142,21 +144,13 @@ const quoteButton = shallowRef<HTMLElement>();
|
|||
const menuButton = shallowRef<HTMLElement>();
|
||||
const likeButton = shallowRef<HTMLElement>();
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
const renoteTooltip = computeRenoteTooltip(appearNote);
|
||||
|
||||
const appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
|
||||
const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
|
||||
const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds && props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`,
|
||||
|
|
@ -177,7 +171,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
|
|||
}
|
||||
}
|
||||
|
||||
const { muted } = checkMutes(appearNote.value);
|
||||
const { muted, noteMuted } = checkMutes(appearNote);
|
||||
|
||||
useNoteCapture({
|
||||
rootEl: el,
|
||||
|
|
@ -188,16 +182,6 @@ useNoteCapture({
|
|||
onDeleteCallback: props.detail && props.depth < prefer.s.numberOfReplies ? props.onDeleteCallback : undefined,
|
||||
});
|
||||
|
||||
if ($i) {
|
||||
misskeyApi('notes/renotes', {
|
||||
noteId: appearNote.value.id,
|
||||
userId: $i.id,
|
||||
limit: 1,
|
||||
}).then((res) => {
|
||||
renoted.value = res.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function focus() {
|
||||
el.value?.focus();
|
||||
}
|
||||
|
|
@ -206,8 +190,8 @@ async function reply(viaKeyboard = false): Promise<void> {
|
|||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||
showMovedDialog();
|
||||
await os.post({
|
||||
reply: props.note,
|
||||
channel: props.note.channel ?? undefined,
|
||||
reply: appearNote.value,
|
||||
channel: appearNote.value.channel ?? undefined,
|
||||
animation: !viaKeyboard,
|
||||
});
|
||||
focus();
|
||||
|
|
@ -217,9 +201,9 @@ function react(): void {
|
|||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||
showMovedDialog();
|
||||
sound.playMisskeySfx('reaction');
|
||||
if (props.note.reactionAcceptance === 'likeOnly') {
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
misskeyApi('notes/like', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
override: defaultLike.value,
|
||||
});
|
||||
const el = reactButton.value as HTMLElement | null | undefined;
|
||||
|
|
@ -233,12 +217,12 @@ function react(): void {
|
|||
}
|
||||
} else {
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value ?? null, props.note, reaction => {
|
||||
reactionPicker.show(reactButton.value ?? null, appearNote.value, reaction => {
|
||||
misskeyApi('notes/reactions/create', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
reaction: reaction,
|
||||
});
|
||||
if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
|
||||
if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
|
||||
claimAchievement('reactWithoutRead');
|
||||
}
|
||||
}, () => {
|
||||
|
|
@ -252,7 +236,7 @@ function like(): void {
|
|||
showMovedDialog();
|
||||
sound.playMisskeySfx('reaction');
|
||||
misskeyApi('notes/like', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
override: defaultLike.value,
|
||||
});
|
||||
const el = likeButton.value as HTMLElement | null | undefined;
|
||||
|
|
@ -275,12 +259,12 @@ function undoReact(note): void {
|
|||
}
|
||||
|
||||
function undoRenote() : void {
|
||||
if (!renoted.value) return;
|
||||
if (!appearNote.value.isRenoted) return;
|
||||
misskeyApi('notes/unrenote', {
|
||||
noteId: appearNote.value.id,
|
||||
});
|
||||
os.toast(i18n.ts.rmboost);
|
||||
renoted.value = false;
|
||||
appearNote.value.isRenoted = false;
|
||||
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
|
|
@ -327,7 +311,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
channelId: appearNote.value.channelId,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
});
|
||||
} else {
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
|
|
@ -346,7 +330,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
visibility: visibility,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -361,7 +345,7 @@ function quote() {
|
|||
}).then((cancelled) => {
|
||||
if (cancelled) return;
|
||||
misskeyApi('notes/renotes', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
userId: $i?.id,
|
||||
limit: 1,
|
||||
quote: true,
|
||||
|
|
@ -383,12 +367,12 @@ function quote() {
|
|||
}
|
||||
|
||||
function menu(): void {
|
||||
const { menu, cleanup } = getNoteMenu({ note: props.note, translating, translation, isDeleted });
|
||||
const { menu, cleanup } = getNoteMenu({ note: appearNote.value, translating, translation, isDeleted });
|
||||
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||
}
|
||||
|
||||
async function clip(): Promise<void> {
|
||||
os.popupMenu(await getNoteClipMenu({ note: props.note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||
os.popupMenu(await getNoteClipMenu({ note: appearNote.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||
}
|
||||
|
||||
async function translate() {
|
||||
|
|
@ -397,7 +381,7 @@ async function translate() {
|
|||
|
||||
if (props.detail) {
|
||||
misskeyApi('notes/children', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
limit: prefer.s.numberOfReplies,
|
||||
showQuotes: false,
|
||||
}).then(res => {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: pagination.reversed }]">
|
||||
|
|
@ -30,7 +25,6 @@ import type { Paging } from '@/components/MkPagination.vue';
|
|||
import DynamicNote from '@/components/DynamicNote.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
||||
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
|
||||
<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
|
||||
<div
|
||||
|
|
@ -206,7 +205,6 @@ import { userPage } from '@/filters/user.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
|
|
|||
|
|
@ -4,14 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkPullToRefresh :refresher="() => reload()">
|
||||
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reload()">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.noNotifications }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotifications"/></template>
|
||||
|
||||
<template #default="{ items: notifications }">
|
||||
<SkTransitionGroup
|
||||
|
|
@ -30,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</SkTransitionGroup>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkPullToRefresh>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -42,7 +37,6 @@ import XNotification from '@/components/MkNotification.vue';
|
|||
import DynamicNote from '@/components/DynamicNote.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import SkTransitionGroup from '@/components/SkTransitionGroup.vue';
|
||||
|
|
@ -104,18 +98,38 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_x_move,
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
|
||||
.transition_x_move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.transition_x_enterFrom,
|
||||
|
||||
.transition_x_enterActive {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
&.item,
|
||||
.item {
|
||||
/* Skip Note Rendering有効時、TransitionGroupで通知を追加するときに一瞬がくっとなる問題を抑制する */
|
||||
content-visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveActive {
|
||||
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.transition_x_enterFrom {
|
||||
opacity: 0;
|
||||
transform: translateY(max(-64px, -100%));
|
||||
}
|
||||
|
||||
@supports (interpolate-size: allow-keywords) {
|
||||
.transition_x_enterFrom {
|
||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
|
|
|
|||
|
|
@ -16,12 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkError v-else-if="error" @retry="init()"/>
|
||||
|
||||
<div v-else-if="empty" key="_empty_">
|
||||
<slot name="empty">
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="empty"><MkResult type="empty"/></slot>
|
||||
</div>
|
||||
|
||||
<div v-else ref="rootEl" class="_gaps">
|
||||
|
|
@ -88,7 +83,6 @@ function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): M
|
|||
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, toRaw } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import { toASCII } from 'punycode.js';
|
||||
|
|
@ -1359,7 +1359,7 @@ defineExpose({
|
|||
}
|
||||
|
||||
&.danger {
|
||||
color: #ff2a2a;
|
||||
color: var(--MI_THEME-warn);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div ref="rootEl">
|
||||
<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
|
||||
<div ref="rootEl" :class="isPulling ? $style.isPulling : null">
|
||||
<!-- 小数が含まれるとレンダリングが高頻度になりすぎパフォーマンスが悪化するためround -->
|
||||
<div v-if="isPulling" :class="$style.frame" :style="`--frame-min-height: ${Math.round(pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR)))}px;`">
|
||||
<div :class="$style.frameContent">
|
||||
<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
|
||||
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
|
||||
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPulledEnough }]"></i>
|
||||
<div :class="$style.text">
|
||||
<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
|
||||
<template v-if="isPulledEnough">{{ i18n.ts.releaseToRefresh }}</template>
|
||||
<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
|
||||
<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
|
||||
</div>
|
||||
|
|
@ -29,24 +30,21 @@ import { isHorizontalSwipeSwiping } from '@/utility/touch.js';
|
|||
|
||||
const SCROLL_STOP = 10;
|
||||
const MAX_PULL_DISTANCE = Infinity;
|
||||
const FIRE_THRESHOLD = 230;
|
||||
const FIRE_THRESHOLD = 200;
|
||||
const RELEASE_TRANSITION_DURATION = 200;
|
||||
const PULL_BRAKE_BASE = 1.5;
|
||||
const PULL_BRAKE_FACTOR = 170;
|
||||
|
||||
const isPullStart = ref(false);
|
||||
const isPullEnd = ref(false);
|
||||
const isPulling = ref(false);
|
||||
const isPulledEnough = ref(false);
|
||||
const isRefreshing = ref(false);
|
||||
const pullDistance = ref(0);
|
||||
|
||||
let supportPointerDesktop = false;
|
||||
let startScreenY: number | null = null;
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
let scrollEl: HTMLElement | null = null;
|
||||
|
||||
let disabled = false;
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
refresher: () => Promise<void>;
|
||||
}>(), {
|
||||
|
|
@ -57,19 +55,72 @@ const emit = defineEmits<{
|
|||
(ev: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
function getScreenY(event) {
|
||||
if (supportPointerDesktop) {
|
||||
function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number {
|
||||
if (event.touches && event.touches[0] && event.touches[0].screenY != null) {
|
||||
return event.touches[0].screenY;
|
||||
} else {
|
||||
return event.screenY;
|
||||
}
|
||||
return event.touches[0].screenY;
|
||||
}
|
||||
|
||||
function moveStart(event) {
|
||||
if (!isPullStart.value && !isRefreshing.value && !disabled) {
|
||||
isPullStart.value = true;
|
||||
startScreenY = getScreenY(event);
|
||||
pullDistance.value = 0;
|
||||
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
|
||||
function lockDownScroll() {
|
||||
if (scrollEl == null) return;
|
||||
scrollEl.style.touchAction = 'pan-x pan-down pinch-zoom';
|
||||
scrollEl.style.overscrollBehavior = 'none';
|
||||
}
|
||||
|
||||
function unlockDownScroll() {
|
||||
if (scrollEl == null) return;
|
||||
scrollEl.style.touchAction = 'auto';
|
||||
scrollEl.style.overscrollBehavior = 'contain';
|
||||
}
|
||||
|
||||
function moveStartByMouse(event: MouseEvent) {
|
||||
if (event.button !== 1) return;
|
||||
if (isRefreshing.value) return;
|
||||
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
if (scrollPos !== 0) {
|
||||
unlockDownScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
lockDownScroll();
|
||||
|
||||
event.preventDefault(); // 中クリックによるスクロール、テキスト選択などを防ぐ
|
||||
|
||||
isPulling.value = true;
|
||||
startScreenY = getScreenY(event);
|
||||
pullDistance.value = 0;
|
||||
|
||||
window.addEventListener('mousemove', moving, { passive: true });
|
||||
window.addEventListener('mouseup', () => {
|
||||
window.removeEventListener('mousemove', moving);
|
||||
onPullRelease();
|
||||
}, { passive: true, once: true });
|
||||
}
|
||||
|
||||
function moveStartByTouch(event: TouchEvent) {
|
||||
if (isRefreshing.value) return;
|
||||
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
if (scrollPos !== 0) {
|
||||
unlockDownScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
lockDownScroll();
|
||||
|
||||
isPulling.value = true;
|
||||
startScreenY = getScreenY(event);
|
||||
pullDistance.value = 0;
|
||||
|
||||
window.addEventListener('touchmove', moving, { passive: true });
|
||||
window.addEventListener('touchend', () => {
|
||||
window.removeEventListener('touchmove', moving);
|
||||
onPullRelease();
|
||||
}, { passive: true, once: true });
|
||||
}
|
||||
|
||||
function moveBySystem(to: number): Promise<void> {
|
||||
|
|
@ -108,31 +159,36 @@ async function closeContent() {
|
|||
}
|
||||
}
|
||||
|
||||
function moveEnd() {
|
||||
if (isPullStart.value && !isRefreshing.value) {
|
||||
startScreenY = null;
|
||||
if (isPullEnd.value) {
|
||||
isPullEnd.value = false;
|
||||
isRefreshing.value = true;
|
||||
fixOverContent().then(() => {
|
||||
emit('refresh');
|
||||
props.refresher().then(() => {
|
||||
refreshFinished();
|
||||
});
|
||||
function onPullRelease() {
|
||||
startScreenY = null;
|
||||
if (isPulledEnough.value) {
|
||||
isPulledEnough.value = false;
|
||||
isRefreshing.value = true;
|
||||
fixOverContent().then(() => {
|
||||
emit('refresh');
|
||||
props.refresher().then(() => {
|
||||
refreshFinished();
|
||||
});
|
||||
} else {
|
||||
closeContent().then(() => isPullStart.value = false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
closeContent().then(() => isPulling.value = false);
|
||||
}
|
||||
}
|
||||
|
||||
function moving(event: TouchEvent | PointerEvent) {
|
||||
if (!isPullStart.value || isRefreshing.value || disabled) return;
|
||||
function toggleScrollLockOnTouchEnd() {
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
if (scrollPos === 0) {
|
||||
lockDownScroll();
|
||||
} else {
|
||||
unlockDownScroll();
|
||||
}
|
||||
}
|
||||
|
||||
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value) || isHorizontalSwipeSwiping.value) {
|
||||
function moving(event: MouseEvent | TouchEvent) {
|
||||
if ((scrollEl?.scrollTop ?? 0) > SCROLL_STOP + pullDistance.value || isHorizontalSwipeSwiping.value) {
|
||||
pullDistance.value = 0;
|
||||
isPullEnd.value = false;
|
||||
moveEnd();
|
||||
isPulledEnough.value = false;
|
||||
onPullRelease();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -144,15 +200,7 @@ function moving(event: TouchEvent | PointerEvent) {
|
|||
const moveHeight = moveScreenY - startScreenY!;
|
||||
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||
|
||||
if (pullDistance.value > 0) {
|
||||
if (event.cancelable) event.preventDefault();
|
||||
}
|
||||
|
||||
if (pullDistance.value > SCROLL_STOP) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
|
||||
isPulledEnough.value = pullDistance.value >= FIRE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -162,65 +210,33 @@ function moving(event: TouchEvent | PointerEvent) {
|
|||
*/
|
||||
function refreshFinished() {
|
||||
closeContent().then(() => {
|
||||
isPullStart.value = false;
|
||||
isPulling.value = false;
|
||||
isRefreshing.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function setDisabled(value) {
|
||||
disabled = value;
|
||||
}
|
||||
|
||||
function onScrollContainerScroll() {
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
|
||||
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
|
||||
if (scrollPos === 0) {
|
||||
scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
|
||||
registerEventListenersForReadyToPull();
|
||||
} else {
|
||||
scrollEl!.style.touchAction = 'auto';
|
||||
unregisterEventListenersForReadyToPull();
|
||||
}
|
||||
}
|
||||
|
||||
function registerEventListenersForReadyToPull() {
|
||||
if (rootEl.value == null) return;
|
||||
rootEl.value.addEventListener('touchstart', moveStart, { passive: true });
|
||||
rootEl.value.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
|
||||
}
|
||||
|
||||
function unregisterEventListenersForReadyToPull() {
|
||||
if (rootEl.value == null) return;
|
||||
rootEl.value.removeEventListener('touchstart', moveStart);
|
||||
rootEl.value.removeEventListener('touchmove', moving);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (rootEl.value == null) return;
|
||||
|
||||
scrollEl = getScrollContainer(rootEl.value);
|
||||
if (scrollEl == null) return;
|
||||
|
||||
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
|
||||
|
||||
rootEl.value.addEventListener('touchend', moveEnd, { passive: true });
|
||||
|
||||
registerEventListenersForReadyToPull();
|
||||
lockDownScroll();
|
||||
rootEl.value.addEventListener('mousedown', moveStartByMouse, { passive: false }); // preventDefaultするため
|
||||
rootEl.value.addEventListener('touchstart', moveStartByTouch, { passive: true });
|
||||
rootEl.value.addEventListener('touchend', toggleScrollLockOnTouchEnd, { passive: true });
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
|
||||
|
||||
unregisterEventListenersForReadyToPull();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
setDisabled,
|
||||
unlockDownScroll();
|
||||
if (rootEl.value) rootEl.value.removeEventListener('mousedown', moveStartByMouse);
|
||||
if (rootEl.value) rootEl.value.removeEventListener('touchstart', moveStartByTouch);
|
||||
if (rootEl.value) rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.isPulling {
|
||||
will-change: contents;
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
|
|
@ -242,7 +258,6 @@ defineExpose({
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
> .icon, > .loader {
|
||||
margin: 6px 0;
|
||||
|
|
@ -258,6 +273,7 @@ defineExpose({
|
|||
|
||||
> .text {
|
||||
margin: 5px 0;
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -13,12 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>{{ i18n.ts.schedulePostList }}</template>
|
||||
<div class="_spacer" style="--MI_SPACER-min: 14px; --MI_SPACER-max: 16px;">
|
||||
<MkPagination ref="paginationEl" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty><MkResult type="empty" :text="i18n.ts.nothing"/></template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps">
|
||||
|
|
@ -37,7 +32,6 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
|
|||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'cancel'): void;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="{ [$style.clickToOpen]: prefer.s.clickToOpen }" @click.stop="prefer.s.clickToOpen ? noteclick(note.id) : undefined">
|
||||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
|
||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" @click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :isBlock="true" :author="note.user" :nyaize="'respect'" :isAnim="allowAnim" :emojiUrls="note.emojis"/>
|
||||
<div>
|
||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" @click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :isAnim="allowAnim" :emojiUrls="note.emojis"/>
|
||||
</div>
|
||||
<MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
|
||||
<MkButton v-else-if="!prefer.s.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
|
||||
<SkNoteTranslation :note="note" :translation="translation" :translating="translating"></SkNoteTranslation>
|
||||
|
|
@ -35,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
import MkPoll from '@/components/MkPoll.vue';
|
||||
|
|
|
|||
|
|
@ -54,12 +54,12 @@ const MIN_SWIPE_DISTANCE = 20;
|
|||
// スワイプ時の動作を発火する最小の距離
|
||||
const SWIPE_DISTANCE_THRESHOLD = 70;
|
||||
|
||||
// スワイプを中断するY方向の移動距離
|
||||
const SWIPE_ABORT_Y_THRESHOLD = 75;
|
||||
|
||||
// スワイプできる最大の距離
|
||||
const MAX_SWIPE_DISTANCE = 120;
|
||||
|
||||
// スワイプ方向を判定する角度の許容範囲(度数)
|
||||
const SWIPE_DIRECTION_ANGLE_THRESHOLD = 50;
|
||||
|
||||
// ▲ しきい値 ▲ //
|
||||
|
||||
let startScreenX: number | null = null;
|
||||
|
|
@ -71,6 +71,7 @@ const pullDistance = ref(0);
|
|||
const isSwipingForClass = ref(false);
|
||||
let swipeAborted = false;
|
||||
const isUserHome = props.page === 'user' && tabModel.value === 'home';
|
||||
let swipeDirectionLocked: 'horizontal' | 'vertical' | null = null;
|
||||
|
||||
function touchStart(event: TouchEvent) {
|
||||
if (!prefer.r.enableHorizontalSwipe.value) return;
|
||||
|
|
@ -81,6 +82,7 @@ function touchStart(event: TouchEvent) {
|
|||
|
||||
startScreenX = event.touches[0].screenX;
|
||||
startScreenY = event.touches[0].screenY;
|
||||
swipeDirectionLocked = null; // スワイプ方向をリセット
|
||||
}
|
||||
|
||||
function touchMove(event: TouchEvent) {
|
||||
|
|
@ -97,15 +99,24 @@ function touchMove(event: TouchEvent) {
|
|||
let distanceX = event.touches[0].screenX - startScreenX;
|
||||
let distanceY = event.touches[0].screenY - startScreenY;
|
||||
|
||||
if (Math.abs(distanceY) > SWIPE_ABORT_Y_THRESHOLD) {
|
||||
swipeAborted = true;
|
||||
// スワイプ方向をロック
|
||||
if (!swipeDirectionLocked) {
|
||||
const angle = Math.abs(Math.atan2(distanceY, distanceX) * (180 / Math.PI));
|
||||
if (angle > 90 - SWIPE_DIRECTION_ANGLE_THRESHOLD && angle < 90 + SWIPE_DIRECTION_ANGLE_THRESHOLD) {
|
||||
swipeDirectionLocked = 'vertical';
|
||||
} else {
|
||||
swipeDirectionLocked = 'horizontal';
|
||||
}
|
||||
}
|
||||
|
||||
// 縦方向のスワイプの場合は中断
|
||||
if (swipeDirectionLocked === 'vertical') {
|
||||
swipeAborted = true;
|
||||
pullDistance.value = 0;
|
||||
isSwiping.value = false;
|
||||
window.setTimeout(() => {
|
||||
isSwipingForClass.value = false;
|
||||
}, 400);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -166,6 +177,8 @@ function touchEnd(event: TouchEvent) {
|
|||
window.setTimeout(() => {
|
||||
isSwipingForClass.value = false;
|
||||
}, 400);
|
||||
|
||||
swipeDirectionLocked = null; // スワイプ方向をリセット
|
||||
}
|
||||
|
||||
/** 横スワイプに関与する可能性のある要素を調べる */
|
||||
|
|
@ -192,7 +205,7 @@ watch(tabModel, (newTab, oldTab) => {
|
|||
const newIndex = props.tabs.findIndex(tab => tab.key === newTab);
|
||||
const oldIndex = props.tabs.findIndex(tab => tab.key === oldTab);
|
||||
|
||||
if (oldIndex >= 0 && newIndex && oldIndex < newIndex) {
|
||||
if (oldIndex >= 0 && newIndex >= 0 && oldIndex < newIndex) {
|
||||
transitionName.value = 'swipeAnimationLeft';
|
||||
} else {
|
||||
transitionName.value = 'swipeAnimationRight';
|
||||
|
|
|
|||
|
|
@ -4,14 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()">
|
||||
<MkPagination v-if="paginationQuery" ref="pagingComponent" :pagination="paginationQuery" @queue="emit('queue', $event)" @status="prComponent?.setDisabled($event)">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reloadTimeline()">
|
||||
<MkPagination v-if="paginationQuery" ref="pagingComponent" :pagination="paginationQuery" @queue="emit('queue', $event)">
|
||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<SkTransitionGroup
|
||||
|
|
@ -20,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
:moveClass=" $style.transition_x_move"
|
||||
:moveClass="$style.transition_x_move"
|
||||
tag="div"
|
||||
>
|
||||
<div v-for="(note, i) in notes" :key="note.id" :class="{ '_gaps': !noGap }">
|
||||
|
|
@ -30,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</SkTransitionGroup>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkPullToRefresh>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -47,7 +42,6 @@ import { prefer } from '@/preferences.js';
|
|||
import DynamicNote from '@/components/DynamicNote.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import SkTransitionGroup from '@/components/SkTransitionGroup.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
@ -91,7 +85,6 @@ type TimelineQueryType = {
|
|||
roleId?: string
|
||||
};
|
||||
|
||||
const prComponent = useTemplateRef('prComponent');
|
||||
const pagingComponent = useTemplateRef('pagingComponent');
|
||||
|
||||
let tlNotesCount = 0;
|
||||
|
|
@ -328,18 +321,38 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_x_move,
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
|
||||
.transition_x_move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.transition_x_enterFrom,
|
||||
|
||||
.transition_x_enterActive {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
&.note,
|
||||
.note {
|
||||
/* Skip Note Rendering有効時、TransitionGroupでnoteを追加するときに一瞬がくっとなる問題を抑制する */
|
||||
content-visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveActive {
|
||||
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.transition_x_enterFrom {
|
||||
opacity: 0;
|
||||
transform: translateY(max(-64px, -100%));
|
||||
}
|
||||
|
||||
@supports (interpolate-size: allow-keywords) {
|
||||
.transition_x_leaveTo {
|
||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.reverse {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkPagination :pagination="pagination" :displayLimit="50">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.noUsers }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.root">
|
||||
|
|
@ -25,7 +20,6 @@ import type { Paging } from '@/components/MkPagination.vue';
|
|||
import MkUserInfo from '@/components/MkUserInfo.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
appear @afterLeave="emit('closed')"
|
||||
>
|
||||
<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
|
||||
<div v-if="user != null">
|
||||
<MkError v-if="error" @retry="fetchUser()"/>
|
||||
<div v-else-if="user != null">
|
||||
<div :class="$style.banner" :style="user.bannerUrl ? { backgroundImage: `url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` } : ''">
|
||||
<span v-if="$i && $i.id != user.id && user.isFollowed && user.isFollowing" :class="$style.followed">{{ i18n.ts.mutuals }}</span>
|
||||
<span v-else-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
|
||||
|
|
@ -99,6 +100,7 @@ const zIndex = os.claimZIndex('middle');
|
|||
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
||||
const top = ref(0);
|
||||
const left = ref(0);
|
||||
const error = ref(false);
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
if (user.value == null) return;
|
||||
|
|
@ -106,19 +108,27 @@ function showMenu(ev: MouseEvent) {
|
|||
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
async function fetchUser() {
|
||||
if (typeof props.q === 'object') {
|
||||
user.value = props.q;
|
||||
error.value = false;
|
||||
} else {
|
||||
const query = props.q.startsWith('@') ?
|
||||
const query: Omit<Misskey.entities.UsersShowRequest, 'userIds'> = props.q.startsWith('@') ?
|
||||
Misskey.acct.parse(props.q.substring(1)) :
|
||||
{ userId: props.q };
|
||||
|
||||
misskeyApi('users/show', query).then(res => {
|
||||
if (!props.showing) return;
|
||||
user.value = res;
|
||||
error.value = false;
|
||||
}, () => {
|
||||
error.value = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchUser();
|
||||
|
||||
const rect = props.source.getBoundingClientRect();
|
||||
const x = Math.max(1, ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.scrollX);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div>{{ email }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div :class="$style.label">Reason</div>
|
||||
<div :class="$style.label">{{ i18n.ts.signupReason }}</div>
|
||||
<div>{{ reason }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkA>
|
||||
</header>
|
||||
<div>
|
||||
<div v-if="muted" :class="[$style.text, $style.muted]">
|
||||
<SkMutedNote :muted="muted" :note="note"></SkMutedNote>
|
||||
<div v-if="muted || threadMuted || noteMuted" :class="[$style.text, $style.muted]">
|
||||
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="note"></SkMutedNote>
|
||||
</div>
|
||||
<Mfm v-else :class="$style.text" :text="getNoteSummary(note)" :isBlock="true" :plain="true" :nowrap="false" :isNote="true" nyaize="respect" :author="note.user"/>
|
||||
</div>
|
||||
|
|
@ -35,6 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed } from 'vue';
|
||||
import { getNoteSummary } from '@/utility/get-note-summary.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
|
|
@ -49,8 +50,7 @@ defineEmits<{
|
|||
(event: 'select', user: Misskey.entities.UserLite): void
|
||||
}>();
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const { muted, hardMuted } = checkMutes(props.note);
|
||||
const { muted, hardMuted, threadMuted, noteMuted } = checkMutes(computed(() => props.note));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkPullToRefresh :refresher="() => reload()">
|
||||
<MkPagination ref="latestNotesPaging" :pagination="latestNotesPagination" @init="onListReady">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false" :alt="i18n.ts.noNotes" aria-hidden="true"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<!-- TODO replace with SkDateSeparatedList when merged -->
|
||||
|
|
@ -27,7 +22,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, shallowRef } from 'vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import type { FollowingFeedTab } from '@/types/following-feed.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
|
|
|||
|
|
@ -4,24 +4,39 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
||||
<I18n v-if="noteMuted" :src="i18n.ts.userSaysSomethingInMutedNote" tag="small">
|
||||
<template #name>
|
||||
<MkUserName :user="note.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else-if="!prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<I18n v-else-if="threadMuted" :src="i18n.ts.userSaysSomethingInMutedThread" tag="small">
|
||||
<template #name>
|
||||
<MkUserName :user="note.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
|
||||
<template #name>
|
||||
<MkUserName :user="note.user"/>
|
||||
</template>
|
||||
<template #word>
|
||||
{{ mutedWords }}
|
||||
</template>
|
||||
</I18n>
|
||||
|
||||
<br v-if="threadMuted && muted">
|
||||
|
||||
<template v-if="muted">
|
||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
||||
<template #name>
|
||||
<MkUserName :user="note.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else-if="!prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkUserName :user="note.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
|
||||
<template #name>
|
||||
<MkUserName :user="note.user"/>
|
||||
</template>
|
||||
<template #word>
|
||||
{{ mutedWords }}
|
||||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -30,10 +45,15 @@ import { computed } from 'vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
muted: false | 'sensitiveMute' | string[];
|
||||
threadMuted?: boolean;
|
||||
noteMuted?: boolean;
|
||||
note: Misskey.entities.Note;
|
||||
}>();
|
||||
}>(), {
|
||||
threadMuted: false,
|
||||
noteMuted: false,
|
||||
});
|
||||
|
||||
const mutedWords = computed(() => Array.isArray(props.muted)
|
||||
? props.muted.join(', ')
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="!hardMuted && muted === false"
|
||||
v-if="!hardMuted && muted === false && !threadMuted && !noteMuted"
|
||||
v-show="!isDeleted"
|
||||
ref="rootEl"
|
||||
v-hotkey="keymap"
|
||||
|
|
@ -96,8 +96,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files" @click.stop/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<SkUrlPreviewGroup :sourceUrls="urls" :sourceNote="appearNote" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" :class="$style.urlPreview" @click.stop/>
|
||||
<div v-if="isEnabledUrlPreview" :class="[$style.urlPreview, '_gaps_s']" @click.stop>
|
||||
<SkUrlPreviewGroup :sourceUrls="urls" :sourceNote="appearNote" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds"/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
||||
|
|
@ -125,9 +125,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-tooltip="renoteTooltip"
|
||||
:class="$style.footerButton"
|
||||
class="_button"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@click.stop
|
||||
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
|
||||
@mousedown.prevent="appearNote.isRenoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
|
|
@ -168,8 +168,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click.stop="muted = false">
|
||||
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!--
|
||||
|
|
@ -181,7 +181,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, ref, useTemplateRef, watch, provide } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||
|
|
@ -305,13 +305,12 @@ const galleryEl = useTemplateRef('galleryEl');
|
|||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(prefer.s.uncollapseCW);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : []);
|
||||
const urls = computed(() => parsed.value ? extractPreviewUrls(appearNote.value, parsed.value) : []);
|
||||
const selfNoteIds = computed(() => getSelfNoteIds(props.note));
|
||||
const isLong = shouldCollapsed(appearNote.value, urls.value);
|
||||
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const { muted, hardMuted } = checkMutes(appearNote.value, props.withHardMute);
|
||||
const { muted, hardMuted, threadMuted, noteMuted } = checkMutes(appearNote, computed(() => props.withHardMute));
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||
|
|
@ -319,7 +318,9 @@ const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.vi
|
|||
const renoteCollapsed = ref(
|
||||
prefer.s.collapseRenotes && isRenote && (
|
||||
($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
|
||||
(appearNote.value.myReaction != null)
|
||||
(appearNote.value.myReaction != null) ||
|
||||
(appearNote.value.isFavorited) ||
|
||||
(appearNote.value.isRenoted)
|
||||
),
|
||||
);
|
||||
const inReplyToCollapsed = ref(prefer.s.collapseNotesRepliedTo);
|
||||
|
|
@ -334,7 +335,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
|||
|
||||
const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
const renoteTooltip = computeRenoteTooltip(appearNote);
|
||||
|
||||
let renoting = false;
|
||||
|
||||
|
|
@ -349,7 +350,7 @@ const keymap = {
|
|||
},
|
||||
'q': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
if (canRenote.value && !renoted.value && !renoting) renote(prefer.s.visibilityOnBoost);
|
||||
if (canRenote.value && !appearNote.value.isRenoted && !renoting) renote(prefer.s.visibilityOnBoost);
|
||||
},
|
||||
'm': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
|
|
@ -459,16 +460,6 @@ if (!props.mock) {
|
|||
});
|
||||
});
|
||||
|
||||
if ($i) {
|
||||
misskeyApi('notes/renotes', {
|
||||
noteId: appearNote.value.id,
|
||||
userId: $i.id,
|
||||
limit: 1,
|
||||
}).then((res) => {
|
||||
renoted.value = res.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
useTooltip(reactButton, async (showing) => {
|
||||
const reactions = await misskeyApiGet('notes/reactions', {
|
||||
|
|
@ -527,7 +518,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
channelId: appearNote.value.channelId,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
}).finally(() => { renoting = false; });
|
||||
}
|
||||
} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
|
||||
|
|
@ -548,7 +539,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
renoteId: appearNote.value.id,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
}).finally(() => { renoting = false; });
|
||||
}
|
||||
}
|
||||
|
|
@ -727,7 +718,7 @@ function undoRenote(note) : void {
|
|||
noteId: note.id,
|
||||
});
|
||||
os.toast(i18n.ts.rmboost);
|
||||
renoted.value = false;
|
||||
appearNote.value.isRenoted = false;
|
||||
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="!muted"
|
||||
v-if="!muted && !threadMuted && !noteMuted"
|
||||
v-show="!isDeleted"
|
||||
ref="rootEl"
|
||||
v-hotkey="keymap"
|
||||
|
|
@ -108,7 +108,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:isBlock="true"
|
||||
class="_selectable"
|
||||
/>
|
||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||
<SkNoteTranslation :note="note" :translation="translation" :translating="translating"></SkNoteTranslation>
|
||||
<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
|
||||
<MkButton v-else-if="!prefer.s.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
|
||||
|
|
@ -116,8 +115,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<SkUrlPreviewGroup :sourceNodes="nodes" :sourceNote="appearNote" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;" @click.stop/>
|
||||
<div v-if="isEnabledUrlPreview" class="_gaps_s" style="margin-top: 6px;" @click.stop>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsed" :sourceNote="appearNote" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds"/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
||||
</div>
|
||||
|
|
@ -143,8 +142,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-tooltip="renoteTooltip"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@mousedown.prevent="appearNote.isRenoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
|
|
@ -231,14 +230,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
<div v-else class="_panel" :class="$style.muted" @click.stop="muted = false">
|
||||
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, onUnmounted, onUpdated, provide, ref, useTemplateRef, watch } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import * as config from '@@/js/config.js';
|
||||
|
|
@ -342,7 +341,6 @@ const galleryEl = useTemplateRef('galleryEl');
|
|||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(prefer.s.uncollapseCW);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
|
||||
const translating = ref(false);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : []);
|
||||
|
|
@ -358,24 +356,14 @@ const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null);
|
|||
|
||||
const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
const renoteTooltip = computeRenoteTooltip(appearNote);
|
||||
|
||||
const { muted } = checkMutes(appearNote.value);
|
||||
const { muted, threadMuted, noteMuted } = checkMutes(appearNote);
|
||||
|
||||
watch(() => props.expandAllCws, (expandAllCws) => {
|
||||
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
|
||||
});
|
||||
|
||||
if ($i) {
|
||||
misskeyApi('notes/renotes', {
|
||||
noteId: appearNote.value.id,
|
||||
userId: $i.id,
|
||||
limit: 1,
|
||||
}).then((res) => {
|
||||
renoted.value = res.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
let renoting = false;
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
|
|
@ -386,7 +374,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
|||
const keymap = {
|
||||
'r': () => reply(),
|
||||
'e|a|plus': () => react(),
|
||||
'q': () => { if (canRenote.value && !renoted.value && !renoting) renote(prefer.s.visibilityOnBoost); },
|
||||
'q': () => { if (canRenote.value && !appearNote.value.isRenoted && !renoting) renote(prefer.s.visibilityOnBoost); },
|
||||
'm': () => showMenu(),
|
||||
'c': () => {
|
||||
if (!prefer.s.showClipButtonInNoteFooter) return;
|
||||
|
|
@ -420,6 +408,11 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
|
|||
const tab = ref(props.initialTab);
|
||||
const reactionTabType = ref<string | null>(null);
|
||||
|
||||
// Auto-select the first page of reactions
|
||||
watch(appearNote, n => {
|
||||
reactionTabType.value ??= Object.keys(n.reactions)[0] ?? null;
|
||||
}, { immediate: true });
|
||||
|
||||
const renotesPagination = computed<Paging>(() => ({
|
||||
endpoint: 'notes/renotes',
|
||||
limit: 10,
|
||||
|
|
@ -555,7 +548,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
channelId: appearNote.value.channelId,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
}).finally(() => { renoting = false; });
|
||||
} else {
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
|
|
@ -574,7 +567,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
renoteId: appearNote.value.id,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
}).finally(() => { renoting = false; });
|
||||
}
|
||||
}
|
||||
|
|
@ -722,12 +715,12 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
|||
}
|
||||
|
||||
function undoRenote() : void {
|
||||
if (!renoted.value) return;
|
||||
if (!appearNote.value.isRenoted) return;
|
||||
misskeyApi('notes/unrenote', {
|
||||
noteId: appearNote.value.id,
|
||||
});
|
||||
os.toast(i18n.ts.rmboost);
|
||||
renoted.value = false;
|
||||
appearNote.value.isRenoted = false;
|
||||
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-show="!isDeleted" v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1, [$style.isReply]: props.isReply, [$style.detailed]: props.detailed }]">
|
||||
<div v-show="!isDeleted" v-if="!muted && !noteMuted" ref="el" :class="[$style.root, { [$style.children]: depth > 1, [$style.isReply]: props.isReply, [$style.detailed]: props.detailed }]">
|
||||
<div v-if="!hideLine" :class="$style.line"></div>
|
||||
<div :class="$style.main">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
|
|
@ -39,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-tooltip="renoteTooltip"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@click.stop="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
|
||||
@click.stop="appearNote.isRenoted ? undoRenote() : boostVisibility($event.shiftKey)"
|
||||
>
|
||||
<i class="ph-rocket-launch ph-bold ph-lg"></i>
|
||||
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
|
||||
|
|
@ -86,8 +86,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.muted" @click="muted = false">
|
||||
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
|
||||
<div v-else :class="$style.muted" @click.stop="muted = false">
|
||||
<SkMutedNote :muted="muted" :threadMuted="false" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -122,6 +122,7 @@ import { prefer } from '@/preferences.js';
|
|||
import { useNoteCapture } from '@/use/use-note-capture.js';
|
||||
import SkMutedNote from '@/components/SkMutedNote.vue';
|
||||
import { instance, policies } from '@/instance';
|
||||
import { getAppearNote } from '@/utility/get-appear-note';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -141,14 +142,15 @@ const props = withDefaults(defineProps<{
|
|||
onDeleteCallback: undefined,
|
||||
});
|
||||
|
||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i?.id);
|
||||
const appearNote = computed(() => getAppearNote(props.note));
|
||||
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
|
||||
const hideLine = computed(() => props.detail);
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
|
||||
const translating = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
const clipButton = useTemplateRef('clipButton');
|
||||
const renoteButton = shallowRef<HTMLElement>();
|
||||
|
|
@ -156,21 +158,13 @@ const quoteButton = shallowRef<HTMLElement>();
|
|||
const menuButton = shallowRef<HTMLElement>();
|
||||
const likeButton = shallowRef<HTMLElement>();
|
||||
|
||||
const renoteTooltip = computeRenoteTooltip(renoted);
|
||||
const renoteTooltip = computeRenoteTooltip(appearNote);
|
||||
|
||||
let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
|
||||
const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
|
||||
const mergedCW = computed(() => computeMergedCw(appearNote.value));
|
||||
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds && props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`,
|
||||
|
|
@ -191,7 +185,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
|
|||
}
|
||||
}
|
||||
|
||||
const { muted } = checkMutes(appearNote.value);
|
||||
const { muted, noteMuted } = checkMutes(appearNote);
|
||||
|
||||
useNoteCapture({
|
||||
rootEl: el,
|
||||
|
|
@ -202,16 +196,6 @@ useNoteCapture({
|
|||
onDeleteCallback: props.detail && props.depth < prefer.s.numberOfReplies ? props.onDeleteCallback : undefined,
|
||||
});
|
||||
|
||||
if ($i) {
|
||||
misskeyApi('notes/renotes', {
|
||||
noteId: appearNote.value.id,
|
||||
userId: $i.id,
|
||||
limit: 1,
|
||||
}).then((res) => {
|
||||
renoted.value = res.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function focus() {
|
||||
el.value?.focus();
|
||||
}
|
||||
|
|
@ -220,8 +204,8 @@ async function reply(viaKeyboard = false): Promise<void> {
|
|||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||
showMovedDialog();
|
||||
await os.post({
|
||||
reply: props.note,
|
||||
channel: props.note.channel ?? undefined,
|
||||
reply: appearNote.value,
|
||||
channel: appearNote.value.channel ?? undefined,
|
||||
animation: !viaKeyboard,
|
||||
});
|
||||
focus();
|
||||
|
|
@ -231,9 +215,9 @@ function react(): void {
|
|||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||
showMovedDialog();
|
||||
sound.playMisskeySfx('reaction');
|
||||
if (props.note.reactionAcceptance === 'likeOnly') {
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
misskeyApi('notes/like', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
override: defaultLike.value,
|
||||
});
|
||||
const el = reactButton.value as HTMLElement | null | undefined;
|
||||
|
|
@ -247,12 +231,12 @@ function react(): void {
|
|||
}
|
||||
} else {
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value ?? null, props.note, reaction => {
|
||||
reactionPicker.show(reactButton.value ?? null, appearNote.value, reaction => {
|
||||
misskeyApi('notes/reactions/create', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
reaction: reaction,
|
||||
});
|
||||
if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
|
||||
if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
|
||||
claimAchievement('reactWithoutRead');
|
||||
}
|
||||
}, () => {
|
||||
|
|
@ -266,7 +250,7 @@ function like(): void {
|
|||
showMovedDialog();
|
||||
sound.playMisskeySfx('reaction');
|
||||
misskeyApi('notes/like', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
override: defaultLike.value,
|
||||
});
|
||||
const el = likeButton.value as HTMLElement | null | undefined;
|
||||
|
|
@ -289,12 +273,12 @@ function undoReact(note): void {
|
|||
}
|
||||
|
||||
function undoRenote() : void {
|
||||
if (!renoted.value) return;
|
||||
if (!appearNote.value.isRenoted) return;
|
||||
misskeyApi('notes/unrenote', {
|
||||
noteId: appearNote.value.id,
|
||||
});
|
||||
os.toast(i18n.ts.rmboost);
|
||||
renoted.value = false;
|
||||
appearNote.value.isRenoted = false;
|
||||
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
|
|
@ -341,7 +325,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
channelId: appearNote.value.channelId,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
});
|
||||
} else {
|
||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||
|
|
@ -360,7 +344,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
|
|||
visibility: visibility,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
renoted.value = true;
|
||||
appearNote.value.isRenoted = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -375,7 +359,7 @@ function quote() {
|
|||
}).then((cancelled) => {
|
||||
if (cancelled) return;
|
||||
misskeyApi('notes/renotes', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
userId: $i?.id,
|
||||
limit: 1,
|
||||
quote: true,
|
||||
|
|
@ -397,12 +381,12 @@ function quote() {
|
|||
}
|
||||
|
||||
function menu(): void {
|
||||
const { menu, cleanup } = getNoteMenu({ note: props.note, translating, translation, isDeleted });
|
||||
const { menu, cleanup } = getNoteMenu({ note: appearNote.value, translating, translation, isDeleted });
|
||||
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||
}
|
||||
|
||||
async function clip(): Promise<void> {
|
||||
os.popupMenu(await getNoteClipMenu({ note: props.note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||
os.popupMenu(await getNoteClipMenu({ note: appearNote.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||
}
|
||||
|
||||
async function translate() {
|
||||
|
|
@ -411,7 +395,7 @@ async function translate() {
|
|||
|
||||
if (props.detail) {
|
||||
misskeyApi('notes/children', {
|
||||
noteId: props.note.id,
|
||||
noteId: appearNote.value.id,
|
||||
limit: prefer.s.numberOfReplies,
|
||||
showQuotes: false,
|
||||
}).then(res => {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll"/>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsed" :sourceNote="appearNote" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;" @click.stop/>
|
||||
<div class="_gaps_s" style="margin-top: 6px;" @click.stop>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsed" :sourceNote="appearNote" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds"/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
||||
|
|
@ -76,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { inject, onMounted, ref, shallowRef, computed } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { versatileLang } from '@@/js/intl-const';
|
||||
import promiseLimit from 'promise-limit';
|
||||
|
|
@ -93,10 +93,9 @@ const urls = computed<string[]>(() => {
|
|||
return [];
|
||||
});
|
||||
|
||||
// todo un-ref these
|
||||
const isRefreshing = ref<Promise<void> | false>(false);
|
||||
const cachedNotes = ref(new Map<string, Misskey.entities.Note | null>());
|
||||
const cachedPreviews = ref(new Map<string, Summary | null>());
|
||||
const cachedNotes = new Map<string, Misskey.entities.Note | null>();
|
||||
const cachedPreviews = new Map<string, Summary | null>();
|
||||
const cachedUsers = new Map<string, Misskey.entities.User | null>();
|
||||
|
||||
/**
|
||||
|
|
@ -151,7 +150,7 @@ async function fetchPreviews(): Promise<Summary[]> {
|
|||
}
|
||||
|
||||
async function fetchPreview(url: string): Promise<Summary | null> {
|
||||
const cached = cachedPreviews.value.get(url);
|
||||
const cached = cachedPreviews.get(url);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
|
@ -163,15 +162,15 @@ async function fetchPreview(url: string): Promise<Summary | null> {
|
|||
if (res?.ok) {
|
||||
// Success - got the summary
|
||||
const summary: Summary = await res.json();
|
||||
cachedPreviews.value.set(url, summary);
|
||||
cachedPreviews.set(url, summary);
|
||||
if (summary.url !== url) {
|
||||
cachedPreviews.value.set(summary.url, summary);
|
||||
cachedPreviews.set(summary.url, summary);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Failed, blocked, or not found
|
||||
cachedPreviews.value.set(url, null);
|
||||
cachedPreviews.set(url, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +186,7 @@ async function attachNote(summary: Summary, noteLimiter: Limiter<Misskey.entitie
|
|||
}
|
||||
|
||||
async function fetchNote(noteUri: string): Promise<Misskey.entities.Note | null> {
|
||||
const cached = cachedNotes.value.get(noteUri);
|
||||
const cached = cachedNotes.get(noteUri);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
|
@ -197,15 +196,15 @@ async function fetchNote(noteUri: string): Promise<Misskey.entities.Note | null>
|
|||
const note = response['object'];
|
||||
|
||||
// Success - got the note
|
||||
cachedNotes.value.set(noteUri, note);
|
||||
cachedNotes.set(noteUri, note);
|
||||
if (note.uri && note.uri !== noteUri) {
|
||||
cachedNotes.value.set(note.uri, note);
|
||||
cachedNotes.set(note.uri, note);
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
// Failed, blocked, or not found
|
||||
cachedNotes.value.set(noteUri, null);
|
||||
cachedNotes.set(noteUri, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,20 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
|
||||
<div :class="$style.root">
|
||||
<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/>
|
||||
<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
|
||||
<MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton>
|
||||
</div>
|
||||
</Transition>
|
||||
<MkResult type="error">
|
||||
<MkButton :class="$style.button" rounded @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton>
|
||||
</MkResult>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { serverErrorImageUrl } from '@/instance.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'retry'): void;
|
||||
|
|
@ -25,25 +19,7 @@ const emit = defineEmits<{
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.img {
|
||||
vertical-align: bottom;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--MI-radius-md);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { h, defineAsyncComponent } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import CkFollowMouse from '../CkFollowMouse.vue';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MkResult from './MkResult.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkResult,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkResult v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
type: 'empty',
|
||||
text: 'Lorem Ipsum',
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkResult>;
|
||||
export const emptyWithNoText = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
text: undefined,
|
||||
},
|
||||
} satisfies StoryObj<typeof MkResult>;
|
||||
export const notFound = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
type: 'notFound',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkResult>;
|
||||
export const errorType = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
type: 'error',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkResult>;
|
||||
53
packages/frontend/src/components/global/MkResult.vue
Normal file
53
packages/frontend/src/components/global/MkResult.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
|
||||
<div :class="[$style.root, { [$style.warn]: type === 'notFound', [$style.error]: type === 'error' }]" class="_gaps">
|
||||
<img v-if="type === 'empty' && instance.infoImageUrl" :src="instance.infoImageUrl" draggable="false" :class="$style.img"/>
|
||||
<MkSystemIcon v-else-if="type === 'empty'" type="info" :class="$style.icon"/>
|
||||
<img v-if="type === 'notFound' && instance.notFoundImageUrl" :src="instance.notFoundImageUrl" draggable="false" :class="$style.img"/>
|
||||
<MkSystemIcon v-else-if="type === 'notFound'" type="question" :class="$style.icon"/>
|
||||
<img v-if="type === 'error' && instance.serverErrorImageUrl" :src="instance.serverErrorImageUrl" draggable="false" :class="$style.img"/>
|
||||
<MkSystemIcon v-else-if="type === 'error'" type="error" :class="$style.icon"/>
|
||||
|
||||
<div style="opacity: 0.7;">{{ props.text ?? (type === 'empty' ? i18n.ts.nothing : type === 'notFound' ? i18n.ts.notFound : type === 'error' ? i18n.ts.somethingHappened : null) }}</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from 'vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'empty' | 'notFound' | 'error';
|
||||
text?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.img {
|
||||
vertical-align: bottom;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--MI-radius-md);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
109
packages/frontend/src/components/global/MkSystemIcon.vue
Normal file
109
packages/frontend/src/components/global/MkSystemIcon.vue
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<svg v-if="type === 'info'" :class="[$style.icon, $style.info]" viewBox="0 0 160 160">
|
||||
<path d="M80,108L80,72" style="--l:37;" :class="[$style.line, $style.anim]"/>
|
||||
<path d="M80,52L80,52" :class="[$style.line, $style.fade]"/>
|
||||
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.anim]"/>
|
||||
</svg>
|
||||
<svg v-else-if="type === 'question'" :class="[$style.icon, $style.question]" viewBox="0 0 160 160">
|
||||
<path d="M80,92L79.991,84C88.799,83.98 96,76.962 96,68C96,59.038 88.953,52 79.991,52C71.03,52 64,59.038 64,68" style="--l:85;" :class="[$style.line, $style.anim]"/>
|
||||
<path d="M80,108L80,108" :class="[$style.line, $style.fade]"/>
|
||||
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.anim]"/>
|
||||
</svg>
|
||||
<svg v-else-if="type === 'success'" :class="[$style.icon, $style.success]" viewBox="0 0 160 160">
|
||||
<path d="M62,80L74,92L98,68" style="--l:50;" :class="[$style.line, $style.anim]"/>
|
||||
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.anim]"/>
|
||||
</svg>
|
||||
<svg v-else-if="type === 'warn'" :class="[$style.icon, $style.warn]" viewBox="0 0 160 160">
|
||||
<path d="M80,64L80,88" style="--l:27;" :class="[$style.line, $style.anim]"/>
|
||||
<path d="M80,108L80,108" :class="[$style.line, $style.fade]"/>
|
||||
<path d="M92,28L144,116C148.709,124.65 144.083,135.82 136,136L24,136C15.917,135.82 11.291,124.65 16,116L68,28C73.498,19.945 86.771,19.945 92,28Z" style="--l:390;" :class="[$style.line, $style.anim]"/>
|
||||
</svg>
|
||||
<svg v-else-if="type === 'error'" :class="[$style.icon, $style.error]" viewBox="0 0 160 160">
|
||||
<path d="M63,63L96,96" style="--l:47;--duration:0.3s;" :class="[$style.line, $style.anim]"/>
|
||||
<path d="M96,63L63,96" style="--l:47;--duration:0.3s;--delay:0.2s;" :class="[$style.line, $style.anim]"/>
|
||||
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.anim]"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'info' | 'question' | 'success' | 'warn' | 'error';
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.icon {
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
|
||||
&.info {
|
||||
color: var(--MI_THEME-accent);
|
||||
}
|
||||
|
||||
&.question {
|
||||
color: var(--MI_THEME-fg);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--MI_THEME-success);
|
||||
}
|
||||
|
||||
&.warn {
|
||||
color: var(--MI_THEME-warn);
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: var(--MI_THEME-error);
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 8px;
|
||||
}
|
||||
|
||||
.fill {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.anim {
|
||||
stroke-dasharray: var(--l);
|
||||
stroke-dashoffset: var(--l);
|
||||
animation: line-animation var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards;
|
||||
animation-delay: var(--delay, 0s);
|
||||
}
|
||||
|
||||
.fade {
|
||||
opacity: 0;
|
||||
animation: fade-in var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards;
|
||||
animation-delay: var(--delay, 0s);
|
||||
}
|
||||
|
||||
@keyframes line-animation {
|
||||
0% {
|
||||
stroke-dashoffset: var(--l);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" v-bind="pageHeaderProps" :class="{ _spacer: spacer }"/></template>
|
||||
<div :class="[ $style.body, { _spacer: spacer } ]">
|
||||
<MkSwiper v-if="swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs" :page="props.page">
|
||||
<MkSwiper v-if="prefer.s.enableHorizontalSwipe && swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs" :page="props.page">
|
||||
<slot></slot>
|
||||
</MkSwiper>
|
||||
<slot v-else></slot>
|
||||
|
|
@ -25,6 +25,7 @@ import type { PageHeaderProps } from './MkPageHeader.vue';
|
|||
import { useScrollPositionKeeper } from '@/use/use-scroll-position-keeper.js';
|
||||
import MkSwiper from '@/components/MkSwiper.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<PageHeaderProps & {
|
||||
reversed?: boolean;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import MkAd from './global/MkAd.vue';
|
|||
import MkPageHeader from './global/MkPageHeader.vue';
|
||||
import MkStickyContainer from './global/MkStickyContainer.vue';
|
||||
import MkLazy from './global/MkLazy.vue';
|
||||
import MkResult from './global/MkResult.vue';
|
||||
import MkSystemIcon from './global/MkSystemIcon.vue';
|
||||
import PageWithHeader from './global/PageWithHeader.vue';
|
||||
import PageWithAnimBg from './global/PageWithAnimBg.vue';
|
||||
import SearchMarker from './global/SearchMarker.vue';
|
||||
|
|
@ -61,6 +63,8 @@ export const components = {
|
|||
MkPageHeader: MkPageHeader,
|
||||
MkStickyContainer: MkStickyContainer,
|
||||
MkLazy: MkLazy,
|
||||
MkResult: MkResult,
|
||||
MkSystemIcon: MkSystemIcon,
|
||||
PageWithHeader: PageWithHeader,
|
||||
PageWithAnimBg: PageWithAnimBg,
|
||||
SearchMarker: SearchMarker,
|
||||
|
|
@ -92,6 +96,8 @@ declare module '@vue/runtime-core' {
|
|||
MkPageHeader: typeof MkPageHeader;
|
||||
MkStickyContainer: typeof MkStickyContainer;
|
||||
MkLazy: typeof MkLazy;
|
||||
MkResult: typeof MkResult;
|
||||
MkSystemIcon: typeof MkSystemIcon;
|
||||
PageWithHeader: typeof PageWithHeader;
|
||||
PageWithAnimBg: typeof PageWithAnimBg;
|
||||
SearchMarker: typeof SearchMarker;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div class="_gaps" :class="$style.textRoot">
|
||||
<Mfm :text="block.text ?? ''" :isBlock="true" :isNote="false"/>
|
||||
<div v-if="isEnabledUrlPreview" class="_gaps_s">
|
||||
<SkUrlPreviewGroup :sourceText="block.text" :showAsQuote="!page.user.rejectQuotes" @click.stop/>
|
||||
<div v-if="isEnabledUrlPreview" class="_gaps_s" @click.stop>
|
||||
<SkUrlPreviewGroup :sourceText="block.text" :showAsQuote="!page.user.rejectQuotes"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue