;
app.enableShutdownHooks();
});
@@ -102,6 +143,15 @@ describe('CheckModeratorsActivityProcessorService', () => {
now: new Date(baseDate),
shouldClearNativeTimers: true,
});
+
+ systemWebhook1 = crateSystemWebhook({ on: ['inactiveModeratorsWarning'] });
+ systemWebhook2 = crateSystemWebhook({ on: ['inactiveModeratorsWarning', 'inactiveModeratorsInvitationOnlyChanged'] });
+ systemWebhook3 = crateSystemWebhook({ on: ['abuseReport'] });
+
+ emailService.sendEmail.mockReturnValue(Promise.resolve());
+ announcementService.create.mockReturnValue(Promise.resolve({} as never));
+ systemWebhookService.fetchActiveSystemWebhooks.mockResolvedValue([systemWebhook1, systemWebhook2, systemWebhook3]);
+ systemWebhookService.enqueueSystemWebhook.mockReturnValue(Promise.resolve({} as never));
});
afterEach(async () => {
@@ -109,6 +159,9 @@ describe('CheckModeratorsActivityProcessorService', () => {
await usersRepository.delete({});
await userProfilesRepository.delete({});
roleService.getModerators.mockReset();
+ announcementService.create.mockReset();
+ emailService.sendEmail.mockReset();
+ systemWebhookService.enqueueSystemWebhook.mockReset();
});
afterAll(async () => {
@@ -152,7 +205,7 @@ describe('CheckModeratorsActivityProcessorService', () => {
expect(result.inactiveModerators).toEqual([user1]);
});
- test('[countdown] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
+ test('[remainingTime] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -165,10 +218,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
- expect(result.inactivityLimitCountdown).toBe(1);
+ expect(result.remainingTime.asDays).toBe(1);
+ expect(result.remainingTime.asHours).toBe(24);
});
- test('[countdown] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
+ test('[remainingTime] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -181,10 +235,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
- expect(result.inactivityLimitCountdown).toBe(1);
+ expect(result.remainingTime.asDays).toBe(1);
+ expect(result.remainingTime.asHours).toBe(25);
});
- test('[countdown] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
+ test('[remainingTime] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -197,10 +252,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
- expect(result.inactivityLimitCountdown).toBe(0);
+ expect(result.remainingTime.asDays).toBe(0);
+ expect(result.remainingTime.asHours).toBe(23);
});
- test('[countdown] 期限ちょうどの場合、猶予0日として計算される', async () => {
+ test('[remainingTime] 期限ちょうどの場合、猶予0日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -213,10 +269,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
- expect(result.inactivityLimitCountdown).toBe(0);
+ expect(result.remainingTime.asDays).toBe(0);
+ expect(result.remainingTime.asHours).toBe(0);
});
- test('[countdown] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
+ test('[remainingTime] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -229,7 +286,94 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(true);
expect(result.inactiveModerators).toEqual([user1, user2]);
- expect(result.inactivityLimitCountdown).toBe(-1);
+ expect(result.remainingTime.asDays).toBe(-1);
+ expect(result.remainingTime.asHours).toBe(-1);
+ });
+
+ test('[remainingTime] 期限より25時間超過している場合、猶予-2日として計算される', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 10) }),
+ // 猶予はこのユーザ基準で計算される想定。
+ // 期限より1時間超過->猶予-1日として計算されるはずである
+ createUser({ lastActiveDate: subDays(subHours(baseDate, 25), 7) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(true);
+ expect(result.inactiveModerators).toEqual([user1, user2]);
+ expect(result.remainingTime.asDays).toBe(-2);
+ expect(result.remainingTime.asHours).toBe(-25);
+ });
+ });
+
+ describe('notifyInactiveModeratorsWarning', () => {
+ test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => {
+ const [user1, user2, user3, user4, root] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ createUser({}, { email: 'user2@example.com', emailVerified: false }),
+ createUser({}, { email: null, emailVerified: false }),
+ createUser({}, { email: 'user4@example.com', emailVerified: true }),
+ createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1, user2, user3, root]);
+ await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+ expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+ expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+ expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+ });
+
+ test('[systemWebhook] "inactiveModeratorsWarning"が有効なSystemWebhookに対して送信される', async () => {
+ const [user1] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1]);
+ await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(2);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook1);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook2);
+ });
+ });
+
+ describe('notifyChangeToInvitationOnly', () => {
+ test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => {
+ const [user1, user2, user3, user4, root] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ createUser({}, { email: 'user2@example.com', emailVerified: false }),
+ createUser({}, { email: null, emailVerified: false }),
+ createUser({}, { email: 'user4@example.com', emailVerified: true }),
+ createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1, user2, user3, root]);
+ await service.notifyChangeToInvitationOnly();
+
+ expect(announcementService.create).toHaveBeenCalledTimes(4);
+ expect(announcementService.create.mock.calls[0][0].userId).toBe(user1.id);
+ expect(announcementService.create.mock.calls[1][0].userId).toBe(user2.id);
+ expect(announcementService.create.mock.calls[2][0].userId).toBe(user3.id);
+ expect(announcementService.create.mock.calls[3][0].userId).toBe(root.id);
+
+ expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+ expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+ expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+ });
+
+ test('[systemWebhook] "inactiveModeratorsInvitationOnlyChanged"が有効なSystemWebhookに対して送信される', async () => {
+ const [user1] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1]);
+ await service.notifyChangeToInvitationOnly();
+
+ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2);
});
});
});
diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
index d4b4827c90..f5b064c293 100644
--- a/packages/frontend-embed/src/components/EmNote.vue
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -108,6 +108,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, inject, ref, shallowRef } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { url } from '@@/js/config.js';
import I18n from '@/components/I18n.vue';
import EmNoteSub from '@/components/EmNoteSub.vue';
import EmNoteHeader from '@/components/EmNoteHeader.vue';
@@ -123,8 +125,6 @@ import EmUserName from '@/components/EmUserName.vue';
import EmTime from '@/components/EmTime.vue';
import { userPage } from '@/utils.js';
import { i18n } from '@/i18n.js';
-import { shouldCollapsed } from '@@/js/collapsed.js';
-import { url } from '@@/js/config.js';
function getAppearNote(note: Misskey.entities.Note) {
return Misskey.note.isPureRenote(note) ? note.renote : note;
@@ -164,14 +164,8 @@ const isDeleted = ref(false);
font-size: 1.05em;
overflow: clip;
contain: content;
-
- // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
- // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
- // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
- // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
- // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
- //content-visibility: auto;
- //contain-intrinsic-size: 0 128px;
+ content-visibility: auto;
+ contain-intrinsic-size: 0 150px;
&:focus-visible {
outline: none;
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index a8a32e8bc7..f04e5cf7c6 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -9,6 +9,7 @@ import MkAd from '@/components/global/MkAd.vue';
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js';
import { MisskeyEntity } from '@/types/date-separated-list.js';
@@ -99,11 +100,13 @@ export default defineComponent({
return [el, separator];
} else {
- if (props.ad && item._shouldInsertAd_) {
- return [h(MkAd, {
+ if (props.ad && instance.ads.length > 0 && item._shouldInsertAd_) {
+ return [h('div', {
key: item.id + ':ad',
+ class: $style['ad-wrapper'],
+ }, [h(MkAd, {
prefer: ['horizontal', 'horizontal-big'],
- }), el];
+ })]), el];
} else {
return el;
}
@@ -253,5 +256,11 @@ export default defineComponent({
.date-2-icon {
margin-left: 8px;
}
+
+.ad-wrapper {
+ padding: 8px;
+ background-size: auto auto;
+ background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
+}
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 2dcba7a50e..32c1a2d172 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -12,13 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ item.text }}
{{ item.indicateValue }}
-
+
{{ item.text }}
{{ item.indicateValue }}
-
+
@@ -139,7 +139,6 @@ function close() {
left: 32px;
color: var(--MI_THEME-indicator);
font-size: 8px;
- animation: global-blink 1s infinite;
@media (max-width: 500px) {
top: 16px;
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 59f36f8eec..13a65e411f 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ item.text }}
-
+
{{ item.text }}
-
+
{{ item.text }}
-
+
@@ -639,7 +639,6 @@ onBeforeUnmount(() => {
align-items: center;
color: var(--MI_THEME-indicator);
font-size: 12px;
- animation: global-blink 1s infinite;
}
.divider {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index be93b3c529..828ad2e872 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
- :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
+ :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]"
:tabindex="isDeleted ? '-1' : '0'"
>
@@ -171,6 +171,9 @@ import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } fro
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { host } from '@@/js/config.js';
+import type { MenuItem } from '@/types/menu.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -200,11 +203,8 @@ import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
-import type { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
-import { shouldCollapsed } from '@@/js/collapsed.js';
-import { host } from '@@/js/config.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
@@ -619,14 +619,6 @@ function emitUpdReaction(emoji: string, delta: number) {
overflow: clip;
contain: content;
- // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
- // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
- // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
- // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
- // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
- //content-visibility: auto;
- //contain-intrinsic-size: 0 128px;
-
&:focus-visible {
outline: none;
@@ -687,6 +679,11 @@ function emitUpdReaction(emoji: string, delta: number) {
}
}
+.skipRender {
+ content-visibility: auto;
+ contain-intrinsic-size: 0 150px;
+}
+
.tip {
display: flex;
align-items: center;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index bef425097e..093bdb8b4c 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -220,6 +220,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
overflow-wrap: break-word;
display: flex;
contain: content;
+ content-visibility: auto;
+ contain-intrinsic-size: 0 100px;
--eventFollow: #36aed2;
--eventRenote: #36d298;
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index a00cf0d9d3..485d003f93 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -55,6 +55,18 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ {{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsWarning }}
+
+
+
+
+
+ {{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsInvitationOnlyChanged }}
+
+
+
@@ -100,6 +112,8 @@ type EventType = {
abuseReport: boolean;
abuseReportResolved: boolean;
userCreated: boolean;
+ inactiveModeratorsWarning: boolean;
+ inactiveModeratorsInvitationOnlyChanged: boolean;
}
const emit = defineEmits<{
@@ -123,6 +137,8 @@ const events = ref({
abuseReport: true,
abuseReportResolved: true,
userCreated: true,
+ inactiveModeratorsWarning: true,
+ inactiveModeratorsInvitationOnlyChanged: true,
});
const isActive = ref(true);
@@ -130,6 +146,8 @@ const disabledEvents = ref({
abuseReport: false,
abuseReportResolved: false,
userCreated: false,
+ inactiveModeratorsWarning: false,
+ inactiveModeratorsInvitationOnlyChanged: false,
});
const disableSubmitButton = computed(() => {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 3194641cdb..7cb48f6afb 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -51,6 +51,11 @@ watch(name, () => {
// 空文字列をnullにしたいので??は使うな
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
name: name.value || null,
+ }, undefined, {
+ '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
+ title: i18n.ts.yourNameContainsProhibitedWords,
+ text: i18n.ts.yourNameContainsProhibitedWordsDescription,
+ },
});
});
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 646304fb06..0d68d02e35 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -30,12 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
Ads by {{ host }}
-
-
{{ i18n.ts._ad.reduceFrequencyOfThisAd }}
-
-
+
Ads by {{ host }}
+
+
{{ i18n.ts._ad.reduceFrequencyOfThisAd }}
+
@@ -123,8 +121,7 @@ function reduceFrequency(): void {
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 7c291b1072..e83af585d8 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -10,6 +10,7 @@ import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/scripts/form.js';
+import type { MenuItem } from '@/types/menu.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
@@ -22,7 +23,6 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue';
-import type { MenuItem } from '@/types/menu.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index d33b116059..948e7a3cce 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -153,6 +153,12 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.new }}
+
+ {{ i18n.ts.filter }}
+
+
+
+
@@ -254,11 +260,15 @@ const filesPagination = {
userId: props.userId,
})),
};
+
+const announcementsStatus = ref<'active' | 'archived'>('active');
+
const announcementsPagination = {
endpoint: 'admin/announcements/list' as const,
limit: 10,
params: computed(() => ({
userId: props.userId,
+ status: announcementsStatus.value,
})),
};
const expandedRoles = ref([]);
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 04d23b1358..5d8a581b2e 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -57,6 +57,18 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ {{ i18n.ts.prohibitedWordsForNameOfUser }}
+
+
+
+ {{ i18n.ts.prohibitedWordsForNameOfUserDescription }}
{{ i18n.ts.prohibitedWordsDescription2 }}
+
+ {{ i18n.ts.save }}
+
+
+
{{ i18n.ts.hiddenTags }}
@@ -131,6 +143,7 @@ const enableRegistration = ref(false);
const emailRequiredForSignup = ref(false);
const sensitiveWords = ref('');
const prohibitedWords = ref('');
+const prohibitedWordsForNameOfUser = ref('');
const hiddenTags = ref('');
const preservedUsernames = ref('');
const blockedHosts = ref('');
@@ -143,10 +156,11 @@ async function init() {
emailRequiredForSignup.value = meta.emailRequiredForSignup;
sensitiveWords.value = meta.sensitiveWords.join('\n');
prohibitedWords.value = meta.prohibitedWords.join('\n');
+ prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
blockedHosts.value = meta.blockedHosts.join('\n');
- silencedHosts.value = meta.silencedHosts.join('\n');
+ silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
}
@@ -190,6 +204,14 @@ function save_prohibitedWords() {
});
}
+function save_prohibitedWordsForNameOfUser() {
+ os.apiWithDialog('admin/update-meta', {
+ prohibitedWordsForNameOfUser: prohibitedWordsForNameOfUser.value.split('\n'),
+ }).then(() => {
+ fetchInstance(true);
+ });
+}
+
function save_hiddenTags() {
os.apiWithDialog('admin/update-meta', {
hiddenTags: hiddenTags.value.split('\n'),
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
index 7e0a932f82..12338f0bf9 100644
--- a/packages/frontend/src/pages/admin/performance.vue
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -29,6 +29,13 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ {{ i18n.ts.enableStatsForFederatedInstances }}
+ {{ i18n.ts.turnOffToImprovePerformance }}
+
+
+
{{ i18n.ts.enableChartsForFederatedInstances }}
@@ -120,6 +127,7 @@ const meta = await misskeyApi('admin/meta');
const enableServerMachineStats = ref(meta.enableServerMachineStats);
const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration);
const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser);
+const enableStatsForFederatedInstances = ref(meta.enableStatsForFederatedInstances);
const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances);
function onChange_enableServerMachineStats(value: boolean) {
@@ -146,6 +154,14 @@ function onChange_enableChartsForRemoteUser(value: boolean) {
});
}
+function onChange_enableStatsForFederatedInstances(value: boolean) {
+ os.apiWithDialog('admin/update-meta', {
+ enableStatsForFederatedInstances: value,
+ }).then(() => {
+ fetchInstance(true);
+ });
+}
+
function onChange_enableChartsForFederatedInstances(value: boolean) {
os.apiWithDialog('admin/update-meta', {
enableChartsForFederatedInstances: value,
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 410a3f53c7..4a52e59d02 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -54,6 +54,9 @@ SPDX-License-Identifier: AGPL-3.0-only
Enable condensed line
+
+ Enable note render skipping
+
@@ -105,9 +108,14 @@ const $i = signinRequired();
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine'));
+const skipNoteRender = computed(defaultStore.makeGetterSetter('skipNoteRender'));
const devMode = computed(defaultStore.makeGetterSetter('devMode'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
+watch(skipNoteRender, async () => {
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
+});
+
async function deleteAccount() {
{
const { canceled } = await os.confirm({
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 0d61f8d851..561894d2b7 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -142,13 +142,17 @@ const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.d
const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
+function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
+ return lang != null && lang in langmap;
+}
+
const profile = reactive({
name: $i.name,
description: $i.description,
followedMessage: $i.followedMessage,
location: $i.location,
birthday: $i.birthday,
- lang: $i.lang,
+ lang: assertVaildLang($i.lang) ? $i.lang : null,
isBot: $i.isBot ?? false,
isCat: $i.isCat ?? false,
});
@@ -202,6 +206,11 @@ function save() {
lang: profile.lang || null,
isBot: !!profile.isBot,
isCat: !!profile.isCat,
+ }, undefined, {
+ '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
+ title: i18n.ts.yourNameContainsProhibitedWords,
+ text: i18n.ts.yourNameContainsProhibitedWordsDescription,
+ },
});
globalEvents.emit('requestClearPageCache');
claimAchievement('profileFilled');
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 4ffa0ab94d..c1846b0589 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -245,13 +245,10 @@ export function getNoteMenu(props: {
function togglePin(pin: boolean): void {
os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
noteId: appearNote.id,
- }, undefined, null, res => {
- if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
- os.alert({
- type: 'error',
- text: i18n.ts.pinLimitExceeded,
- });
- }
+ }, undefined, {
+ '72dab508-c64d-498f-8740-a8eec1ba385a': {
+ text: i18n.ts.pinLimitExceeded,
+ },
});
}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index cb52938980..aab67e0b5c 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -468,6 +468,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: 'app' as 'app' | 'appWithShift' | 'native',
},
+ skipNoteRender: {
+ where: 'device',
+ default: true,
+ },
sound_masterVolume: {
where: 'device',
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 1e6561bdb9..4204c5af59 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -480,7 +480,11 @@ html[data-color-scheme=dark] ._woodenFrame {
transform: scale(0.9);
}
-@keyframes global-blink {
+._blink {
+ animation: blink 1s infinite;
+}
+
+@keyframes blink {
0% { opacity: 1; transform: scale(1); }
30% { opacity: 1; transform: scale(1); }
90% { opacity: 0; transform: scale(0.5); }
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 9acf7b2ede..44253e93bd 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ navbarItemDef[item].title }}
-
+
{{ navbarItemDef[item].indicateValue }}
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.settings }}
@@ -257,7 +257,6 @@ function more() {
left: 20px;
color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index cbfdaac235..8ae11efa2c 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
>
{{ navbarItemDef[item].title }}
-
+
{{ navbarItemDef[item].indicateValue }}
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.settings }}
@@ -350,7 +350,6 @@ function more(ev: MouseEvent) {
left: 20px;
color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
@@ -555,7 +554,6 @@ function more(ev: MouseEvent) {
left: 24px;
color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index a0a8601887..f4633314ae 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -142,7 +142,6 @@ onMounted(() => {
left: 0;
color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
}
&:hover {
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 4d1846c34c..5acef0bef8 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ navbarItemDef[item].title }}
-
+
{{ navbarItemDef[item].indicateValue }}
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.settings }}
@@ -222,7 +222,6 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
left: 0;
color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 36ffca8264..a1a76a7e7d 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -50,11 +50,11 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -97,6 +97,7 @@ import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue';
import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
import type { ColumnType } from './deck/deck-store.js';
+import type { MenuItem } from '@/types/menu.js';
import XSidebar from '@/ui/_common_/navbar.vue';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import MkButton from '@/components/MkButton.vue';
@@ -118,7 +119,6 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
import XDirectColumn from '@/ui/deck/direct-column.vue';
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
import { mainRouter } from '@/router/main.js';
-import type { MenuItem } from '@/types/menu.js';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
@@ -479,7 +479,6 @@ body {
left: 0;
color: var(--MI_THEME-indicator);
font-size: 16px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 9fc8bd102d..d739c2e1cd 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -96,9 +96,11 @@ SPDX-License-Identifier: AGPL-3.0-only