merge: upstream
This commit is contained in:
commit
647e76ab36
37 changed files with 493 additions and 130 deletions
|
@ -98,6 +98,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.where('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.where('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
let timeline = await query.limit(ps.limit).getMany();
|
||||
|
|
|
@ -34,7 +34,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { soundConfigStore } from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
|
@ -54,19 +54,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
|
||||
<div style="container-type: inline-size;">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'" :i="$i"/>
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;" v-on:click.stop/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == 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 pg-lg"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm
|
||||
v-if="appearNote.text"
|
||||
:parsedNodes="parsed"
|
||||
:text="appearNote.text"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'account'"
|
||||
:emojiUrls="appearNote.emojis"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -275,6 +284,13 @@ const keymap = {
|
|||
's': () => showContent.value !== showContent.value,
|
||||
};
|
||||
|
||||
provide('react', (reaction: string) => {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: appearNote.id,
|
||||
reaction: reaction,
|
||||
});
|
||||
});
|
||||
|
||||
if (props.mock) {
|
||||
watch(() => props.note, (to) => {
|
||||
note = deepClone(to);
|
||||
|
|
|
@ -68,19 +68,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</header>
|
||||
<div :class="$style.noteContent">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'" :i="$i"/>
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == 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 pg-lg"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm
|
||||
v-if="appearNote.text"
|
||||
:parsedNodes="parsed"
|
||||
:text="appearNote.text"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'account'"
|
||||
:emojiUrls="appearNote.emojis"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files.length > 0">
|
||||
|
@ -209,7 +218,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
|
@ -335,6 +344,13 @@ const keymap = {
|
|||
's': () => showContent.value !== showContent.value,
|
||||
};
|
||||
|
||||
provide('react', (reaction: string) => {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: appearNote.id,
|
||||
reaction: reaction,
|
||||
});
|
||||
});
|
||||
|
||||
let tab = $ref('replies');
|
||||
let reactionTabType = $ref(null);
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
<div>
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :i="$i" :emojiUrls="note.emojis"/>
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/>
|
||||
<MkCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
<div :class="$style.content">
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :i="$i"/>
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'"/>
|
||||
<MkCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
|
|
|
@ -166,7 +166,7 @@ defineExpose({
|
|||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
overscroll-behavior: none;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
|
|
|
@ -23,16 +23,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { onMounted, onUnmounted, watch } from 'vue';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||
|
||||
const SCROLL_STOP = 10;
|
||||
const MAX_PULL_DISTANCE = Infinity;
|
||||
const FIRE_THRESHOLD = 230;
|
||||
const RELEASE_TRANSITION_DURATION = 200;
|
||||
const PULL_BRAKE_BASE = 2;
|
||||
const PULL_BRAKE_FACTOR = 200;
|
||||
const PULL_BRAKE_BASE = 1.5;
|
||||
const PULL_BRAKE_FACTOR = 170;
|
||||
|
||||
let isPullStart = $ref(false);
|
||||
let isPullEnd = $ref(false);
|
||||
|
@ -57,18 +58,6 @@ const emit = defineEmits<{
|
|||
(ev: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
function getScrollableParentElement(node) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.scrollHeight > node.clientHeight) {
|
||||
return node;
|
||||
} else {
|
||||
return getScrollableParentElement(node.parentNode);
|
||||
}
|
||||
}
|
||||
|
||||
function getScreenY(event) {
|
||||
if (supportPointerDesktop) {
|
||||
return event.screenY;
|
||||
|
@ -138,12 +127,9 @@ function moveEnd() {
|
|||
}
|
||||
}
|
||||
|
||||
function moving(event) {
|
||||
function moving(event: TouchEvent | PointerEvent) {
|
||||
if (!isPullStart || isRefreshing || disabled) return;
|
||||
|
||||
if (!scrollEl) {
|
||||
scrollEl = getScrollableParentElement(rootEl);
|
||||
}
|
||||
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
|
||||
pullDistance = 0;
|
||||
isPullEnd = false;
|
||||
|
@ -159,6 +145,10 @@ function moving(event) {
|
|||
const moveHeight = moveScreenY - startScreenY!;
|
||||
pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||
|
||||
if (pullDistance > 0) {
|
||||
if (event.cancelable) event.preventDefault();
|
||||
}
|
||||
|
||||
isPullEnd = pullDistance >= FIRE_THRESHOLD;
|
||||
}
|
||||
|
||||
|
@ -178,24 +168,48 @@ function setDisabled(value) {
|
|||
disabled = value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// マウス操作でpull to refreshするのは不便そう
|
||||
//supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop';
|
||||
function onScrollContainerScroll() {
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
|
||||
if (supportPointerDesktop) {
|
||||
rootEl.addEventListener('pointerdown', moveStart);
|
||||
// ポインターの場合、ポップアップ系の動作をするとdownだけ発火されてupが発火されないため
|
||||
window.addEventListener('pointerup', moveEnd);
|
||||
rootEl.addEventListener('pointermove', moving, { passive: true });
|
||||
// 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 {
|
||||
rootEl.addEventListener('touchstart', moveStart);
|
||||
rootEl.addEventListener('touchend', moveEnd);
|
||||
rootEl.addEventListener('touchmove', moving, { passive: true });
|
||||
scrollEl!.style.touchAction = 'auto';
|
||||
unregisterEventListenersForReadyToPull();
|
||||
}
|
||||
}
|
||||
|
||||
function registerEventListenersForReadyToPull() {
|
||||
if (rootEl == null) return;
|
||||
rootEl.addEventListener('touchstart', moveStart, { passive: true });
|
||||
rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
|
||||
}
|
||||
|
||||
function unregisterEventListenersForReadyToPull() {
|
||||
if (rootEl == null) return;
|
||||
rootEl.removeEventListener('touchstart', moveStart);
|
||||
rootEl.removeEventListener('touchmove', moving);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (rootEl == null) return;
|
||||
|
||||
scrollEl = getScrollContainer(rootEl);
|
||||
if (scrollEl == null) return;
|
||||
|
||||
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
|
||||
|
||||
rootEl.addEventListener('touchend', moveEnd, { passive: true });
|
||||
|
||||
registerEventListenersForReadyToPull();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd);
|
||||
if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
|
||||
|
||||
unregisterEventListenersForReadyToPull();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -9,12 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" v-on:click.stop><i class="ph-arrow-bend-left-up ph-bold pg-lg"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'account'" :i="$i" :emojiUrls="note.emojis"/>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/>
|
||||
<div v-if="note.text && translating || note.text && translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="note.user" :nyaize="'account'" :i="$i" :emojiUrls="note.emojis"/>
|
||||
<Mfm :text="translation.text" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" v-on:click.stop>RN: ...</MkA>
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div class="_gaps">
|
||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._postNote.description1 }}</div>
|
||||
<MkPostForm :class="$style.exampleRoot" :mock="true"/>
|
||||
<MkPostForm :class="$style.exampleRoot" :mock="true" :autofocus="false"/>
|
||||
<MkFormSection>
|
||||
<template #label>{{ i18n.ts.visibility }}</template>
|
||||
<div class="_gaps">
|
||||
|
|
|
@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPostForm
|
||||
:class="$style.exampleRoot"
|
||||
:mock="true"
|
||||
:autofocus="false"
|
||||
:initialNote="exampleNote"
|
||||
@fileChangeSensitive="doSucceeded"
|
||||
></MkPostForm>
|
||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
|
||||
<div :class="$style.description">
|
||||
<div v-if="user.description" :class="$style.mfm">
|
||||
<Mfm :text="user.description" :author="user" :i="$i"/>
|
||||
<Mfm :text="user.description" :author="user"/>
|
||||
</div>
|
||||
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.username"><MkAcct :user="user"/></div>
|
||||
</div>
|
||||
<div :class="$style.description">
|
||||
<Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user" :i="$i"/>
|
||||
<Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user"/>
|
||||
<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
|
||||
</div>
|
||||
<div :class="$style.status">
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div :class="$style.description">
|
||||
<div v-if="user.description" :class="$style.mfm">
|
||||
<Mfm :text="user.description" :author="user" :i="$i"/>
|
||||
<Mfm :text="user.description" :author="user"/>
|
||||
</div>
|
||||
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
|
||||
</div>
|
||||
|
|
|
@ -29,19 +29,19 @@
|
|||
</header>
|
||||
<div :class="$style.noteContent">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'" :i="$i"/>
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == 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 pg-lg"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/>
|
||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files.length > 0">
|
||||
|
|
|
@ -5,14 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<span v-if="errored">:{{ customEmojiName }}:</span>
|
||||
<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/>
|
||||
<img
|
||||
v-else
|
||||
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
|
||||
:src="url"
|
||||
:alt="alt"
|
||||
:title="alt"
|
||||
decoding="async"
|
||||
@error="errored = true"
|
||||
@load="errored = false"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, inject } from 'vue';
|
||||
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
import * as os from '@/os.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
|
@ -21,8 +34,12 @@ const props = defineProps<{
|
|||
host?: string | null;
|
||||
url?: string;
|
||||
useOriginalSize?: boolean;
|
||||
menu?: boolean;
|
||||
menuReaction?: boolean;
|
||||
}>();
|
||||
|
||||
const react = inject<((name: string) => void) | null>('react', null);
|
||||
|
||||
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
|
||||
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
|
||||
|
||||
|
@ -55,6 +72,28 @@ const url = computed(() => {
|
|||
|
||||
const alt = computed(() => `:${customEmojiName.value}:`);
|
||||
let errored = $ref(url.value == null);
|
||||
|
||||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: `:${props.name}:`,
|
||||
}, {
|
||||
text: i18n.ts.copy,
|
||||
icon: 'ti ti-copy',
|
||||
action: () => {
|
||||
copyToClipboard(`:${props.name}:`);
|
||||
os.success();
|
||||
},
|
||||
}, ...(props.menuReaction && react ? [{
|
||||
text: i18n.ts.doReaction,
|
||||
icon: 'ti ti-plus',
|
||||
action: () => {
|
||||
react(`:${props.name}:`);
|
||||
},
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -33,12 +33,13 @@ type MfmProps = {
|
|||
plain?: boolean;
|
||||
nowrap?: boolean;
|
||||
author?: Misskey.entities.UserLite;
|
||||
i?: Misskey.entities.UserLite | null;
|
||||
isNote?: boolean;
|
||||
emojiUrls?: string[];
|
||||
rootScale?: number;
|
||||
nyaize: boolean | 'account';
|
||||
parsedNodes?: mfm.MfmNode[] | null;
|
||||
enableEmojiMenu?: boolean;
|
||||
enableEmojiMenuReaction?: boolean;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@ -328,6 +329,8 @@ export default function(props: MfmProps) {
|
|||
normal: props.plain,
|
||||
host: null,
|
||||
useOriginalSize: scale >= 2.5,
|
||||
menu: props.enableEmojiMenu,
|
||||
menuReaction: props.enableEmojiMenuReaction,
|
||||
})];
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<Mfm :text="block.text" :isNote="false" :i="$i"/>
|
||||
<Mfm :text="block.text" :isNote="false"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template>
|
||||
<div :class="$style.contributors">
|
||||
<div :class="$style.contributors" style="margin-bottom: 8px;">
|
||||
<a href="https://github.com/Mar0xy" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/8841466?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@Mar0xy</span>
|
||||
|
@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>Misskey Contributors</template>
|
||||
<div :class="$style.contributors">
|
||||
<div :class="$style.contributors" style="margin-bottom: 8px;">
|
||||
<a href="https://github.com/syuilo" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/4439005?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@syuilo</span>
|
||||
|
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.bannerFade"></div>
|
||||
</div>
|
||||
<div v-if="channel.description" :class="$style.description">
|
||||
<Mfm :text="channel.description" :isNote="false" :i="$i"/>
|
||||
<Mfm :text="channel.description" :isNote="false"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="clip" class="_gaps">
|
||||
<div class="_panel">
|
||||
<div v-if="clip.description" :class="$style.description">
|
||||
<Mfm :text="clip.description" :isNote="false" :i="$i"/>
|
||||
<Mfm :text="clip.description" :isNote="false"/>
|
||||
</div>
|
||||
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||
|
|
|
@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
|
||||
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
|
||||
<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.pinnedList }}</template>
|
||||
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
|
||||
|
@ -53,7 +52,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
|
||||
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
|
||||
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
|
||||
<MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch>
|
||||
<MkRadios v-model="reactionsDisplaySize">
|
||||
<template #label>{{ i18n.ts.reactionsDisplaySize }}</template>
|
||||
<option value="small">{{ i18n.ts.small }}</option>
|
||||
|
@ -159,6 +157,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_m">
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
|
||||
<MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch>
|
||||
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
||||
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
||||
<MkSwitch v-model="clickToOpen">{{ i18n.ts.clickToOpen }}</MkSwitch>
|
||||
|
@ -270,7 +269,6 @@ const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('
|
|||
const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
|
||||
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
|
||||
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
||||
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
||||
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
||||
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<FormSection>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
|
||||
<MkButton danger @click="updateRepliesAll(true)"><i class="ph-chats ph-bold ph-lg"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton>
|
||||
<MkButton danger @click="updateRepliesAll(false)"><i class="ph-chat ph-bold ph-lg"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
|
||||
</div>
|
||||
|
@ -102,6 +103,7 @@ import FormSection from '@/components/form/section.vue';
|
|||
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
|
||||
const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
|
||||
const devMode = computed(defaultStore.makeGetterSetter('devMode'));
|
||||
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
||||
|
||||
function onChangeInjectFeaturedNote(v) {
|
||||
os.api('i/update', {
|
||||
|
|
|
@ -32,20 +32,20 @@ import MkRange from '@/components/MkRange.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { soundConfigStore } from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const masterVolume = computed(soundConfigStore.makeGetterSetter('sound_masterVolume'));
|
||||
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
|
||||
|
||||
const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel'] as const;
|
||||
|
||||
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
|
||||
note: soundConfigStore.reactiveState.sound_note,
|
||||
noteMy: soundConfigStore.reactiveState.sound_noteMy,
|
||||
notification: soundConfigStore.reactiveState.sound_notification,
|
||||
antenna: soundConfigStore.reactiveState.sound_antenna,
|
||||
channel: soundConfigStore.reactiveState.sound_channel,
|
||||
note: defaultStore.reactiveState.sound_note,
|
||||
noteMy: defaultStore.reactiveState.sound_noteMy,
|
||||
notification: defaultStore.reactiveState.sound_notification,
|
||||
antenna: defaultStore.reactiveState.sound_antenna,
|
||||
channel: defaultStore.reactiveState.sound_channel,
|
||||
});
|
||||
|
||||
async function updated(type: keyof typeof sounds.value, sound) {
|
||||
|
@ -54,14 +54,14 @@ async function updated(type: keyof typeof sounds.value, sound) {
|
|||
volume: sound.volume,
|
||||
};
|
||||
|
||||
soundConfigStore.set(`sound_${type}`, v);
|
||||
defaultStore.set(`sound_${type}`, v);
|
||||
sounds.value[type] = v;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) {
|
||||
const v = soundConfigStore.def[`sound_${sound}`].default;
|
||||
soundConfigStore.set(`sound_${sound}`, v);
|
||||
const v = defaultStore.def[`sound_${sound}`].default;
|
||||
defaultStore.set(`sound_${sound}`, v);
|
||||
sounds.value[sound] = v;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div class="description">
|
||||
<MkOmit>
|
||||
<Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user" :i="$i"/>
|
||||
<Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user"/>
|
||||
<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
|
||||
</MkOmit>
|
||||
</div>
|
||||
|
@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Mfm :text="field.name" :plain="true" :colored="false"/>
|
||||
</dt>
|
||||
<dd class="value">
|
||||
<Mfm :text="field.value" :author="user" :i="$i" :colored="false"/>
|
||||
<Mfm :text="field.value" :author="user" :colored="false"/>
|
||||
<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ph-seal-check ph-bold ph-lg" :class="$style.verifiedLink"></i>
|
||||
</dd>
|
||||
</dl>
|
||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_panel" :class="$style.content">
|
||||
<div>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ph-arrow-u-up-left ph-bold pg-lg"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<div v-if="note.files.length > 0" :class="$style.richcontent">
|
||||
|
|
|
@ -3,18 +3,25 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const enRegex1 = /(?<=n)a/gi;
|
||||
const enRegex2 = /(?<=morn)ing/gi;
|
||||
const enRegex3 = /(?<=every)one/gi;
|
||||
const koRegex1 = /[나-낳]/g;
|
||||
const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm;
|
||||
const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm;
|
||||
|
||||
export function nyaize(text: string): string {
|
||||
return text
|
||||
// ja-JP
|
||||
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
|
||||
// en-US
|
||||
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
|
||||
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
|
||||
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
|
||||
.replace(enRegex1, x => x === 'A' ? 'YA' : 'ya')
|
||||
.replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan')
|
||||
.replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan')
|
||||
// ko-KR
|
||||
.replace(/[나-낳]/g, match => String.fromCharCode(
|
||||
.replace(koRegex1, match => String.fromCharCode(
|
||||
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
|
||||
))
|
||||
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
|
||||
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
|
||||
.replace(koRegex2, '다냥')
|
||||
.replace(koRegex3, '냥');
|
||||
}
|
||||
|
|
|
@ -3,47 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { markRaw } from 'vue';
|
||||
import { Storage } from '@/pizzax.js';
|
||||
|
||||
export const soundConfigStore = markRaw(new Storage('sound', {
|
||||
sound_masterVolume: {
|
||||
where: 'device',
|
||||
default: 0.3,
|
||||
},
|
||||
sound_note: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/n-aec', volume: 0 },
|
||||
},
|
||||
sound_noteMy: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/n-cea-4va', volume: 1 },
|
||||
},
|
||||
sound_notification: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/n-ea', volume: 1 },
|
||||
},
|
||||
sound_antenna: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/triple', volume: 1 },
|
||||
},
|
||||
sound_channel: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/square-pico', volume: 1 },
|
||||
},
|
||||
}));
|
||||
|
||||
await soundConfigStore.ready;
|
||||
|
||||
//#region サウンドのColdDeviceStorage => indexedDBのマイグレーション
|
||||
for (const target of Object.keys(soundConfigStore.state) as Array<keyof typeof soundConfigStore.state>) {
|
||||
const value = localStorage.getItem(`miux:${target}`);
|
||||
if (value) {
|
||||
soundConfigStore.set(target, JSON.parse(value) as typeof soundConfigStore.def[typeof target]['default']);
|
||||
localStorage.removeItem(`miux:${target}`);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const cache = new Map<string, HTMLAudioElement>();
|
||||
|
||||
|
@ -112,13 +72,13 @@ export function getAudio(file: string, useCache = true): HTMLAudioElement {
|
|||
}
|
||||
|
||||
export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
|
||||
const masterVolume = soundConfigStore.state.sound_masterVolume;
|
||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||
audio.volume = masterVolume - ((1 - volume) * masterVolume);
|
||||
return audio;
|
||||
}
|
||||
|
||||
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
|
||||
const sound = soundConfigStore.state[`sound_${type}`];
|
||||
const sound = defaultStore.state[`sound_${type}`];
|
||||
if (_DEV_) console.log('play', type, sound);
|
||||
if (sound.type == null) return;
|
||||
playFile(sound.type, sound.volume);
|
||||
|
|
|
@ -398,6 +398,31 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
|
||||
sound_masterVolume: {
|
||||
where: 'device',
|
||||
default: 0.3,
|
||||
},
|
||||
sound_note: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/n-aec', volume: 0 },
|
||||
},
|
||||
sound_noteMy: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/n-cea-4va', volume: 1 },
|
||||
},
|
||||
sound_notification: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/n-ea', volume: 1 },
|
||||
},
|
||||
sound_antenna: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/triple', volume: 1 },
|
||||
},
|
||||
sound_channel: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/square-pico', volume: 1 },
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
|
|
@ -324,7 +324,7 @@ $widgets-hide-threshold: 1090px;
|
|||
min-width: 0;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior: none;
|
||||
overscroll-behavior: contain;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue