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>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { computed, nextTick, reactive } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js';
|
||||
import { $i } from '@/i';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
|
@ -31,12 +30,6 @@ if (providedAt > cachedAt) {
|
|||
|
||||
export const instance: Misskey.entities.MetaDetailed = reactive(cachedMeta ?? {});
|
||||
|
||||
export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL);
|
||||
|
||||
export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO_IMAGE_URL);
|
||||
|
||||
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
|
||||
|
||||
export const isEnabledUrlPreview = computed(() => instance.enableUrlPreview ?? true);
|
||||
|
||||
export const policies = computed<Misskey.entities.RolePolicies>(() => $i?.policies ?? instance.policies);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-if="!loaded"/>
|
||||
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
|
||||
<div v-show="loaded" :class="$style.root">
|
||||
<img :src="serverErrorImageUrl" draggable="false" :class="$style.img"/>
|
||||
<img v-if="instance.serverErrorImageUrl" :src="instance.serverErrorImageUrl" draggable="false" :class="$style.img"/>
|
||||
<div class="_gaps">
|
||||
<div><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></div>
|
||||
<div v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</div>
|
||||
|
|
@ -36,7 +36,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePage } from '@/page.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { serverErrorImageUrl } from '@/instance.js';
|
||||
import { instance } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
error?: Error;
|
||||
|
|
|
|||
|
|
@ -130,6 +130,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection v-else-if="info.signupReason">
|
||||
<template #label>{{ i18n.ts.signupReason }}</template>
|
||||
{{ info.signupReason }}
|
||||
</FormSection>
|
||||
|
||||
<FormSection v-if="!isSystem && user && iAmModerator">
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
|
||||
|
|
|
|||
|
|
@ -58,14 +58,40 @@ const pagination = {
|
|||
})),
|
||||
};
|
||||
|
||||
function clear() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.clearCachedFilesConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
async function clear() {
|
||||
const { canceled, result } = await os.form(i18n.ts.clearCachedFilesOptions.title, {
|
||||
olderThanEnum: {
|
||||
label: i18n.ts.clearCachedFilesOptions.olderThan,
|
||||
type: 'enum',
|
||||
default: 'now',
|
||||
required: true,
|
||||
enum: [
|
||||
{ label: i18n.ts.clearCachedFilesOptions.now, value: 'now' },
|
||||
{ label: i18n.ts.clearCachedFilesOptions.oneWeek, value: 'oneWeek' },
|
||||
{ label: i18n.ts.clearCachedFilesOptions.oneMonth, value: 'oneMonth' },
|
||||
{ label: i18n.ts.clearCachedFilesOptions.oneYear, value: 'oneYear' },
|
||||
],
|
||||
},
|
||||
keepFilesInUse: {
|
||||
label: i18n.ts.clearCachedFilesOptions.keepFilesInUse,
|
||||
description: i18n.ts.clearCachedFilesOptions.keepFilesInUseDescription,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
os.apiWithDialog('admin/drive/clean-remote-files', {});
|
||||
if (canceled) return;
|
||||
|
||||
const timesMap = {
|
||||
now: 0,
|
||||
oneWeek: 7 * 86400,
|
||||
oneMonth: 30 * 86400,
|
||||
oneYear: 365 * 86400,
|
||||
};
|
||||
|
||||
await os.apiWithDialog('admin/drive/clean-remote-files', {
|
||||
olderThanSeconds: timesMap[result.olderThanEnum] ?? 0,
|
||||
keepFilesInUse: result.keepFilesInUse,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTl :events="timeline" :displayLimit="50" style="margin-top: var(--MI-margin);">
|
||||
<template #left="{ event }">
|
||||
<div>
|
||||
<MkAvatar :user="event.user" style="width: 24px; height: 24px;"/>
|
||||
<MkAvatar :user="event.user" style="width: 26px; height: 26px;"/>
|
||||
</div>
|
||||
</template>
|
||||
<template #right="{ event, timestamp, delta }">
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps">
|
||||
<div class="_buttons">
|
||||
<MkButton primary rounded @click="edit"><i class="ti ti-pencil"></i> {{ i18n.ts.edit }}</MkButton>
|
||||
<MkButton secondary rounded @click="clone"><i class="ti ti-copy"></i> {{ i18n.ts.clone }}</MkButton>
|
||||
<MkButton danger rounded @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
<MkFolder>
|
||||
|
|
@ -24,12 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary rounded @click="assign"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
|
||||
|
||||
<MkPagination :pagination="usersPagination" :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="_gaps_s">
|
||||
|
|
@ -70,7 +66,6 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
|
@ -97,6 +92,13 @@ function edit() {
|
|||
router.push('/admin/roles/' + role.id + '/edit');
|
||||
}
|
||||
|
||||
async function clone() {
|
||||
const newRole = await misskeyApi('admin/roles/clone', {
|
||||
roleId: role.id,
|
||||
});
|
||||
router.push('/admin/roles/' + newRole.id + '/edit');
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
|
|
|
|||
|
|
@ -262,6 +262,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-list"></i></template>
|
||||
<template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template>
|
||||
<template #footer>
|
||||
<div class="_buttons">
|
||||
<MkButton @click="federationForm.state.deliverSuspendedSoftware.push({software: '', versionRange: ''})"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div :class="$style.metadataRoot" class="_gaps_s">
|
||||
<MkInfo>{{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }}</MkInfo>
|
||||
<div v-for="(element, index) in federationForm.state.deliverSuspendedSoftware" :key="index" v-panel :class="$style.fieldDragItem">
|
||||
<button class="_button" :class="$style.dragItemRemove" @click="federationForm.state.deliverSuspendedSoftware.splice(index, 1)"><i class="ti ti-x"></i></button>
|
||||
<div :class="$style.dragItemForm">
|
||||
<FormSplit :minWidth="200">
|
||||
<MkInput v-model="element.software" small :placeholder="i18n.ts.softwareName">
|
||||
</MkInput>
|
||||
<MkInput v-model="element.versionRange" small :placeholder="i18n.ts.version">
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
|
@ -420,10 +445,12 @@ const urlPreviewForm = useForm({
|
|||
const federationForm = useForm({
|
||||
federation: meta.federation,
|
||||
federationHosts: meta.federationHosts.join('\n'),
|
||||
deliverSuspendedSoftware: meta.deliverSuspendedSoftware,
|
||||
}, async (state) => {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
federation: state.federation,
|
||||
federationHosts: state.federationHosts.split('\n'),
|
||||
deliverSuspendedSoftware: state.deliverSuspendedSoftware,
|
||||
});
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
|
@ -470,4 +497,53 @@ definePage(() => ({
|
|||
font-size: 0.85em;
|
||||
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
|
||||
}
|
||||
|
||||
.metadataRoot {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.fieldDragItem {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
align-items: flex-end;
|
||||
border-radius: 6px;
|
||||
|
||||
/* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */
|
||||
@container (max-width: 452px) {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.dragItemHandle {
|
||||
cursor: grab;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0 8px 0 0;
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.dragItemRemove {
|
||||
@extend .dragItemHandle;
|
||||
|
||||
color: #ff2a2a;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.dragItemForm {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
/>
|
||||
<MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
|
||||
</MkFukidashi>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsed" :showAsQuote="!message.fromUser.rejectQuotes" style="margin: 8px 0;"/>
|
||||
<div class="_gaps_s" style="margin: 8px 0;" @click.stop>
|
||||
<SkUrlPreviewGroup :sourceNodes="parsed" :showAsQuote="!message.fromUser.rejectQuotes"/>
|
||||
</div>
|
||||
<div :class="$style.footer">
|
||||
<button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button>
|
||||
<MkTime :class="$style.time" :time="message.createdAt"/>
|
||||
|
|
@ -53,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, provide } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
<div v-if="!fetching && invitations.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noInvitations }}</div>
|
||||
</div>
|
||||
<MkResult v-if="!fetching && invitations.length == 0" type="empty" :text="i18n.ts._chat.noInvitations"/>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="memberships.length > 0" class="_gaps_s">
|
||||
<XRoom v-for="membership in memberships" :key="membership.id" :room="membership.room!"/>
|
||||
</div>
|
||||
<div v-if="!fetching && memberships.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noRooms }}</div>
|
||||
</div>
|
||||
<MkResult v-if="!fetching && memberships.length == 0" type="empty" :text="i18n.ts._chat.noRooms"/>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="rooms.length > 0" class="_gaps_s">
|
||||
<XRoom v-for="room in rooms" :key="room.id" :room="room"/>
|
||||
</div>
|
||||
<div v-if="!fetching && rooms.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noRooms }}</div>
|
||||
</div>
|
||||
<MkResult v-if="!fetching && rooms.length == 0" type="empty" :text="i18n.ts._chat.noRooms"/>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -24,10 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<XMessage :message="message" :user="message.fromUser" :isSearchResult="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.notFound }}</div>
|
||||
</div>
|
||||
<MkResult v-else type="notFound"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -38,7 +35,6 @@ import * as Misskey from 'misskey-js';
|
|||
import XMessage from './XMessage.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
|
|
|
|||
66
packages/frontend/src/pages/debug.vue
Normal file
66
packages/frontend/src/pages/debug.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<PageWithHeader>
|
||||
<div class="_spacer" style="--MI_SPACER-w: 600px;">
|
||||
<div class="_gaps_m">
|
||||
<MkResult v-if="resultType === 'empty'" type="empty"/>
|
||||
<MkResult v-if="resultType === 'notFound'" type="notFound"/>
|
||||
<MkResult v-if="resultType === 'error'" type="error"/>
|
||||
<MkSelect
|
||||
v-model="resultType" :items="[
|
||||
{ label: 'empty', value: 'empty' },
|
||||
{ label: 'notFound', value: 'notFound' },
|
||||
{ label: 'error', value: 'error' },
|
||||
]"
|
||||
></MkSelect>
|
||||
|
||||
<MkSystemIcon v-if="iconType === 'info'" type="info" style="width: 60px;"/>
|
||||
<MkSystemIcon v-if="iconType === 'question'" type="question" style="width: 60px;"/>
|
||||
<MkSystemIcon v-if="iconType === 'success'" type="success" style="width: 60px;"/>
|
||||
<MkSystemIcon v-if="iconType === 'warn'" type="warn" style="width: 60px;"/>
|
||||
<MkSystemIcon v-if="iconType === 'error'" type="error" style="width: 60px;"/>
|
||||
<MkSelect
|
||||
v-model="iconType" :items="[
|
||||
{ label: 'info', value: 'info' },
|
||||
{ label: 'question', value: 'question' },
|
||||
{ label: 'success', value: 'success' },
|
||||
{ label: 'warn', value: 'warn' },
|
||||
{ label: 'error', value: 'error' },
|
||||
]"
|
||||
></MkSelect>
|
||||
|
||||
<div class="_buttons">
|
||||
<MkButton @click="os.alert({ type: 'error', title: 'Error', text: 'error' })">Error</MkButton>
|
||||
<MkButton @click="os.alert({ type: 'warning', title: 'Warning', text: 'warning' })">Warning</MkButton>
|
||||
<MkButton @click="os.alert({ type: 'info', title: 'Info', text: 'info' })">Info</MkButton>
|
||||
<MkButton @click="os.alert({ type: 'success', title: 'Success', text: 'success' })">Success</MkButton>
|
||||
<MkButton @click="os.alert({ type: 'question', title: 'Question', text: 'question' })">Question</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const resultType = ref('empty');
|
||||
const iconType = ref('info');
|
||||
|
||||
definePage(() => ({
|
||||
title: 'DEBUG ROOM',
|
||||
icon: 'ti ti-help-circle',
|
||||
}));
|
||||
</script>
|
||||
|
|
@ -68,10 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkKeyValue>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
<MkResult v-else type="empty"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -82,7 +79,6 @@ import MkInfo from '@/components/MkInfo.vue';
|
|||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
|
|
|||
|
|
@ -7,12 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<PageWithHeader>
|
||||
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||
<MkPagination :pagination="pagination">
|
||||
<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 }">
|
||||
<!-- TODO replace with SkDateSeparatedList when merged -->
|
||||
|
|
@ -31,7 +26,6 @@ import DynamicNote from '@/components/DynamicNote.vue';
|
|||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'i/favorites' as const,
|
||||
|
|
|
|||
|
|
@ -7,12 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty><MkResult type="empty" :text="i18n.ts.noFollowRequests"/></template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests _gaps">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
|
|
@ -48,7 +43,6 @@ import { userPage, acct } from '@/filters/user.js';
|
|||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
const paginationComponent = useTemplateRef('paginationComponent');
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps">
|
||||
<MkInfo v-if="isBaseSilenced" warn>{{ i18n.ts.silencedByBase }}</MkInfo>
|
||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance || isBaseSilenced" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||
<MkSwitch v-model="isSuspended" :disabled="!instance" @update:modelValue="toggleSuspended">{{ i18n.ts._delivery.stop }}</MkSwitch>
|
||||
<MkSwitch v-model="isSuspended" :disabled="!instance || suspensionState == 'softwareSuspended'" @update:modelValue="toggleSuspended">{{ i18n.ts._delivery.stop }}</MkSwitch>
|
||||
<MkInfo v-if="isBaseBlocked" warn>{{ i18n.ts.blockedByBase }}</MkInfo>
|
||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance || isBaseBlocked" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||
<MkSwitch v-model="rejectQuotes" :disabled="!instance" @update:modelValue="toggleRejectQuotes">{{ i18n.ts.rejectQuotesInstance }}</MkSwitch>
|
||||
|
|
@ -203,7 +203,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, useCssModule } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { ChartSrc } from '@/components/MkChart.vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
|
|
@ -231,13 +231,10 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { $i } from '@/i.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard';
|
||||
import { acct } from '@/filters/user';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkNumber from '@/components/MkNumber.vue';
|
||||
import SkBadgeStrip from '@/components/SkBadgeStrip.vue';
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
host: string;
|
||||
metaHint?: Misskey.entities.AdminMetaResponse;
|
||||
|
|
@ -252,7 +249,7 @@ const tab = ref('overview');
|
|||
const chartSrc = ref<ChartSrc>('instance-requests');
|
||||
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
|
||||
const instance = ref<Misskey.entities.FederationInstance | null>(null);
|
||||
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
|
||||
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding' | 'softwareSuspended'>('none');
|
||||
const isSuspended = ref(false);
|
||||
const isBlocked = ref(false);
|
||||
const isSilenced = ref(false);
|
||||
|
|
@ -439,6 +436,7 @@ async function toggleMediaSilenced(): Promise<void> {
|
|||
|
||||
async function toggleSuspended(): Promise<void> {
|
||||
if (!iAmModerator) return;
|
||||
if (suspensionState.value === 'softwareSuspended') return;
|
||||
await os.promiseDialog(async () => {
|
||||
if (!instance.value) throw new Error('No instance?');
|
||||
suspensionState.value = isSuspended.value ? 'manuallySuspended' : 'none';
|
||||
|
|
|
|||
|
|
@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<PageWithHeader>
|
||||
<div v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" class="_spacer" style="--MI_SPACER-w: 1200px;">
|
||||
<div :class="$style.root">
|
||||
<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/>
|
||||
<div :class="$style.text">
|
||||
<i class="ti ti-alert-triangle"></i>
|
||||
{{ i18n.ts.nothing }}
|
||||
</div>
|
||||
</div>
|
||||
<MkResult type="empty"/>
|
||||
</div>
|
||||
<div v-else class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||
<div class="_gaps_m" style="text-align: center;">
|
||||
|
|
@ -43,7 +37,7 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkInviteCode from '@/components/MkInviteCode.vue';
|
||||
import { definePage } from '@/page.js';
|
||||
import { serverErrorImageUrl, instance } from '@/instance.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
const pagingComponent = useTemplateRef('pagingComponent');
|
||||
|
|
@ -96,23 +90,3 @@ definePage(() => ({
|
|||
icon: 'ti ti-user-plus',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.img {
|
||||
vertical-align: bottom;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--MI-radius-md);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<div v-if="error != null" class="_spacer" style="--MI_SPACER-w: 1200px;">
|
||||
<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.nothing }}
|
||||
</p>
|
||||
</div>
|
||||
<MkResult type="error"/>
|
||||
</div>
|
||||
<div v-else-if="list" class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<div v-if="list" class="members _margin">
|
||||
|
|
@ -42,7 +36,6 @@ import { i18n } from '@/i18n.js';
|
|||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { definePage } from '@/page.js';
|
||||
import { serverErrorImageUrl } from '@/instance.js';
|
||||
|
||||
const props = defineProps<{
|
||||
listId: string;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<div>
|
||||
<div v-if="antennas.length === 0" class="empty">
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkResult v-if="antennas.length === 0" type="empty"/>
|
||||
|
||||
<MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
|
|
@ -32,7 +27,6 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { antennasCache } from '@/cache.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const antennas = computed(() => antennasCache.value.value ?? []);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<div class="_gaps">
|
||||
<div v-if="items.length === 0" class="empty">
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkResult v-if="items.length === 0" type="empty"/>
|
||||
|
||||
<MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton>
|
||||
|
||||
|
|
@ -35,7 +30,6 @@ import * as os from '@/os.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { userListsCache } from '@/cache.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
|
|
|||
|
|
@ -4,11 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="_fullinfo">
|
||||
<img :src="notFoundImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.notFoundDescription }}</div>
|
||||
</div>
|
||||
<div style="align-content: center; height: 100cqh;">
|
||||
<MkResult type="notFound" :text="i18n.ts.notFoundDescription"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -17,7 +14,6 @@ import { computed } from 'vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { pleaseLogin } from '@/utility/please-login.js';
|
||||
import { notFoundImageUrl } from '@/instance.js';
|
||||
|
||||
const props = defineProps<{
|
||||
showLoginPopup?: boolean;
|
||||
|
|
|
|||
|
|
@ -6,30 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :displayBackButton="true" :tabs="headerTabs">
|
||||
<div v-if="error != null" class="_spacer" style="--MI_SPACER-w: 1200px;">
|
||||
<div :class="$style.root">
|
||||
<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/>
|
||||
<p :class="$style.text">
|
||||
<i class="ti ti-alert-triangle"></i>
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
<MkResult type="error" :text="error"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'users'" class="_spacer" style="--MI_SPACER-w: 1200px;">
|
||||
<div class="_gaps_s">
|
||||
<div v-if="role">{{ role.description }}</div>
|
||||
<MkUserList v-if="visible" :pagination="users" :extractor="(item) => item.user"/>
|
||||
<div v-else-if="!visible" class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
<MkResult v-else-if="!visible" type="empty" :text="i18n.ts.nothing"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab === 'timeline'" class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/>
|
||||
<div v-else-if="!visible" class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
<MkResult v-else-if="!visible" type="empty" :text="i18n.ts.nothing"/>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
|
@ -37,13 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { instanceName } from '@@/js/config.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkUserList from '@/components/MkUserList.vue';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
roleId: string;
|
||||
|
|
@ -97,23 +83,3 @@ definePage(() => ({
|
|||
icon: 'ti ti-badge',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.img {
|
||||
vertical-align: bottom;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--MI-radius-md);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div class="_gaps_m">
|
||||
<FormPagination ref="list" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty><MkResult type="empty"/></template>
|
||||
<template #default="{items}">
|
||||
<div class="_gaps">
|
||||
<MkFolder v-for="token in items" :key="token.id" :defaultOpen="true">
|
||||
|
|
@ -63,7 +58,6 @@ import { definePage } from '@/page.js';
|
|||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const list = ref<InstanceType<typeof FormPagination>>();
|
||||
|
||||
|
|
|
|||
|
|
@ -78,12 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label><SearchLabel>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</SearchLabel></template>
|
||||
|
||||
<MkPagination :pagination="renoteMutingPagination">
|
||||
<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="_gaps_s">
|
||||
|
|
@ -115,12 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.mutedUsers }}</template>
|
||||
|
||||
<MkPagination :pagination="mutingPagination">
|
||||
<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="_gaps_s">
|
||||
|
|
@ -154,12 +144,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.blockedUsers }}</template>
|
||||
|
||||
<MkPagination :pagination="blockingPagination">
|
||||
<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="_gaps_s">
|
||||
|
|
@ -197,7 +182,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePage } from '@/page.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { instance, infoImageUrl } from '@/instance.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
|
|
|||
|
|
@ -628,6 +628,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['swipe', 'pull', 'refresh']">
|
||||
<MkPreferenceContainer k="enablePullToRefresh">
|
||||
<MkSwitch v-model="enablePullToRefresh">
|
||||
<template #label><SearchLabel>{{ i18n.ts._settings.enablePullToRefresh }}</SearchLabel></template>
|
||||
<template #caption><SearchKeyword>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchKeyword></template>
|
||||
</MkSwitch>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['keep', 'screen', 'display', 'on']">
|
||||
<MkPreferenceContainer k="keepScreenOn">
|
||||
<MkSwitch v-model="keepScreenOn">
|
||||
|
|
@ -1032,6 +1041,7 @@ const animatedMfm = prefer.model('animatedMfm');
|
|||
const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages');
|
||||
const keepScreenOn = prefer.model('keepScreenOn');
|
||||
const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
|
||||
const enablePullToRefresh = prefer.model('enablePullToRefresh');
|
||||
const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
|
||||
const contextMenu = prefer.model('contextMenu');
|
||||
const menuStyle = prefer.model('menuStyle');
|
||||
|
|
@ -1088,6 +1098,8 @@ watch([
|
|||
keepScreenOn,
|
||||
contextMenu,
|
||||
makeEveryTextElementsSelectable,
|
||||
enableHorizontalSwipe,
|
||||
enablePullToRefresh,
|
||||
noteDesign,
|
||||
], async () => {
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['description', 'bio']">
|
||||
<MkTextarea v-model="profile.description" :max="500" tall manualSave mfmAutocomplete :mfmPreview="true">
|
||||
<MkTextarea v-model="profile.description" :max="instance.maxBioLength" tall manualSave mfmAutocomplete :mfmPreview="true">
|
||||
<template #label><SearchLabel>{{ i18n.ts._profile.description }}</SearchLabel></template>
|
||||
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
|
||||
</MkTextarea>
|
||||
|
|
@ -195,6 +195,7 @@ import { langmap } from '@/utility/langmap.js';
|
|||
import { definePage } from '@/page.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { store } from '@/store.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
||||
|
|
@ -270,18 +271,13 @@ function save() {
|
|||
}
|
||||
os.apiWithDialog('i/update', {
|
||||
// 空文字列をnullにしたいので??は使うな
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
name: profile.name || null,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
description: profile.description || null,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
followedMessage: profile.followedMessage || null,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
// eslint-disable-next-line id-denylist
|
||||
location: profile.location || null,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
birthday: profile.birthday || null,
|
||||
listenbrainz: profile.listenbrainz || null,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
lang: profile.lang || null,
|
||||
isBot: !!profile.isBot,
|
||||
isCat: !!profile.isCat,
|
||||
|
|
|
|||
47
packages/frontend/src/pages/settings/profiles.vue
Normal file
47
packages/frontend/src/pages/settings/profiles.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<SearchMarker path="/settings/profiles" :label="i18n.ts._preferencesProfile.manageProfiles" :keywords="['profile', 'settings', 'preferences', 'manage']" icon="ti ti-settings-cog">
|
||||
<div class="_gaps">
|
||||
<MkFolder v-for="backup in backups">
|
||||
<template #label>{{ backup.name }}</template>
|
||||
<MkButton danger @click="del(backup)">{{ i18n.ts.delete }}</MkButton>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { deleteCloudBackup, listCloudBackups } from '@/preferences/utility.js';
|
||||
|
||||
const backups = await listCloudBackups();
|
||||
|
||||
function del(backup) {
|
||||
deleteCloudBackup(backup.name);
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePage(() => ({
|
||||
title: i18n.ts._preferencesProfile.manageProfiles,
|
||||
icon: 'ti ti-settings-cog',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
</style>
|
||||
|
|
@ -173,8 +173,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLazy>
|
||||
<div v-if="noteview === 'pinned'" class="_gaps">
|
||||
<div v-if="user.pinnedNotes.length < 1" class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false" aria-hidden="true" :alt="i18n.ts.noNotes"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
<MkResult type="empty" :text="i18n.ts.noNotes"/>
|
||||
</div>
|
||||
<div v-else class="_panel">
|
||||
<DynamicNote v-for="note of user.pinnedNotes" :key="note.id" class="note" :class="$style.pinnedNote" :note="note" :pinned="true"/>
|
||||
|
|
@ -220,7 +219,6 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
|||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import MkSparkle from '@/components/MkSparkle.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import DynamicNote from '@/components/DynamicNote.vue';
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
<div v-if="tab === 'pinned'" class="_gaps">
|
||||
<div v-if="user.pinnedNotes.length < 1" class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false" aria-hidden="true" :alt="i18n.ts.noNotes"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
<MkResult type="empty" :text="i18n.ts.noNotes"/>
|
||||
</div>
|
||||
<div v-else class="_panel">
|
||||
<DynamicNote v-for="note of user.pinnedNotes" :key="note.id" class="note" :class="$style.pinnedNote" :note="note" :pinned="true"/>
|
||||
|
|
@ -33,7 +32,6 @@ import * as Misskey from 'misskey-js';
|
|||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import DynamicNote from '@/components/DynamicNote.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
|
|
@ -10,14 +10,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div><Mfm :text="note.cw" :author="note.user"/></div>
|
||||
<MkCwButton v-model="showContent" :text="note.text" :renote="note.renote" :files="note.files" :poll="note.poll" style="margin: 4px 0;"/>
|
||||
<div v-if="showContent">
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :isBlock="true" :author="note.user"/>
|
||||
<div>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
|
||||
</div>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="noteTextEl" :class="[$style.text, { [$style.collapsed]: shouldCollapse }]">
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :isBlock="true" :author="note.user"/>
|
||||
<div>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
|
||||
</div>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<div v-if="note.files && note.files.length > 0" :class="$style.richcontent">
|
||||
|
|
|
|||
|
|
@ -303,6 +303,9 @@ export const PREF_DEF = {
|
|||
default: false,
|
||||
},
|
||||
enableHorizontalSwipe: {
|
||||
default: false,
|
||||
},
|
||||
enablePullToRefresh: {
|
||||
default: true,
|
||||
},
|
||||
useNativeUiForVideoAudioPlayer: {
|
||||
|
|
|
|||
|
|
@ -74,12 +74,17 @@ export function getPreferencesProfileMenu(): MenuItem[] {
|
|||
action: () => {
|
||||
importProfile();
|
||||
},
|
||||
}, {
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'link',
|
||||
text: i18n.ts._preferencesProfile.manageProfiles + '...',
|
||||
icon: 'ti ti-settings-cog',
|
||||
to: '/settings/profiles',
|
||||
}];
|
||||
|
||||
if (prefer.s.devMode) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: 'Copy profile as text',
|
||||
icon: 'ti ti-clipboard',
|
||||
action: () => {
|
||||
|
|
@ -145,17 +150,30 @@ export async function cloudBackup() {
|
|||
});
|
||||
}
|
||||
|
||||
export async function restoreFromCloudBackup() {
|
||||
if ($i == null) return;
|
||||
|
||||
// TODO: 更新日時でソートして取得したい
|
||||
export async function listCloudBackups() {
|
||||
const keys = await misskeyApi('i/registry/keys', {
|
||||
scope: ['client', 'preferences', 'backups'],
|
||||
});
|
||||
|
||||
if (_DEV_) console.debug(keys);
|
||||
return keys.map(k => ({
|
||||
name: k,
|
||||
}));
|
||||
}
|
||||
|
||||
if (keys.length === 0) {
|
||||
export async function deleteCloudBackup(key: string) {
|
||||
await os.apiWithDialog('i/registry/remove', {
|
||||
scope: ['client', 'preferences', 'backups'],
|
||||
key,
|
||||
});
|
||||
}
|
||||
|
||||
export async function restoreFromCloudBackup() {
|
||||
if ($i == null) return;
|
||||
|
||||
// TODO: 更新日時でソートしたい
|
||||
const backups = await listCloudBackups();
|
||||
|
||||
if (backups.length === 0) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
title: i18n.ts._preferencesBackup.noBackupsFoundTitle,
|
||||
|
|
@ -166,9 +184,9 @@ export async function restoreFromCloudBackup() {
|
|||
|
||||
const select = await os.select({
|
||||
title: i18n.ts._preferencesBackup.selectBackupToRestore,
|
||||
items: keys.map(k => ({
|
||||
text: k,
|
||||
value: k,
|
||||
items: backups.map(backup => ({
|
||||
text: backup.name,
|
||||
value: backup.name,
|
||||
})),
|
||||
});
|
||||
if (select.canceled) return;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type { RouteDef } from '@/lib/nirax.js';
|
|||
import { $i, iAmModerator } from '@/i.js';
|
||||
import MkLoading from '@/pages/_loading_.vue';
|
||||
import MkError from '@/pages/_error_.vue';
|
||||
import PageTimeline from '@/pages/timeline.vue';
|
||||
|
||||
export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
|
||||
loader: loader,
|
||||
|
|
@ -21,6 +22,13 @@ function chatPage(...args: Parameters<typeof page>) {
|
|||
}
|
||||
|
||||
export const ROUTE_DEF = [{
|
||||
name: 'index',
|
||||
path: '/',
|
||||
component: $i ? PageTimeline : page(() => import('@/pages/welcome.vue')),
|
||||
}, {
|
||||
path: '/timeline',
|
||||
component: PageTimeline,
|
||||
}, {
|
||||
path: '/@:username/pages/:pageName(*)',
|
||||
component: page(() => import('@/pages/page.vue')),
|
||||
}, {
|
||||
|
|
@ -172,6 +180,10 @@ export const ROUTE_DEF = [{
|
|||
path: '/custom-css',
|
||||
name: 'preferences',
|
||||
component: page(() => import('@/pages/settings/custom-css.vue')),
|
||||
}, {
|
||||
path: '/profiles',
|
||||
name: 'profiles',
|
||||
component: page(() => import('@/pages/settings/profiles.vue')),
|
||||
}, {
|
||||
path: '/accounts',
|
||||
name: 'profile',
|
||||
|
|
@ -600,12 +612,9 @@ export const ROUTE_DEF = [{
|
|||
component: page(() => import('@/pages/reversi/game.vue')),
|
||||
loginRequired: false,
|
||||
}, {
|
||||
path: '/timeline',
|
||||
component: page(() => import('@/pages/timeline.vue')),
|
||||
}, {
|
||||
name: 'index',
|
||||
path: '/',
|
||||
component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
|
||||
path: '/debug',
|
||||
component: page(() => import('@/pages/debug.vue')),
|
||||
loginRequired: false,
|
||||
}, {
|
||||
// テスト用リダイレクト設定。ログイン中ユーザのプロフィールにリダイレクトする
|
||||
path: '/redirect-test',
|
||||
|
|
|
|||
|
|
@ -209,7 +209,12 @@ rt {
|
|||
._spacer {
|
||||
width: 100%;
|
||||
max-width: min(var(--MI_SPACER-w, 100%), calc(100% - (var(--MI_SPACER-max, 24px) * 2)));
|
||||
margin: var(--MI_SPACER-max, 24px) auto;
|
||||
|
||||
/* marginを使って余白を表現すると、margin特有の親突き抜け仕様などが厄介になってくるので上下はpaddingを使う */
|
||||
padding: var(--MI_SPACER-max, 24px) 0;
|
||||
margin: 0 auto;
|
||||
|
||||
box-sizing: border-box;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
|
|
@ -222,13 +227,13 @@ rt {
|
|||
|
||||
._forceShrinkSpacer ._spacer {
|
||||
max-width: min(var(--MI_SPACER-w, 100%), calc(100% - (var(--MI_SPACER-min, 12px) * 2)));
|
||||
margin: var(--MI_SPACER-min, 12px) auto;
|
||||
padding: var(--MI_SPACER-min, 12px) 0;
|
||||
}
|
||||
|
||||
@container (max-width: 450px) {
|
||||
._spacer {
|
||||
max-width: min(var(--MI_SPACER-w, 100%), calc(100% - (var(--MI_SPACER-min, 12px) * 2)));
|
||||
margin: var(--MI_SPACER-min, 12px) auto;
|
||||
padding: var(--MI_SPACER-min, 12px) 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -531,18 +536,6 @@ rt {
|
|||
}
|
||||
}
|
||||
|
||||
._fullinfo {
|
||||
padding: 64px 32px;
|
||||
text-align: center;
|
||||
|
||||
> img {
|
||||
vertical-align: bottom;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--MI-radius-md);
|
||||
}
|
||||
}
|
||||
|
||||
._link {
|
||||
color: var(--MI_THEME-link);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import type { PageMetadata } from '@/page.js';
|
|||
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
|
||||
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
|
||||
import XTitlebar from '@/ui/_common_/titlebar.vue';
|
||||
import XSidebar from '@/ui/_common_/navbar.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
|
@ -51,7 +52,6 @@ import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
|
|||
import { DI } from '@/di.js';
|
||||
|
||||
const XWidgets = defineAsyncComponent(() => import('./_common_/widgets.vue'));
|
||||
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
|
||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import type { Reactive } from 'vue';
|
||||
import { deepEqual } from '@/utility/deep-equal';
|
||||
|
||||
function copy<T>(v: T): T {
|
||||
return JSON.parse(JSON.stringify(v));
|
||||
|
|
@ -27,7 +28,7 @@ export function useForm<T extends Record<string, any>>(initialState: T, save: (n
|
|||
|
||||
watch([currentState, previousState], () => {
|
||||
for (const key in modifiedStates) {
|
||||
modifiedStates[key] = currentState[key] !== previousState[key];
|
||||
modifiedStates[key] = !deepEqual(currentState[key], previousState[key]);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
|
|
|
|||
|
|
@ -82,9 +82,9 @@ export function boostMenuItems(appearNote: Ref<Misskey.entities.Note>, renote: (
|
|||
];
|
||||
}
|
||||
|
||||
export function computeRenoteTooltip(renoted: Ref<boolean>): ComputedRef<string> {
|
||||
export function computeRenoteTooltip(note: ComputedRef<Misskey.entities.Note>): ComputedRef<string> {
|
||||
return computed(() => {
|
||||
if (renoted.value) return i18n.ts.unrenote;
|
||||
if (note.value.isRenoted) return i18n.ts.unrenote;
|
||||
if (prefer.s.showVisibilitySelectorOnBoost) return i18n.ts.renote;
|
||||
return i18n.ts.renoteShift;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
|
||||
export function checkAnimationFromMfm(nodes: mfm.MfmNode[]): boolean {
|
||||
const animatedNodes = mfm.extract(nodes, (node) => {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,39 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { inject, ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import type { Ref, ComputedRef } from 'vue';
|
||||
import { $i } from '@/i';
|
||||
|
||||
export function checkMutes(noteToCheck: Misskey.entities.Note, withHardMute = false) {
|
||||
const muted = ref(checkMute(noteToCheck, $i?.mutedWords));
|
||||
const hardMuted = ref(withHardMute && checkMute(noteToCheck, $i?.hardMutedWords, true));
|
||||
return { muted, hardMuted };
|
||||
export function checkMutes(noteToCheck: ComputedRef<Misskey.entities.Note>, withHardMute?: ComputedRef<boolean>) {
|
||||
const muteEnable = ref(true);
|
||||
|
||||
const muted = computed<false | string[], boolean>({
|
||||
get() {
|
||||
if (!muteEnable.value) return false;
|
||||
return checkMute(noteToCheck.value, $i?.mutedWords);
|
||||
},
|
||||
set(value: boolean) {
|
||||
muteEnable.value = value;
|
||||
},
|
||||
});
|
||||
|
||||
const threadMuted = computed(() => {
|
||||
if (!muteEnable.value) return false;
|
||||
return noteToCheck.value.isMutingThread;
|
||||
});
|
||||
|
||||
const noteMuted = computed(() => {
|
||||
if (!muteEnable.value) return false;
|
||||
return noteToCheck.value.isMutingNote;
|
||||
});
|
||||
|
||||
const hardMuted = computed(() => {
|
||||
if (!withHardMute?.value) return false;
|
||||
return checkMute(noteToCheck.value, $i?.hardMutedWords, true);
|
||||
});
|
||||
|
||||
return { muted, hardMuted, threadMuted, noteMuted };
|
||||
}
|
||||
|
||||
export function checkMute(note: Misskey.entities.Note, mutes: undefined | null): false;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
// test is located in test/extract-mentions
|
||||
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
|
||||
export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] {
|
||||
// TODO: 重複を削除
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import type * as mfm from '@transfem-org/sfm-js';
|
||||
import type * as mfm from 'mfm-js';
|
||||
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
|
||||
import { getNoteUrls } from '@/utility/getNoteUrls';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
|
||||
// unique without hash
|
||||
// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
|
||||
|
|
|
|||
|
|
@ -213,9 +213,9 @@ export function getNoteMenu(props: {
|
|||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
|
||||
os.post({ initialNote: appearNote, renote: appearNote.renote ?? undefined, reply: appearNote.reply ?? undefined, channel: appearNote.channel });
|
||||
|
||||
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
|
||||
if ($i && Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
|
||||
claimAchievement('noteDeletedWithin1min');
|
||||
}
|
||||
});
|
||||
|
|
@ -224,8 +224,8 @@ export function getNoteMenu(props: {
|
|||
function edit(): void {
|
||||
os.post({
|
||||
initialNote: appearNote,
|
||||
renote: appearNote.renote,
|
||||
reply: appearNote.reply,
|
||||
renote: appearNote.renote ?? undefined,
|
||||
reply: appearNote.reply ?? undefined,
|
||||
channel: appearNote.channel,
|
||||
editId: appearNote.id,
|
||||
initialFiles: appearNote.files,
|
||||
|
|
@ -242,6 +242,14 @@ export function getNoteMenu(props: {
|
|||
function toggleThreadMute(mute: boolean): void {
|
||||
os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
|
||||
noteId: appearNote.id,
|
||||
noteOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
function toggleNoteMute(mute: boolean): void {
|
||||
os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
|
||||
noteId: appearNote.id,
|
||||
noteOnly: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -293,9 +301,11 @@ export function getNoteMenu(props: {
|
|||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if ($i) {
|
||||
/*
|
||||
const statePromise = misskeyApi('notes/state', {
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
*/
|
||||
|
||||
if (props.currentClip?.userId === $i.id) {
|
||||
menuItems.push({
|
||||
|
|
@ -352,15 +362,19 @@ export function getNoteMenu(props: {
|
|||
|
||||
menuItems.push({ type: 'divider' });
|
||||
|
||||
menuItems.push(statePromise.then(state => state.isFavorited ? {
|
||||
icon: 'ti ti-star-off',
|
||||
text: i18n.ts.unfavorite,
|
||||
action: () => toggleFavorite(false),
|
||||
} : {
|
||||
icon: 'ti ti-star',
|
||||
text: i18n.ts.favorite,
|
||||
action: () => toggleFavorite(true),
|
||||
}));
|
||||
if (appearNote.isFavorited) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-star-off',
|
||||
text: i18n.ts.unfavorite,
|
||||
action: () => toggleFavorite(false),
|
||||
});
|
||||
} else {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-star',
|
||||
text: i18n.ts.favorite,
|
||||
action: () => toggleFavorite(true),
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
|
|
@ -369,15 +383,33 @@ export function getNoteMenu(props: {
|
|||
children: () => getNoteClipMenu(props),
|
||||
});
|
||||
|
||||
menuItems.push(statePromise.then(state => state.isMutedThread ? {
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.unmuteThread,
|
||||
action: () => toggleThreadMute(false),
|
||||
} : {
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.muteThread,
|
||||
action: () => toggleThreadMute(true),
|
||||
}));
|
||||
if (appearNote.isMutingThread) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.unmuteThread,
|
||||
action: () => toggleThreadMute(false),
|
||||
});
|
||||
} else {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.muteThread,
|
||||
action: () => toggleThreadMute(true),
|
||||
});
|
||||
}
|
||||
|
||||
if (appearNote.isMutingNote) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.unmuteNote,
|
||||
action: () => toggleNoteMute(false),
|
||||
});
|
||||
} else {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.muteNote,
|
||||
action: () => toggleNoteMute(true),
|
||||
});
|
||||
}
|
||||
|
||||
if (appearNote.userId === $i.id) {
|
||||
if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar>
|
||||
</div>
|
||||
<div v-else :class="$style.bdayFFallback">
|
||||
<img :src="infoImageUrl" draggable="false" :class="$style.bdayFFallbackImage"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
<MkResult type="empty"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
|
@ -32,7 +31,6 @@ import type { GetFormResultType } from '@/utility/form.js';
|
|||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
const name = i18n.ts._widgets.birthdayFollowings;
|
||||
|
|
@ -134,12 +132,4 @@ defineExpose<WidgetComponentExpose>({
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bdayFFallbackImage {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
max-width: 90%;
|
||||
margin-bottom: 8px;
|
||||
border-radius: var(--MI-radius);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -11,10 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div class="ekmkgxbj">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else-if="(!items || items.length === 0) && widgetProps.showHeader" class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
<MkResult v-else-if="(!items || items.length === 0) && widgetProps.showHeader" type="empty"/>
|
||||
<div v-else :class="$style.feed">
|
||||
<a v-for="item in items" :key="item.link" :class="$style.item" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
|
||||
</div>
|
||||
|
|
@ -32,7 +29,6 @@ import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps
|
|||
import type { GetFormResultType } from '@/utility/form.js';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const name = 'rss';
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue