Merge remote-tracking branch 'misskey-original/develop' into develop
# Conflicts: # packages/backend/src/models/json-schema/user.ts # packages/frontend/src/components/MkNote.vue # packages/frontend/src/components/MkReactionsViewer.reaction.vue # packages/frontend/src/pages/settings/mute-block.word-mute.vue # packages/misskey-js/etc/misskey-js.api.md
This commit is contained in:
commit
ff79746100
75 changed files with 1234 additions and 537 deletions
|
|
@ -69,7 +69,7 @@
|
|||
"tsc-alias": "1.8.8",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "5.2.2",
|
||||
"typescript": "5.3.2",
|
||||
"uuid": "9.0.1",
|
||||
"v-code-diff": "1.7.2",
|
||||
"vanilla-tilt": "1.8.1",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Mfm :text="report.comment"/>
|
||||
</div>
|
||||
<hr/>
|
||||
<div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div>
|
||||
<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link">@{{ report.reporter.username }}</MkA></div>
|
||||
<div v-if="report.assignee">
|
||||
{{ i18n.ts.moderator }}:
|
||||
<MkAcct :user="report.assignee"/>
|
||||
|
|
|
|||
|
|
@ -246,29 +246,7 @@ function exec() {
|
|||
return;
|
||||
}
|
||||
|
||||
const matched: EmojiDef[] = [];
|
||||
const max = 30;
|
||||
|
||||
emojiDb.value.some(x => {
|
||||
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
||||
return matched.length === max;
|
||||
});
|
||||
|
||||
if (matched.length < max) {
|
||||
emojiDb.value.some(x => {
|
||||
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
||||
return matched.length === max;
|
||||
});
|
||||
}
|
||||
|
||||
if (matched.length < max) {
|
||||
emojiDb.value.some(x => {
|
||||
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
||||
return matched.length === max;
|
||||
});
|
||||
}
|
||||
|
||||
emojis.value = matched;
|
||||
emojis.value = emojiAutoComplete(props.q, emojiDb.value);
|
||||
} else if (props.type === 'mfmTag') {
|
||||
if (!props.q || props.q === '') {
|
||||
mfmTags.value = MFM_TAGS;
|
||||
|
|
@ -279,6 +257,78 @@ function exec() {
|
|||
}
|
||||
}
|
||||
|
||||
type EmojiScore = { emoji: EmojiDef, score: number };
|
||||
|
||||
function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
|
||||
if (!query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matched = new Map<string, EmojiScore>();
|
||||
|
||||
// 前方一致(エイリアスなし)
|
||||
emojiDb.some(x => {
|
||||
if (x.name.startsWith(query) && !x.aliasOf) {
|
||||
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
|
||||
// 前方一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
|
||||
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 部分一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) {
|
||||
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 簡易あいまい検索(3文字以上)
|
||||
if (matched.size < max && query.length > 3) {
|
||||
const queryChars = [...query];
|
||||
const hitEmojis = new Map<string, EmojiScore>();
|
||||
|
||||
for (const x of emojiDb) {
|
||||
// 文字列の位置を進めながら、クエリの文字を順番に探す
|
||||
|
||||
let pos = 0;
|
||||
let hit = 0;
|
||||
for (const c of queryChars) {
|
||||
pos = x.name.indexOf(c, pos);
|
||||
if (pos <= -1) break;
|
||||
hit++;
|
||||
}
|
||||
|
||||
// 半分以上の文字が含まれていればヒットとする
|
||||
if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
|
||||
hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
|
||||
}
|
||||
}
|
||||
|
||||
// ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分)
|
||||
[...hitEmojis.values()]
|
||||
.sort((x, y) => y.score - x.score)
|
||||
.slice(0, 6)
|
||||
.forEach(it => matched.set(it.emoji.name, it));
|
||||
}
|
||||
|
||||
return [...matched.values()]
|
||||
.sort((x, y) => y.score - x.score)
|
||||
.slice(0, max)
|
||||
.map(it => it.emoji);
|
||||
}
|
||||
|
||||
function onMousedown(event: Event) {
|
||||
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,10 @@ watch(v, () => {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.textarea, .codeEditorHighlighter {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -154,6 +158,8 @@ watch(v, () => {
|
|||
background-color: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
min-width: calc(100% - 24px);
|
||||
height: calc(100% - 24px);
|
||||
padding: 12px;
|
||||
line-height: 1.5em;
|
||||
font-size: 1em;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="!muted"
|
||||
v-if="!hardMuted && !muted"
|
||||
v-show="!isDeleted"
|
||||
ref="el"
|
||||
v-hotkey="keymap"
|
||||
|
|
@ -193,6 +193,7 @@ const props = withDefaults(defineProps<{
|
|||
note: Misskey.entities.Note;
|
||||
pinned?: boolean;
|
||||
mock?: boolean;
|
||||
withHardMute?: boolean;
|
||||
}>(), {
|
||||
mock: false,
|
||||
});
|
||||
|
|
@ -248,7 +249,8 @@ const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null);
|
|||
const isLong = shouldCollapsed(appearNote, urls ?? []);
|
||||
const collapsed = ref(appearNote.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||
const muted = ref(checkMute(appearNote, $i?.mutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords));
|
||||
const translation = ref<any>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
|
|
@ -256,6 +258,15 @@ const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibili
|
|||
const hideMutedNotes = defaultStore.state.hideMutedNotes;
|
||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
||||
|
||||
function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
|
||||
if (mutedWords == null) return false;
|
||||
|
||||
if (checkWordMute(note, $i, mutedWords)) return true;
|
||||
if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
|
||||
if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
'e|a|plus': () => react(true),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:ad="true"
|
||||
:class="$style.notes"
|
||||
>
|
||||
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
|
||||
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
</MkDateSeparatedList>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template #default="{ items: notifications }">
|
||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:class="[$style.root, { [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' ,[$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
|
||||
@click="toggleReaction()"
|
||||
>
|
||||
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||
<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||
<span :class="[$style.count,{ [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light'}]">{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
|
@ -214,7 +214,7 @@ if (!mock) {
|
|||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
.limitWidth {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<iframe
|
||||
ref="tweet"
|
||||
allow="fullscreen;web-share"
|
||||
sandbox="allow-popups allow-scripts allow-same-origin"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
|
||||
scrolling="no"
|
||||
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
|
||||
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"
|
||||
|
|
|
|||
|
|
@ -28,12 +28,25 @@ const props = withDefaults(defineProps<{
|
|||
mode: 'relative',
|
||||
});
|
||||
|
||||
const _time = props.time == null ? NaN :
|
||||
typeof props.time === 'number' ? props.time :
|
||||
(props.time instanceof Date ? props.time : new Date(props.time)).getTime();
|
||||
function getDateSafe(n: Date | string | number) {
|
||||
try {
|
||||
if (n instanceof Date) {
|
||||
return n;
|
||||
}
|
||||
return new Date(n);
|
||||
} catch (err) {
|
||||
return {
|
||||
getTime: () => NaN,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
|
||||
const invalid = Number.isNaN(_time);
|
||||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
let now = $ref((props.origin ?? new Date()).getTime());
|
||||
const ago = $computed(() => (now - _time) / 1000/*ms*/);
|
||||
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@kakkokari-gtyih</span>
|
||||
</a>
|
||||
<a href="https://github.com/taichanNE30" target="_blank" :class="$style.contributor">
|
||||
<a href="https://github.com/tai-cha" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@taichanNE30</span>
|
||||
<span :class="$style.contributorUsername">@tai-cha</span>
|
||||
</a>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
|
|
|||
|
|
@ -9,12 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<XHeader :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<MkSwitch :modelValue="publishing" @update:modelValue="onChangePublishing">
|
||||
{{ i18n.ts.publishing }}
|
||||
</MkSwitch>
|
||||
<MkSelect v-model="filterType" :class="$style.input" @update:modelValue="filterItems">
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
||||
<option value="expired">{{ i18n.ts.expired }}</option>
|
||||
</MkSelect>
|
||||
<div>
|
||||
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
||||
<MkAd v-if="ad.url" :specify="ad"/>
|
||||
<MkAd v-if="ad.url" :key="ad.id" :specify="ad"/>
|
||||
<MkInput v-model="ad.url" type="url">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
|
|
@ -82,14 +85,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -101,24 +104,34 @@ let ads: any[] = $ref([]);
|
|||
const localTime = new Date();
|
||||
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
|
||||
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
|
||||
let publishing = false;
|
||||
const filterType = ref('all');
|
||||
let publishing: boolean | null = null;
|
||||
|
||||
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||
ads = adsResponse.map(r => {
|
||||
const exdate = new Date(r.expiresAt);
|
||||
const stdate = new Date(r.startsAt);
|
||||
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
||||
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
||||
return {
|
||||
...r,
|
||||
expiresAt: exdate.toISOString().slice(0, 16),
|
||||
startsAt: stdate.toISOString().slice(0, 16),
|
||||
};
|
||||
});
|
||||
if (adsResponse != null) {
|
||||
ads = adsResponse.map(r => {
|
||||
const exdate = new Date(r.expiresAt);
|
||||
const stdate = new Date(r.startsAt);
|
||||
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
||||
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
||||
return {
|
||||
...r,
|
||||
expiresAt: exdate.toISOString().slice(0, 16),
|
||||
startsAt: stdate.toISOString().slice(0, 16),
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onChangePublishing = (v) => {
|
||||
publishing = v;
|
||||
const filterItems = (v) => {
|
||||
if (v === 'publishing') {
|
||||
publishing = true;
|
||||
} else if (v === 'expired') {
|
||||
publishing = false;
|
||||
} else {
|
||||
publishing = null;
|
||||
}
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
|
|
@ -197,6 +210,7 @@ function save(ad) {
|
|||
|
||||
function more() {
|
||||
os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
|
||||
if (adsResponse == null) return;
|
||||
ads = ads.concat(adsResponse.map(r => {
|
||||
const exdate = new Date(r.expiresAt);
|
||||
const stdate = new Date(r.startsAt);
|
||||
|
|
@ -213,6 +227,7 @@ function more() {
|
|||
|
||||
function refresh() {
|
||||
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||
if (adsResponse == null) return;
|
||||
ads = adsResponse.map(r => {
|
||||
const exdate = new Date(r.expiresAt);
|
||||
const stdate = new Date(r.startsAt);
|
||||
|
|
@ -252,4 +267,7 @@ definePageMetadata({
|
|||
margin-bottom: var(--margin);
|
||||
}
|
||||
}
|
||||
.input {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -214,13 +214,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
|
||||
<div class="_gaps_s">
|
||||
<MkFoldableSection>
|
||||
<template #header>Manual roles</template>
|
||||
<template #header>{{ i18n.ts._role.manualRoles }}</template>
|
||||
<div class="_gaps_s">
|
||||
<MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection>
|
||||
<template #header>Conditional roles</template>
|
||||
<template #header>{{ i18n.ts._role.conditionalRoles }}</template>
|
||||
<div class="_gaps_s">
|
||||
<MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="medium">{{ i18n.ts.medium }}</option>
|
||||
<option value="large">{{ i18n.ts.large }}</option>
|
||||
</MkRadios>
|
||||
<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
|
||||
</div>
|
||||
|
||||
<MkSelect v-model="instanceTicker">
|
||||
|
|
@ -254,6 +255,7 @@ const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serve
|
|||
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
|
||||
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
|
||||
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
|
||||
const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
|
||||
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
||||
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
||||
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
||||
|
|
@ -358,6 +360,7 @@ watch([
|
|||
overridedDeviceKind,
|
||||
mediaListWithOneImageAppearance,
|
||||
reactionsDisplaySize,
|
||||
limitWidthOfReaction,
|
||||
highlightSensitiveMedia,
|
||||
keepScreenOn,
|
||||
showMediaTimeline,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #icon><i class="ti ti-message-off"></i></template>
|
||||
<template #label>{{ i18n.ts.wordMute }}</template>
|
||||
|
||||
<XWordMute/>
|
||||
<XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-message-off"></i></template>
|
||||
<template #label>{{ i18n.ts.hardWordMute }}</template>
|
||||
|
||||
<XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
|
|
@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { $i } from '@/account.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const renoteMutingPagination = {
|
||||
|
|
@ -207,6 +215,14 @@ async function toggleBlockItem(item) {
|
|||
}
|
||||
}
|
||||
|
||||
async function saveMutedWords(mutedWords: (string | string[])[]) {
|
||||
await os.api('i/update', { mutedWords });
|
||||
}
|
||||
|
||||
async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
|
||||
await os.api('i/update', { hardMutedWords });
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
|
|
|||
|
|
@ -19,16 +19,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import {computed, ref, watch} from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import * as os from '@/os.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const props = defineProps<{
|
||||
muted: (string[] | string)[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'save', value: (string[] | string)[]): void;
|
||||
}>();
|
||||
import MkSwitch from "@/components/MkSwitch.vue";
|
||||
|
||||
const render = (mutedWords) => mutedWords.map(x => {
|
||||
|
|
@ -86,9 +87,7 @@ async function save() {
|
|||
return;
|
||||
}
|
||||
|
||||
await os.api('i/update', {
|
||||
mutedWords: parsed,
|
||||
});
|
||||
emit('save', parsed);
|
||||
|
||||
changed.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-if="fetching"/>
|
||||
<div v-if="!fetching && files.length > 0" :class="$style.stream">
|
||||
<template v-for="file in files" :key="file.note.id + file.file.id">
|
||||
<div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.sensitive" @click="showingFiles.push(file.file.id)">
|
||||
<div>
|
||||
<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
|
||||
<div>{{ i18n.ts.clickToShow }}</div>
|
||||
<div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)">
|
||||
<!-- TODO: 画像以外のファイルに対応 -->
|
||||
<ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/>
|
||||
<div :class="$style.sensitive">
|
||||
<div>
|
||||
<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
|
||||
<div>{{ i18n.ts.clickToShow }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkA v-else :class="$style.img" :to="notePage(file.note)">
|
||||
|
|
@ -90,6 +94,7 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
.img {
|
||||
position: relative;
|
||||
height: 128px;
|
||||
border-radius: 6px;
|
||||
overflow: clip;
|
||||
|
|
@ -101,8 +106,24 @@ onMounted(() => {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.sensitiveImg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: brightness(0.7);
|
||||
}
|
||||
.sensitive {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<XPages v-else-if="tab === 'pages'" :user="user"/>
|
||||
<XFlashs v-else-if="tab === 'flashs'" :user="user"/>
|
||||
<XGallery v-else-if="tab === 'gallery'" :user="user"/>
|
||||
<XRaw v-else-if="tab === 'raw'" :user="user"/>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchUser()"/>
|
||||
<MkLoading v-else/>
|
||||
|
|
@ -44,6 +45,7 @@ const XLists = defineAsyncComponent(() => import('./lists.vue'));
|
|||
const XPages = defineAsyncComponent(() => import('./pages.vue'));
|
||||
const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
|
||||
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
||||
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
acct: string;
|
||||
|
|
@ -112,6 +114,10 @@ const headerTabs = $computed(() => user ? [{
|
|||
key: 'gallery',
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'ti ti-icons',
|
||||
}, {
|
||||
key: 'raw',
|
||||
title: 'Raw',
|
||||
icon: 'ti ti-code',
|
||||
}] : []);
|
||||
|
||||
definePageMetadata(computed(() => user ? {
|
||||
|
|
|
|||
130
packages/frontend/src/pages/user/raw.vue
Normal file
130
packages/frontend/src/pages/user/raw.vue
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32">
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.userMInfoRoot">
|
||||
<MkAvatar :class="$style.userMInfoAvatar" :user="user" indicator link preview/>
|
||||
<div :class="$style.userMInfoMetaRoot">
|
||||
<span :class="$style.userMInfoMetaName"><MkUserName :class="$style.userMInfoMetaName" :user="user"/></span>
|
||||
<span :class="$style.userMInfoMetaSub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||
<span :class="$style.userMInfoMetaState">
|
||||
<span v-if="suspended" :class="$style.suspended">Suspended</span>
|
||||
<span v-if="silenced" :class="$style.silenced">Silenced</span>
|
||||
<span v-if="moderator" :class="$style.moderator">Moderator</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<MkKeyValue :copy="user.id" oneline>
|
||||
<template #key>ID</template>
|
||||
<template #value><span class="_monospace">{{ user.id }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.createdAt }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Raw</template>
|
||||
<MkObjectView tall :value="user"></MkObjectView>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { acct } from '@/filters/user.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkObjectView from '@/components/MkObjectView.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
user: Misskey.entities.User;
|
||||
}>();
|
||||
|
||||
const moderator = computed(() => props.user.isModerator ?? false);
|
||||
const silenced = computed(() => props.user.isSilenced ?? false);
|
||||
const suspended = computed(() => props.user.isSuspended ?? false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.userMInfoRoot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.userMInfoAvatar {
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.userMInfoMetaRoot {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.userMInfoMetaName {
|
||||
display: block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.userMInfoMetaSub {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 85%;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.userMInfoMetaState {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 4px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .suspended,
|
||||
> .silenced,
|
||||
> .moderator {
|
||||
display: inline-block;
|
||||
border: solid 1px;
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
> .suspended {
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
> .silenced {
|
||||
color: var(--warn);
|
||||
border-color: var(--warn);
|
||||
}
|
||||
|
||||
> .moderator {
|
||||
color: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -61,7 +61,7 @@ export const soundsTypes = [
|
|||
'noizenecio/kick_gaba7',
|
||||
] as const;
|
||||
|
||||
export async function getAudio(file: string, useCache = true) {
|
||||
export async function loadAudio(file: string, useCache = true) {
|
||||
if (useCache && cache.has(file)) {
|
||||
return cache.get(file)!;
|
||||
}
|
||||
|
|
@ -77,12 +77,6 @@ export async function getAudio(file: string, useCache = true) {
|
|||
return audioBuffer;
|
||||
}
|
||||
|
||||
export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
|
||||
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 = defaultStore.state[`sound_${type}`];
|
||||
if (_DEV_) console.log('play', type, sound);
|
||||
|
|
@ -91,16 +85,22 @@ export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notifica
|
|||
}
|
||||
|
||||
export async function playFile(file: string, volume: number) {
|
||||
const buffer = await loadAudio(file);
|
||||
createSourceNode(buffer, volume)?.start();
|
||||
}
|
||||
|
||||
export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
|
||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||
if (masterVolume === 0 || volume === 0) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const gainNode = ctx.createGain();
|
||||
gainNode.gain.value = masterVolume * volume;
|
||||
|
||||
const soundSource = ctx.createBufferSource();
|
||||
soundSource.buffer = await getAudio(file);
|
||||
soundSource.buffer = buffer;
|
||||
soundSource.connect(gainNode).connect(ctx.destination);
|
||||
soundSource.start();
|
||||
|
||||
return soundSource;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -421,6 +421,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: 'medium' as 'small' | 'medium' | 'large',
|
||||
},
|
||||
limitWidthOfReaction: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
forceShowAds: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,10 @@ const current = reactive({
|
|||
},
|
||||
});
|
||||
const prev = reactive({} as typeof current);
|
||||
const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
|
||||
let jammedAudioBuffer: AudioBuffer | null = $ref(null);
|
||||
let jammedSoundNodePlaying: boolean = $ref(false);
|
||||
|
||||
sound.loadAudio('syuilo/queue-jammed').then(buf => jammedAudioBuffer = buf);
|
||||
|
||||
for (const domain of ['inbox', 'deliver']) {
|
||||
prev[domain] = deepClone(current[domain]);
|
||||
|
|
@ -113,8 +116,13 @@ const onStats = (stats) => {
|
|||
current[domain].waiting = stats[domain].waiting;
|
||||
current[domain].delayed = stats[domain].delayed;
|
||||
|
||||
if (current[domain].waiting > 0 && widgetProps.sound && jammedSound.paused) {
|
||||
jammedSound.play();
|
||||
if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer && !jammedSoundNodePlaying) {
|
||||
const soundNode = sound.createSourceNode(jammedAudioBuffer, 1);
|
||||
if (soundNode) {
|
||||
jammedSoundNodePlaying = true;
|
||||
soundNode.onended = () => jammedSoundNodePlaying = false;
|
||||
soundNode.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ describe('MkUrlPreview', () => {
|
|||
});
|
||||
assert.exists(iframe, 'iframe should exist');
|
||||
assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share');
|
||||
assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-scripts allow-same-origin');
|
||||
assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin');
|
||||
});
|
||||
|
||||
test('Loading a post in iframe', async () => {
|
||||
|
|
@ -159,6 +159,6 @@ describe('MkUrlPreview', () => {
|
|||
});
|
||||
assert.exists(iframe, 'iframe should exist');
|
||||
assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share');
|
||||
assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-scripts allow-same-origin');
|
||||
assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue