@@ -104,8 +104,8 @@ 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];
+const filterType = ref('all');
let publishing: boolean | null = null;
-let type = ref('null');
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
if (adsResponse != null) {
@@ -123,9 +123,15 @@ os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
}
});
-const onChangePublishing = (v) => {
- console.log(v);
- publishing = v === 'true' ? true : v === 'false' ? false : null;
+const filterItems = (v) => {
+ if (v === 'publishing') {
+ publishing = true;
+ } else if (v === 'expired') {
+ publishing = false;
+ } else {
+ publishing = null;
+ }
+
refresh();
};
From ded328fb43adda0ca61946de1ce1e94bc8e37b1d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?=
<46447427+samunohito@users.noreply.github.com>
Date: Thu, 23 Nov 2023 08:13:51 +0900
Subject: [PATCH 04/10] =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=81=AE?=
=?UTF-8?q?=E3=82=AA=E3=83=BC=E3=83=88=E3=82=B3=E3=83=B3=E3=83=97=E3=83=AA?=
=?UTF-8?q?=E3=83=BC=E3=83=88=E5=BC=B7=E5=8C=96=E3=81=AE=E5=AF=BE=E5=BF=9C?=
=?UTF-8?q?=20(#12365)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 前方一致・部分一致でなくても近似値でヒットするように
* fix CHANGELOG.md
* for of に変更
---------
Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
---
CHANGELOG.md | 1 +
.../src/components/MkAutocomplete.vue | 100 ++++++++++++++----
2 files changed, 78 insertions(+), 23 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1113374595..bc39e423c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
### Client
+- Enhance: 絵文字のオートコンプリート機能強化 #12364
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 7e0c219045..a0f4961116 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -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
();
+
+ // 前方一致(エイリアスなし)
+ 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();
+
+ 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();
}
From 864827f788cd1671a4db2ebc159c1c8ab031b7ad Mon Sep 17 00:00:00 2001
From: anatawa12
Date: Thu, 23 Nov 2023 18:56:20 +0900
Subject: [PATCH 05/10] Hard mute (#12376)
* feat(backend,misskey-js): hard mute storage in backend
* fix(backend,misskey-js): mute word record type
* chore(frontend): generalize XWordMute
* feat(frontend): configure hard mute
* feat(frontend): hard mute notes on the timelines
* lint(backend,frontend): fix lint failure
* chore(misskey-js): update api.md
* fix(backend): test failure
* chore(frontend): check word mute for reply
* chore: limit hard mute count
---
locales/index.d.ts | 1 +
locales/ja-JP.yml | 1 +
.../migration/1700383825690-hard-mute.js | 11 ++++++
.../src/core/entities/UserEntityService.ts | 1 +
packages/backend/src/models/UserProfile.ts | 7 +++-
.../backend/src/models/json-schema/user.ts | 12 +++++++
.../src/server/api/endpoints/i/update.ts | 34 +++++++++++++++----
packages/backend/test/e2e/users.ts | 1 +
packages/frontend/src/components/MkNote.vue | 17 ++++++++--
packages/frontend/src/components/MkNotes.vue | 2 +-
.../src/components/MkNotifications.vue | 2 +-
.../src/pages/settings/mute-block.vue | 18 +++++++++-
.../pages/settings/mute-block.word-mute.vue | 22 ++++++------
packages/misskey-js/etc/misskey-js.api.md | 12 ++++---
packages/misskey-js/src/api.types.ts | 3 +-
packages/misskey-js/src/entities.ts | 3 +-
16 files changed, 114 insertions(+), 33 deletions(-)
create mode 100644 packages/backend/migration/1700383825690-hard-mute.js
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 39fbb57799..3ed2706298 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -642,6 +642,7 @@ export interface Locale {
"smtpSecureInfo": string;
"testEmail": string;
"wordMute": string;
+ "hardWordMute": string;
"regexpError": string;
"regexpErrorDescription": string;
"instanceMute": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 3757715c0f..5982e71716 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -639,6 +639,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。"
testEmail: "配信テスト"
wordMute: "ワードミュート"
+hardWordMute: "ハードワードミュート"
regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
instanceMute: "サーバーミュート"
diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js
new file mode 100644
index 0000000000..afd3247f5c
--- /dev/null
+++ b/packages/backend/migration/1700383825690-hard-mute.js
@@ -0,0 +1,11 @@
+export class HardMute1700383825690 {
+ name = 'HardMute1700383825690'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
+ }
+}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 17e7988176..917f4e06d0 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -473,6 +473,7 @@ export class UserEntityService implements OnModuleInit {
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
unreadNotificationsCount: notificationsInfo?.unreadCount,
mutedWords: profile!.mutedWords,
+ hardMutedWords: profile!.hardMutedWords,
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: [], // 後方互換性のため
notificationRecieveConfig: profile!.notificationRecieveConfig,
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index d6d85c5609..8a43b60039 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -215,7 +215,12 @@ export class MiUserProfile {
@Column('jsonb', {
default: [],
})
- public mutedWords: string[][];
+ public mutedWords: (string[] | string)[];
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public hardMutedWords: (string[] | string)[];
@Column('jsonb', {
default: [],
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index b0e18db01a..a2ec203e96 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -530,6 +530,18 @@ export const packedMeDetailedOnlySchema = {
},
},
},
+ hardMutedWords: {
+ type: 'array',
+ nullable: false, optional: false,
+ items: {
+ type: 'array',
+ nullable: false, optional: false,
+ items: {
+ type: 'string',
+ nullable: false, optional: false,
+ },
+ },
+ },
mutedInstances: {
type: 'array',
nullable: true, optional: false,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index b00aa87bee..8ba29c5658 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -123,6 +123,11 @@ export const meta = {
},
} as const;
+const muteWords = { type: 'array', items: { oneOf: [
+ { type: 'array', items: { type: 'string' } },
+ { type: 'string' }
+] } } as const;
+
export const paramDef = {
type: 'object',
properties: {
@@ -171,7 +176,8 @@ export const paramDef = {
autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
- mutedWords: { type: 'array' },
+ mutedWords: muteWords,
+ hardMutedWords: muteWords,
mutedInstances: { type: 'array', items: {
type: 'string',
} },
@@ -234,16 +240,20 @@ export default class extends Endpoint { // eslint-
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
- if (ps.mutedWords !== undefined) {
+
+ function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
// TODO: ちゃんと数える
const length = JSON.stringify(ps.mutedWords).length;
- if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
+ if (length > limit) {
throw new ApiError(meta.errors.tooManyMutedWords);
}
+ }
- // validate regular expression syntax
- ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
- const regexp = x.match(/^\/(.+)\/(.*)$/);
+ function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
+ for (const mutedWord of mutedWords) {
+ if (typeof mutedWord !== "string") continue;
+
+ const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try {
@@ -251,11 +261,21 @@ export default class extends Endpoint { // eslint-
} catch (err) {
throw new ApiError(meta.errors.invalidRegexp);
}
- });
+ }
+ }
+
+ if (ps.mutedWords !== undefined) {
+ checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
+ validateMuteWordRegex(ps.mutedWords);
profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
+ if (ps.hardMutedWords !== undefined) {
+ checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
+ validateMuteWordRegex(ps.hardMutedWords);
+ profileUpdates.hardMutedWords = ps.hardMutedWords;
+ }
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 1867525cc8..2ce8fbc129 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -168,6 +168,7 @@ describe('ユーザー', () => {
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
unreadAnnouncements: user.unreadAnnouncements,
mutedWords: user.mutedWords,
+ hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances,
mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig,
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index e300ef88a5..6349df2e30 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -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(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 | 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),
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 89fd504dcc..7af31074db 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:ad="true"
:class="$style.notes"
>
-
+
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 0c817bd64c..7b072fa492 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index c6cbd424ec..4883ca0df4 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.wordMute }}
-
+
+
+
+
+
+ {{ i18n.ts.hardWordMute }}
+
+
@@ -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(() => []);
diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
index 25a836ea55..7328967c51 100644
--- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
@@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only