Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
@@ -242,29 +242,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;
|
||||
@@ -275,6 +253,82 @@ 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 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
|
||||
// 前方一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.startsWith(query)) {
|
||||
matched.set(x.name, { emoji: x, score: query.length });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 部分一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.includes(query)) {
|
||||
matched.set(x.name, { emoji: x, score: query.length });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 簡易あいまい検索
|
||||
if (matched.size < max) {
|
||||
const queryChars = [...query];
|
||||
const hitEmojis = new Map<string, EmojiScore>();
|
||||
|
||||
for (const x of emojiDb) {
|
||||
// クエリ文字列の1文字単位で絵文字名にヒットするかを見る
|
||||
// ただし、過剰に検出されるのを防ぐためクエリ文字列に登場する順番で絵文字名を走査する
|
||||
|
||||
let queryCharHitPos = 0;
|
||||
let queryCharHitCount = 0;
|
||||
for (let idx = 0; idx < queryChars.length; idx++) {
|
||||
queryCharHitPos = x.name.indexOf(queryChars[idx], queryCharHitPos);
|
||||
if (queryCharHitPos <= -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
queryCharHitCount++;
|
||||
}
|
||||
|
||||
// ヒット数が少なすぎると検索結果が汚れるので調節する
|
||||
if (queryCharHitCount > 2) {
|
||||
hitEmojis.set(x.name, { emoji: x, score: queryCharHitCount });
|
||||
}
|
||||
}
|
||||
|
||||
// ヒットしたものを全部追加すると雑多になるので、先頭の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();
|
||||
}
|
||||
|
@@ -141,6 +141,10 @@ watch(v, () => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.textarea, .codeEditorHighlighter {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -156,6 +160,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"
|
||||
@@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else :class="$style.muted" @click="muted = false">
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
@@ -183,6 +183,7 @@ const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
pinned?: boolean;
|
||||
mock?: boolean;
|
||||
withHardMute?: boolean;
|
||||
}>(), {
|
||||
mock: false,
|
||||
});
|
||||
@@ -239,13 +240,23 @@ 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);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
|
||||
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>
|
||||
|
@@ -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*/);
|
||||
|
||||
|
Reference in New Issue
Block a user