-
-
-
-
-
- RN: ...
-
-
-
-
-
-
-
-
-
-
+
@@ -29,43 +22,54 @@ SPDX-License-Identifier: AGPL-3.0-only
--
cgit v1.2.3-freya
From 121af778a0925d5850e9d88261e9a8e8c6fd968b Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 11 Jul 2024 18:44:18 +0900
Subject: enhance(frontend): 未使用のサウンド設定を削除 (#14116)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* enhance(frontend): 未使用のサウンド設定を削除
* Update Changelog
* Update CHANGELOG.md
---
CHANGELOG.md | 3 +++
locales/index.d.ts | 8 --------
locales/ja-JP.yml | 2 --
packages/frontend/src/pages/settings/preferences-backups.vue | 2 --
packages/frontend/src/pages/settings/sounds.vue | 2 --
packages/frontend/src/scripts/sound.ts | 2 --
packages/frontend/src/store.ts | 8 --------
7 files changed, 3 insertions(+), 24 deletions(-)
(limited to 'packages/frontend/src/pages')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 259420a6c6..cd123c938e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
## Unreleased
+### Note
+- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
+
### General
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index ebd980ed85..5089f7802e 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -7515,14 +7515,6 @@ export interface Locale extends ILocale {
* 通知
*/
"notification": string;
- /**
- * アンテナ受信
- */
- "antenna": string;
- /**
- * チャンネル通知
- */
- "channel": string;
/**
* リアクション選択時
*/
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 0d89d33abe..a03d792140 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1971,8 +1971,6 @@ _sfx:
note: "ノート"
noteMy: "ノート(自分)"
notification: "通知"
- antenna: "アンテナ受信"
- channel: "チャンネル通知"
reaction: "リアクション選択時"
_soundSettings:
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index b6f1043154..dace2cd847 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -113,8 +113,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'sound_note',
'sound_noteMy',
'sound_notification',
- 'sound_antenna',
- 'sound_channel',
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'lightTheme',
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 090f0cf14c..0f1b725fae 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -54,8 +54,6 @@ const sounds = ref
>>({
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,
reaction: defaultStore.reactiveState.sound_reaction,
});
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index fcd59510df..bba855cd64 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -74,8 +74,6 @@ export const soundsTypes = [
export const operationTypes = [
'noteMy',
'note',
- 'antenna',
- 'channel',
'notification',
'reaction',
] as const;
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index e8eb5a1ed7..9cb2742069 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -479,14 +479,6 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
},
- sound_antenna: {
- where: 'device',
- default: { type: 'syuilo/triple', volume: 1 } as SoundStore,
- },
- sound_channel: {
- where: 'device',
- default: { type: 'syuilo/square-pico', volume: 1 } as SoundStore,
- },
sound_reaction: {
where: 'device',
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
--
cgit v1.2.3-freya
From 385969e9f56a39a1e5547b94901d155e1e811263 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 12 Jul 2024 16:25:44 +0900
Subject: fix(frontend): フォーカスの挙動を修正 (#14158)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(frontend): 直前のパターンを記録するように
* fix(frontend): フォーカス/タブ移動に関する挙動を調整 (#226)
Cherry-pick commit e8c030673326871edf3623cf2b8675d68f9e1b13
Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com>
* focusのデザイン修正
* move scripts
* Modalにfocus trapを追加
* 記録するホットキーはレートリミット式にする
* escキーのハンドリングをMkModalに統一
* fix
* enterで子メニューを開けるように
* lint
* fix focus trap
* improve switch accessibility
* 一部のmodalのフォーカストラップが外れない問題を修正
* fix
* fix
* Revert "記録するホットキーはレートリミット式にする"
This reverts commit 40a7509286a87911ad4cc06d9482e8a2e5d0e7e8.
* Revert "fix(frontend): 直前のパターンを記録するように"
This reverts commit 5372b2594023952cff34aa62253ed4efef15b5dd.
* Revert "Revert "fix(frontend): 直前のパターンを記録するように""
This reverts commit a9bb52e799e110927ad92cd8f26af980819334e1.
* Revert "Revert "記録するホットキーはレートリミット式にする""
This reverts commit bdac34273e0bc5f13604c7e2f9fa6b1321a0df3d.
* 試験的にCypressでのFocustrapを無効化
* fix
* fix focus-trap
* Update Changelog
* :v:
* fix focustrap invocation logic
* スクロールがsticky headerを考慮するように
* :art:
* スタイルの微調整
* :art:
* remove deprecated key aliases
* focusElementが足りなかったので修正
* preview系にfocus時スタイルが足りなかったので修正
* `returnFocusElement` -> `returnFocusTo`
* lint
* Update packages/frontend/src/components/MkModalWindow.vue
* Apply suggestions from code review
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
* keydownイベントをまとめる
* use correct pesudo-element selector
* fix
* rename
---------
Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
CHANGELOG.md | 2 +
.../frontend/src/components/MkAchievements.vue | 4 +-
packages/frontend/src/components/MkButton.vue | 1 -
.../src/components/MkChannelFollowButton.vue | 12 +-
.../frontend/src/components/MkChannelPreview.vue | 19 +-
packages/frontend/src/components/MkClipPreview.vue | 8 +
packages/frontend/src/components/MkContextMenu.vue | 2 +-
packages/frontend/src/components/MkCwButton.vue | 4 +-
packages/frontend/src/components/MkDialog.vue | 16 +-
packages/frontend/src/components/MkDrive.file.vue | 32 +-
.../frontend/src/components/MkDrive.folder.vue | 2 +-
packages/frontend/src/components/MkEmojiPicker.vue | 33 +-
.../src/components/MkEmojiPickerDialog.vue | 2 +
.../frontend/src/components/MkFlashPreview.vue | 6 +-
packages/frontend/src/components/MkFolder.vue | 16 +-
.../frontend/src/components/MkFollowButton.vue | 12 +-
.../src/components/MkGalleryPostPreview.vue | 4 +-
.../frontend/src/components/MkImgWithBlurhash.vue | 4 +-
packages/frontend/src/components/MkLaunchPad.vue | 2 +-
packages/frontend/src/components/MkMediaAudio.vue | 26 +-
packages/frontend/src/components/MkMediaList.vue | 54 +++-
packages/frontend/src/components/MkMediaVideo.vue | 10 +-
packages/frontend/src/components/MkMenu.child.vue | 5 +-
packages/frontend/src/components/MkMenu.vue | 345 +++++++++++++--------
packages/frontend/src/components/MkModal.vue | 28 +-
packages/frontend/src/components/MkModalWindow.vue | 13 +-
packages/frontend/src/components/MkNote.vue | 48 ++-
.../frontend/src/components/MkNoteDetailed.vue | 62 ++--
packages/frontend/src/components/MkNotePreview.vue | 2 +-
.../frontend/src/components/MkNotification.vue | 2 +-
packages/frontend/src/components/MkPagePreview.vue | 19 +-
packages/frontend/src/components/MkPopupMenu.vue | 6 +-
packages/frontend/src/components/MkPostForm.vue | 10 +
.../frontend/src/components/MkPostFormDialog.vue | 2 +-
packages/frontend/src/components/MkRadio.vue | 10 +-
packages/frontend/src/components/MkSelect.vue | 27 +-
packages/frontend/src/components/MkSuperMenu.vue | 10 +-
packages/frontend/src/components/MkSwitch.vue | 10 +-
.../src/components/MkTutorialDialog.PostNote.vue | 2 +-
.../src/components/MkTutorialDialog.Sensitive.vue | 2 +-
.../src/components/MkTutorialDialog.Timeline.vue | 2 +-
.../frontend/src/components/MkVisibilityPicker.vue | 2 +-
.../src/components/global/MkStickyContainer.vue | 6 +-
packages/frontend/src/directives/hotkey.ts | 4 +-
packages/frontend/src/os.ts | 27 +-
packages/frontend/src/pages/drive.file.info.vue | 1 +
packages/frontend/src/pages/games.vue | 11 +-
packages/frontend/src/pages/page.vue | 1 +
packages/frontend/src/pages/settings/profile.vue | 1 +
packages/frontend/src/pages/settings/theme.vue | 6 +
packages/frontend/src/scripts/focus-trap.ts | 65 ++++
packages/frontend/src/scripts/focus.ts | 96 ++++--
.../frontend/src/scripts/get-dom-node-or-null.ts | 19 ++
packages/frontend/src/scripts/hotkey.ts | 51 ++-
packages/frontend/src/scripts/scroll.ts | 8 +
packages/frontend/src/style.scss | 25 +-
packages/frontend/src/ui/_common_/common.vue | 2 +-
.../frontend/src/ui/_common_/navbar-for-mobile.vue | 6 +-
packages/frontend/src/ui/_common_/navbar.vue | 86 ++++-
packages/frontend/src/ui/deck/column.vue | 4 +-
packages/frontend/src/widgets/WidgetCalendar.vue | 2 +-
61 files changed, 920 insertions(+), 379 deletions(-)
create mode 100644 packages/frontend/src/scripts/focus-trap.ts
create mode 100644 packages/frontend/src/scripts/get-dom-node-or-null.ts
(limited to 'packages/frontend/src/pages')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd123c938e..c6f48684b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@
### Client
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
- Enhance: 非ログイン時のハイライトTLのデザインを改善
+- Enhance: フロントエンドのアクセシビリティ改善
+ (Based on https://github.com/taiyme/misskey/pull/226)
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index 5d103fa789..c8134416b5 100644
--- a/packages/frontend/src/components/MkAchievements.vue
+++ b/packages/frontend/src/components/MkAchievements.vue
@@ -153,7 +153,7 @@ onMounted(() => {
background: linear-gradient(0deg, #ffee20, #eb7018);
}
- &:before {
+ &::before {
content: "";
display: block;
position: absolute;
@@ -173,7 +173,7 @@ onMounted(() => {
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
}
- &:before {
+ &::before {
content: "";
display: block;
position: absolute;
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 25b003ba5a..9560efb7d9 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -250,7 +250,6 @@ function onMousedown(evt: MouseEvent): void {
}
&:focus-visible {
- outline: solid 2px var(--focus);
outline-offset: 2px;
}
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index 841d37a568..35dc3ad4bf 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -87,17 +87,7 @@ async function onClick() {
}
&:focus-visible {
- &:after {
- content: "";
- pointer-events: none;
- position: absolute;
- top: -5px;
- right: -5px;
- bottom: -5px;
- left: -5px;
- border: 2px solid var(--focus);
- border-radius: 32px;
- }
+ outline-offset: 2px;
}
&:hover {
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 4ff64dc4ba..c30cb66c07 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ channel.name }}
@@ -80,6 +80,7 @@ const bannerStyle = computed(() => {
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index e73d032000..e2f04eb764 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -286,6 +286,7 @@ definePageMetadata(() => ({
background-color: var(--accentedBg);
color: var(--accent);
text-decoration: none;
+ outline: none;
}
}
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 60bf9b4d3d..a328933686 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -342,6 +342,7 @@ definePageMetadata(() => ({
&:hover, &:focus {
opacity: .7;
}
+
&:active {
cursor: pointer;
}
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 0a4bd4b826..7d192bcbea 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -213,12 +213,18 @@ definePageMetadata(() => ({
}
}
+ .dn:focus-visible ~ .toggle {
+ outline: 2px solid var(--focus);
+ outline-offset: 2px;
+ }
+
.toggle {
cursor: pointer;
display: inline-block;
position: relative;
width: 90px;
height: 50px;
+ margin: 4px; // focus用のアウトライン
background-color: #83D8FF;
border-radius: 90px - 6;
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/scripts/focus-trap.ts
new file mode 100644
index 0000000000..734c73652f
--- /dev/null
+++ b/packages/frontend/src/scripts/focus-trap.ts
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
+
+const focusTrapElements = new Set
();
+const ignoreElements = [
+ 'script',
+ 'style',
+];
+
+function containsFocusTrappedElements(el: HTMLElement): boolean {
+ return Array.from(focusTrapElements).some((focusTrapElement) => {
+ return el.contains(focusTrapElement);
+ });
+}
+
+function releaseFocusTrap(el: HTMLElement): void {
+ focusTrapElements.delete(el);
+ if (el.parentElement != null && el !== document.body) {
+ el.parentElement.childNodes.forEach((siblingNode) => {
+ const siblingEl = getHTMLElementOrNull(siblingNode);
+ if (!siblingEl) return;
+ if (siblingEl !== el && (focusTrapElements.has(siblingEl) || containsFocusTrappedElements(siblingEl) || focusTrapElements.size === 0)) {
+ siblingEl.inert = false;
+ } else if (
+ focusTrapElements.size > 0 &&
+ !containsFocusTrappedElements(siblingEl) &&
+ !focusTrapElements.has(siblingEl) &&
+ !ignoreElements.includes(siblingEl.tagName.toLowerCase())
+ ) {
+ siblingEl.inert = true;
+ } else {
+ siblingEl.inert = false;
+ }
+ });
+ releaseFocusTrap(el.parentElement);
+ }
+}
+
+export function focusTrap(el: HTMLElement, parent: true): void;
+export function focusTrap(el: HTMLElement, parent?: false): { release: () => void; };
+export function focusTrap(el: HTMLElement, parent = false): { release: () => void; } | void {
+ if (el.parentElement != null && el !== document.body) {
+ el.parentElement.childNodes.forEach((siblingNode) => {
+ const siblingEl = getHTMLElementOrNull(siblingNode);
+ if (!siblingEl) return;
+ if (siblingEl !== el && !ignoreElements.includes(siblingEl.tagName.toLowerCase())) {
+ siblingEl.inert = true;
+ }
+ });
+ focusTrap(el.parentElement, true);
+ }
+
+ if (!parent) {
+ focusTrapElements.add(el);
+
+ return {
+ release: () => {
+ releaseFocusTrap(el);
+ },
+ };
+ }
+}
diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts
index ea6ee61c88..eb2da5ad86 100644
--- a/packages/frontend/src/scripts/focus.ts
+++ b/packages/frontend/src/scripts/focus.ts
@@ -3,30 +3,78 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-export function focusPrev(el: Element | null, self = false, scroll = true) {
- if (el == null) return;
- if (!self) el = el.previousElementSibling;
- if (el) {
- if (el.hasAttribute('tabindex')) {
- (el as HTMLElement).focus({
- preventScroll: !scroll,
- });
- } else {
- focusPrev(el.previousElementSibling, true);
- }
+import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@/scripts/scroll.js';
+import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
+
+type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement;
+
+export const isFocusable = (input: MaybeHTMLElement | null | undefined): input is HTMLElement => {
+ if (input == null || !(input instanceof HTMLElement)) return false;
+
+ if (input.tabIndex < 0) return false;
+ if ('disabled' in input && input.disabled === true) return false;
+ if ('readonly' in input && input.readonly === true) return false;
+
+ if (!input.ownerDocument.contains(input)) return false;
+
+ const style = window.getComputedStyle(input);
+ if (style.display === 'none') return false;
+ if (style.visibility === 'hidden') return false;
+ if (style.opacity === '0') return false;
+ if (style.pointerEvents === 'none') return false;
+
+ return true;
+};
+
+export const focusPrev = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
+ const element = self ? input : getElementOrNull(input)?.previousElementSibling;
+ if (element == null) return;
+ if (isFocusable(element)) {
+ focusOrScroll(element, scroll);
+ } else {
+ focusPrev(element, false, scroll);
}
-}
-
-export function focusNext(el: Element | null, self = false, scroll = true) {
- if (el == null) return;
- if (!self) el = el.nextElementSibling;
- if (el) {
- if (el.hasAttribute('tabindex')) {
- (el as HTMLElement).focus({
- preventScroll: !scroll,
- });
- } else {
- focusPrev(el.nextElementSibling, true);
+};
+
+export const focusNext = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
+ const element = self ? input : getElementOrNull(input)?.nextElementSibling;
+ if (element == null) return;
+ if (isFocusable(element)) {
+ focusOrScroll(element, scroll);
+ } else {
+ focusNext(element, false, scroll);
+ }
+};
+
+export const focusParent = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
+ const element = self ? input : getNodeOrNull(input)?.parentElement;
+ if (element == null) return;
+ if (isFocusable(element)) {
+ focusOrScroll(element, scroll);
+ } else {
+ focusParent(element, false, scroll);
+ }
+};
+
+const focusOrScroll = (element: HTMLElement, scroll: boolean) => {
+ if (scroll) {
+ const scrollContainer = getScrollContainer(element) ?? document.documentElement;
+ const scrollContainerTop = getScrollPosition(scrollContainer);
+ const stickyTop = getStickyTop(element, scrollContainer);
+ const stickyBottom = getStickyBottom(element, scrollContainer);
+ const top = element.getBoundingClientRect().top;
+ const bottom = element.getBoundingClientRect().bottom;
+
+ let scrollTo = scrollContainerTop;
+ if (top < stickyTop) {
+ scrollTo += top - stickyTop;
+ } else if (bottom > window.innerHeight - stickyBottom) {
+ scrollTo += bottom - window.innerHeight + stickyBottom;
}
+ scrollContainer.scrollTo({ top: scrollTo, behavior: 'instant' });
+ }
+
+ if (document.activeElement !== element) {
+ element.focus({ preventScroll: true });
}
-}
+};
diff --git a/packages/frontend/src/scripts/get-dom-node-or-null.ts b/packages/frontend/src/scripts/get-dom-node-or-null.ts
new file mode 100644
index 0000000000..fbf54675fd
--- /dev/null
+++ b/packages/frontend/src/scripts/get-dom-node-or-null.ts
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const getNodeOrNull = (input: unknown): Node | null => {
+ if (input instanceof Node) return input;
+ return null;
+};
+
+export const getElementOrNull = (input: unknown): Element | null => {
+ if (input instanceof Element) return input;
+ return null;
+};
+
+export const getHTMLElementOrNull = (input: unknown): HTMLElement | null => {
+ if (input instanceof HTMLElement) return input;
+ return null;
+};
diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts
index fd79baa604..ff3cbe98ac 100644
--- a/packages/frontend/src/scripts/hotkey.ts
+++ b/packages/frontend/src/scripts/hotkey.ts
@@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { getHTMLElementOrNull } from "@/scripts/get-dom-node-or-null.js";
//#region types
export type Keymap = Record;
@@ -30,8 +31,8 @@ type Action = {
//#region consts
const KEY_ALIASES = {
'esc': 'Escape',
- 'enter': ['Enter', 'NumpadEnter'],
- 'space': [' ', 'Spacebar'],
+ 'enter': 'Enter',
+ 'space': ' ',
'up': 'ArrowUp',
'down': 'ArrowDown',
'left': 'ArrowLeft',
@@ -44,6 +45,10 @@ const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
const IGNORE_ELEMENTS = ['input', 'textarea'];
//#endregion
+//#region store
+let latestHotkey: Pattern & { callback: CallbackFunction } | null = null;
+//#endregion
+
//#region impl
export const makeHotkey = (keymap: Keymap) => {
const actions = parseKeymap(keymap);
@@ -51,13 +56,14 @@ export const makeHotkey = (keymap: Keymap) => {
if ('pswp' in window && window.pswp != null) return;
if (document.activeElement != null) {
if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return;
- if ((document.activeElement as HTMLElement).isContentEditable) return;
+ if (getHTMLElementOrNull(document.activeElement)?.isContentEditable) return;
}
- for (const { patterns, callback, options } of actions) {
- if (matchPatterns(ev, patterns, options)) {
+ for (const action of actions) {
+ if (matchPatterns(ev, action)) {
ev.preventDefault();
ev.stopPropagation();
- callback(ev);
+ action.callback(ev);
+ storePattern(ev, action.callback);
}
}
};
@@ -102,10 +108,21 @@ const parseOptions = (rawCallback: Keymap[keyof Keymap]) => {
return { ...defaultOptions } as const satisfies Action['options'];
};
-const matchPatterns = (ev: KeyboardEvent, patterns: Action['patterns'], options: Action['options']) => {
+const matchPatterns = (ev: KeyboardEvent, action: Action) => {
+ const { patterns, options, callback } = action;
if (ev.repeat && !options.allowRepeat) return false;
const key = ev.key.toLowerCase();
return patterns.some(({ which, ctrl, shift, alt }) => {
+ if (
+ latestHotkey != null &&
+ latestHotkey.which.includes(key) &&
+ latestHotkey.ctrl === ctrl &&
+ latestHotkey.alt === alt &&
+ latestHotkey.shift === shift &&
+ latestHotkey.callback === callback
+ ) {
+ return false;
+ }
if (!which.includes(key)) return false;
if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false;
if (alt !== ev.altKey) return false;
@@ -114,6 +131,26 @@ const matchPatterns = (ev: KeyboardEvent, patterns: Action['patterns'], options:
});
};
+let lastHotKeyStoreTimer: number | null = null;
+
+const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => {
+ if (lastHotKeyStoreTimer != null) {
+ clearTimeout(lastHotKeyStoreTimer);
+ }
+
+ latestHotkey = {
+ which: [ev.key.toLowerCase()],
+ ctrl: ev.ctrlKey || ev.metaKey,
+ alt: ev.altKey,
+ shift: ev.shiftKey,
+ callback,
+ };
+
+ lastHotKeyStoreTimer = window.setTimeout(() => {
+ latestHotkey = null;
+ }, 500);
+};
+
const parseKeyCode = (input?: string | null) => {
if (input == null) return [];
const raw = getValueByKey(KEY_ALIASES, input);
diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts
index 8edb6fca05..f0274034b5 100644
--- a/packages/frontend/src/scripts/scroll.ts
+++ b/packages/frontend/src/scripts/scroll.ts
@@ -23,6 +23,14 @@ export function getStickyTop(el: HTMLElement, container: HTMLElement | null = nu
return getStickyTop(el.parentElement, container, newTop);
}
+export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) {
+ if (!el.parentElement) return bottom;
+ const data = el.dataset.stickyContainerFooterHeight;
+ const newBottom = data ? Number(data) + bottom : bottom;
+ if (el === container) return newBottom;
+ return getStickyBottom(el.parentElement, container, newBottom);
+}
+
export function getScrollPosition(el: HTMLElement | null): number {
const container = getScrollContainer(el);
return container == null ? window.scrollY : container.scrollTop;
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 250a2616a7..2feb79ef81 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -113,6 +113,10 @@ a {
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
+ &:focus-visible {
+ outline-offset: 2px;
+ }
+
&:hover {
text-decoration: underline;
}
@@ -143,12 +147,21 @@ rt {
white-space: initial;
}
+:focus-visible {
+ outline: var(--focus) solid 2px;
+ outline-offset: -2px;
+
+ &:hover {
+ text-decoration: none;
+ }
+}
+
.ti {
width: 1.28em;
vertical-align: -12%;
line-height: 1em;
- &:before {
+ &::before {
font-size: 128%;
}
}
@@ -230,10 +243,6 @@ rt {
line-height: inherit;
max-width: 100%;
- &:focus-visible {
- outline: none;
- }
-
&:disabled {
opacity: 0.5;
cursor: default;
@@ -270,13 +279,17 @@ rt {
._help {
color: var(--accent);
- cursor: help
+ cursor: help;
}
._textButton {
@extend ._button;
color: var(--accent);
+ &:focus-visible {
+ outline-offset: 2px;
+ }
+
&:not(:disabled):hover {
text-decoration: underline;
}
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 822b552837..d7df2d10f9 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -227,7 +227,7 @@ if ($i) {
right: 15px;
pointer-events: none;
- &:before {
+ &::before {
content: "";
display: block;
width: 18px;
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 699aa1e1c8..87e9e45e63 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -139,7 +139,7 @@ function more() {
font-weight: bold;
text-align: left;
- &:before {
+ &::before {
content: "";
display: block;
width: calc(100% - 38px);
@@ -155,7 +155,7 @@ function more() {
}
&:hover, &.active {
- &:before {
+ &::before {
background: var(--accentLighten);
}
}
@@ -226,7 +226,7 @@ function more() {
}
&:hover, &.active {
- &:before {
+ &::before {
content: "";
display: block;
width: calc(100% - 24px);
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index b029533f28..8307da0d42 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -166,6 +166,15 @@ function more(ev: MouseEvent) {
display: block;
text-align: center;
width: 100%;
+
+ &:focus-visible {
+ outline: none;
+
+ > .instanceIcon {
+ outline: 2px solid var(--focus);
+ outline-offset: 2px;
+ }
+ }
}
.instanceIcon {
@@ -192,7 +201,7 @@ function more(ev: MouseEvent) {
font-weight: bold;
text-align: left;
- &:before {
+ &::before {
content: "";
display: block;
width: calc(100% - 38px);
@@ -207,8 +216,17 @@ function more(ev: MouseEvent) {
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
}
+ &:focus-visible {
+ outline: none;
+
+ &::before {
+ outline: 2px solid var(--fgOnAccent);
+ outline-offset: -4px;
+ }
+ }
+
&:hover, &.active {
- &:before {
+ &::before {
background: var(--accentLighten);
}
}
@@ -234,6 +252,14 @@ function more(ev: MouseEvent) {
text-align: left;
box-sizing: border-box;
overflow: clip;
+
+ &:focus-visible {
+ outline: none;
+
+ > .avatar {
+ box-shadow: 0 0 0 4px var(--focus);
+ }
+ }
}
.avatar {
@@ -282,10 +308,19 @@ function more(ev: MouseEvent) {
color: var(--navActive);
}
- &:hover, &.active {
+ &:focus-visible {
+ outline: none;
+
+ &::before {
+ outline: 2px solid var(--focus);
+ outline-offset: -2px;
+ }
+ }
+
+ &:hover, &.active, &:focus {
color: var(--accent);
- &:before {
+ &::before {
content: "";
display: block;
width: calc(100% - 34px);
@@ -352,6 +387,15 @@ function more(ev: MouseEvent) {
display: block;
text-align: center;
width: 100%;
+
+ &:focus-visible {
+ outline: none;
+
+ > .instanceIcon {
+ outline: 2px solid var(--focus);
+ outline-offset: 2px;
+ }
+ }
}
.instanceIcon {
@@ -376,7 +420,7 @@ function more(ev: MouseEvent) {
height: 52px;
text-align: center;
- &:before {
+ &::before {
content: "";
display: block;
position: absolute;
@@ -391,8 +435,17 @@ function more(ev: MouseEvent) {
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
}
+ &:focus-visible {
+ outline: none;
+
+ &::before {
+ outline: 2px solid var(--fgOnAccent);
+ outline-offset: -4px;
+ }
+ }
+
&:hover, &.active {
- &:before {
+ &::before {
background: var(--accentLighten);
}
}
@@ -413,6 +466,14 @@ function more(ev: MouseEvent) {
padding: 20px 0;
width: 100%;
overflow: clip;
+
+ &:focus-visible {
+ outline: none;
+
+ > .avatar {
+ box-shadow: 0 0 0 4px var(--focus);
+ }
+ }
}
.avatar {
@@ -442,11 +503,20 @@ function more(ev: MouseEvent) {
width: 100%;
text-align: center;
- &:hover, &.active {
+ &:focus-visible {
+ outline: none;
+
+ &::before {
+ outline: 2px solid var(--focus);
+ outline-offset: -2px;
+ }
+ }
+
+ &:hover, &.active, &:focus {
text-decoration: none;
color: var(--accent);
- &:before {
+ &::before {
content: "";
display: block;
height: 100%;
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 07845bacbb..e96402d13b 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -271,7 +271,7 @@ function onDrop(ev) {
border-radius: 10px;
&.draghover {
- &:after {
+ &::after {
content: "";
display: block;
position: absolute;
@@ -285,7 +285,7 @@ function onDrop(ev) {
}
&.dragging {
- &:after {
+ &::after {
content: "";
display: block;
position: absolute;
diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue
index c688e8a0b1..6ece33eff3 100644
--- a/packages/frontend/src/widgets/WidgetCalendar.vue
+++ b/packages/frontend/src/widgets/WidgetCalendar.vue
@@ -121,7 +121,7 @@ defineExpose({
.root {
padding: 16px 0;
- &:after {
+ &::after {
content: "";
display: block;
clear: both;
--
cgit v1.2.3-freya
From 7afa593d114335368d9031b6e1d5b1edbc4ea9c9 Mon Sep 17 00:00:00 2001
From: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
Date: Sun, 14 Jul 2024 09:31:05 +0900
Subject: Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
(#14078)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: implement role policy "canUpdateBioMedia"
* docs(changelog): update changelog
* docs(changelog): update changelog
* chore: regenerate misskey-js type definitions
* chore: Apply suggestion from code review
Co-authored-by: anatawa12
* chore: fix unnecessarily strict inequality check
* chore: policies should be gotten only once
---------
Co-authored-by: anatawa12
---
CHANGELOG.md | 2 ++
locales/index.d.ts | 4 ++++
locales/ja-JP.yml | 1 +
packages/backend/src/core/RoleService.ts | 3 +++
.../src/core/activitypub/models/ApPersonService.ts | 8 ++++++++
packages/backend/src/models/json-schema/role.ts | 4 ++++
.../backend/src/server/api/endpoints/i/update.ts | 23 ++++++++++++++++------
packages/frontend/src/const.ts | 1 +
packages/frontend/src/pages/admin/roles.editor.vue | 20 +++++++++++++++++++
packages/frontend/src/pages/admin/roles.vue | 8 ++++++++
packages/misskey-js/src/autogen/types.ts | 1 +
11 files changed, 69 insertions(+), 6 deletions(-)
(limited to 'packages/frontend/src/pages')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd2a9e32e5..bcc2aa29c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@
### General
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
+- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
+ - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 5089f7802e..c2f8e944dd 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -6594,6 +6594,10 @@ export interface Locale extends ILocale {
* ファイルにNSFWを常に付与
*/
"alwaysMarkNsfw": string;
+ /**
+ * アイコンとバナーの更新を許可
+ */
+ "canUpdateBioMedia": string;
/**
* ノートのピン留めの最大数
*/
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a03d792140..8d117e6dc8 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1705,6 +1705,7 @@ _role:
canManageAvatarDecorations: "アバターデコレーションの管理"
driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
+ canUpdateBioMedia: "アイコンとバナーの更新を許可"
pinMax: "ノートのピン留めの最大数"
antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数"
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index e2ebecb99f..94026fd503 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -47,6 +47,7 @@ export type RolePolicies = {
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
+ canUpdateBioMedia: boolean;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
@@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canHideAds: false,
driveCapacityMb: 100,
alwaysMarkNsfw: false,
+ canUpdateBioMedia: true,
pinLimit: 5,
antennaLimit: 5,
wordMuteLimit: 200,
@@ -376,6 +378,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
+ canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 398c8695d2..457205e023 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -34,6 +34,7 @@ import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js';
@@ -100,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
+
+ private roleService: RoleService,
) {
}
@@ -238,6 +241,11 @@ export class ApPersonService implements OnModuleInit {
return this.apImageService.resolveImage(user, img).catch(() => null);
}));
+ if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null))
+ && !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) {
+ return {};
+ }
+
/*
we don't want to return nulls on errors! if the database fields
are already null, nothing changes; if the database has old
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index d9987a70c3..7366f05356 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ canUpdateBioMedia: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
pinLimit: {
type: 'integer',
optional: false, nullable: false,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index b39b52bc41..a1e2fa5e4c 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js';
-import { RoleService } from '@/core/RoleService.js';
+import { RolePolicies, RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
@@ -256,6 +256,7 @@ export default class extends Endpoint { // eslint-
const profileUpdates = {} as Partial;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+ let policies: RolePolicies | null = null;
if (ps.name !== undefined) {
if (ps.name === null) {
@@ -296,14 +297,16 @@ export default class extends Endpoint { // eslint-
}
if (ps.mutedWords !== undefined) {
- checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ checkMuteWordCount(ps.mutedWords, policies.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);
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit);
validateMuteWordRegex(ps.hardMutedWords);
profileUpdates.hardMutedWords = ps.hardMutedWords;
}
@@ -322,13 +325,17 @@ export default class extends Endpoint { // eslint-
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === 'boolean') {
- if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
}
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
if (ps.avatarId) {
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
+
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
@@ -344,6 +351,9 @@ export default class extends Endpoint { // eslint-
}
if (ps.bannerId) {
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
+
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
@@ -359,14 +369,15 @@ export default class extends Endpoint { // eslint-
}
if (ps.avatarDecorations) {
+ policies ??= await this.roleService.getUserPolicies(user.id);
const decorations = await this.avatarDecorationService.getAll(true);
- const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
+ const myRoles = await this.roleService.getUserRoles(user.id);
const allRoles = await this.roleService.getRoles();
const decorationIds = decorations
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
.map(d => d.id);
- if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
+ if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
id: d.id,
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index 9e41926a97..e135bc69a0 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend/src/const.ts
@@ -87,6 +87,7 @@ export const ROLE_POLICIES = [
'canHideAds',
'driveCapacityMb',
'alwaysMarkNsfw',
+ 'canUpdateBioMedia',
'pinLimit',
'antennaLimit',
'wordMuteLimit',
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index eb8a59b34f..3e948abdf1 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -378,6 +378,26 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts._role._options.canUpdateBioMedia }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.canUpdateBioMedia.value ? i18n.ts.yes : i18n.ts.no }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+ {{ i18n.ts.enable }}
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
{{ i18n.ts._role._options.pinMax }}
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 50323e3de5..6fb950494b 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -134,6 +134,14 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts._role._options.canUpdateBioMedia }}
+ {{ policies.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}
+
+ {{ i18n.ts.enable }}
+
+
+
{{ i18n.ts._role._options.pinMax }}
{{ policies.pinLimit }}
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index ff731a2fa6..d3c857219b 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4786,6 +4786,7 @@ export type components = {
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
+ canUpdateBioMedia: boolean;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
--
cgit v1.2.3-freya
From 6dd6fcf88f621d787c6524d81844b1d514434859 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sun, 14 Jul 2024 14:49:50 +0900
Subject: enhance(frontend): サーバー情報・お問い合わせページを改修 (#14198)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* improve(frontend): サーバー情報・お問い合わせページを改修 (#238)
* Revert "Revert "enhance(frontend): add contact page" (#208)" (This reverts commit 5a329a09c987b3249f97f9d53af67d1bffb09eea.)
* improve(frontend): サーバー情報・お問い合わせページを改修
(cherry picked from commit e72758d8cda3db009c5d1bf1f4141682931b91f8)
* fix
* Update Changelog
* tweak
* lint
* 既存の翻訳を使用するように
---------
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
---
CHANGELOG.md | 2 +
packages/frontend/src/components/MkMenu.vue | 1 +
.../frontend/src/components/MkVisitorDashboard.vue | 11 +-
packages/frontend/src/pages/about.overview.vue | 205 +++++++++++++++++++++
packages/frontend/src/pages/about.vue | 201 +-------------------
packages/frontend/src/pages/contact.vue | 26 ++-
packages/frontend/src/ui/_common_/common.ts | 24 +--
7 files changed, 250 insertions(+), 220 deletions(-)
create mode 100644 packages/frontend/src/pages/about.overview.vue
(limited to 'packages/frontend/src/pages')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bcc2aa29c6..6e411e5329 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,8 @@
- Enhance: 非ログイン時のハイライトTLのデザインを改善
- Enhance: フロントエンドのアクセシビリティ改善
(Based on https://github.com/taiyme/misskey/pull/226)
+- Enhance: サーバー情報ページ・お問い合わせページを改善
+ (Cherry-picked from https://github.com/taiyme/misskey/pull/238)
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 68479989b2..2276da1d21 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="['_button', $style.item]"
:href="item.href"
:target="item.target"
+ :rel="item.target === '_blank' ? 'noopener noreferrer' : undefined"
:download="item.download"
@click.passive="close(true)"
@mouseenter.passive="onItemMouseEnter"
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 4d81bd0283..445780eca7 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.joinThisServer }}
- {{ i18n.ts.exploreOtherServers }}
+ {{ i18n.ts.exploreOtherServers }}
{{ i18n.ts.login }}
@@ -65,7 +65,8 @@ import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import MkNumber from '@/components/MkNumber.vue';
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
-import { openInstanceMenu } from '@/ui/_common_/common';
+import { openInstanceMenu } from '@/ui/_common_/common.js';
+import type { MenuItem } from '@/types/menu.js';
const stats = ref