Merge branch 'enh-14784' of https://github.com/kakkokari-gtyih/misskey into enh-14784

This commit is contained in:
kakkokari-gtyih 2024-10-20 19:30:33 +09:00
commit ff0f0b071f
20 changed files with 218 additions and 153 deletions

View File

@ -4,7 +4,11 @@
- -
### Client ### Client
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
- Enhance: ドライブでソートができるように
- Enhance: ノートの公開範囲に応じて色分けできるように - Enhance: ノートの公開範囲に応じて色分けできるように
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
### Server ### Server
- -

6
locales/index.d.ts vendored
View File

@ -9279,7 +9279,7 @@ export interface Locale extends ILocale {
*/ */
"youGotQuote": ParameterizedString<"name">; "youGotQuote": ParameterizedString<"name">;
/** /**
* {name}Renoteしまし * {name}
*/ */
"youRenoted": ParameterizedString<"name">; "youRenoted": ParameterizedString<"name">;
/** /**
@ -9384,7 +9384,7 @@ export interface Locale extends ILocale {
*/ */
"reply": string; "reply": string;
/** /**
* Renote *
*/ */
"renote": string; "renote": string;
/** /**
@ -9442,7 +9442,7 @@ export interface Locale extends ILocale {
*/ */
"reply": string; "reply": string;
/** /**
* Renote *
*/ */
"renote": string; "renote": string;
}; };

View File

@ -2450,7 +2450,7 @@ _notification:
youGotMention: "{name}からのメンション" youGotMention: "{name}からのメンション"
youGotReply: "{name}からのリプライ" youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用" youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました" youRenoted: "{name}がリノートしました"
youWereFollowed: "フォローされました" youWereFollowed: "フォローされました"
youReceivedFollowRequest: "フォローリクエストが来ました" youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
@ -2478,7 +2478,7 @@ _notification:
follow: "フォロー" follow: "フォロー"
mention: "メンション" mention: "メンション"
reply: "リプライ" reply: "リプライ"
renote: "Renote" renote: "リノート"
quote: "引用" quote: "引用"
reaction: "リアクション" reaction: "リアクション"
pollEnded: "アンケートが終了" pollEnded: "アンケートが終了"
@ -2494,7 +2494,7 @@ _notification:
_actions: _actions:
followBack: "フォローバック" followBack: "フォローバック"
reply: "返信" reply: "返信"
renote: "Renote" renote: "リノート"
_deck: _deck:
alwaysShowMainColumn: "常にメインカラムを表示" alwaysShowMainColumn: "常にメインカラムを表示"

View File

@ -30,6 +30,7 @@ import type {
EndedPollNotificationQueue, EndedPollNotificationQueue,
InboxQueue, InboxQueue,
ObjectStorageQueue, ObjectStorageQueue,
RelationshipQueue,
SystemQueue, SystemQueue,
UserWebhookDeliverQueue, UserWebhookDeliverQueue,
SystemWebhookDeliverQueue, SystemWebhookDeliverQueue,
@ -121,6 +122,7 @@ export class ClientServerService {
@Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
@ -248,6 +250,7 @@ export class ClientServerService {
this.deliverQueue, this.deliverQueue,
this.inboxQueue, this.inboxQueue,
this.dbQueue, this.dbQueue,
this.relationshipQueue,
this.objectStorageQueue, this.objectStorageQueue,
this.userWebhookDeliverQueue, this.userWebhookDeliverQueue,
this.systemWebhookDeliverQueue, this.systemWebhookDeliverQueue,

View File

@ -64,26 +64,30 @@ const showBody = ref(props.expanded);
const ignoreOmit = ref(false); const ignoreOmit = ref(false);
const omitted = ref(false); const omitted = ref(false);
function enter(el) { function enter(el: Element) {
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0; el.style.height = '0';
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
} }
function afterEnter(el) { function afterEnter(el: Element) {
el.style.height = null; if (!(el instanceof HTMLElement)) return;
el.style.height = '';
} }
function leave(el) { function leave(el: Element) {
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px'; el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = 0; el.style.height = '0';
} }
function afterLeave(el) { function afterLeave(el: Element) {
el.style.height = null; if (!(el instanceof HTMLElement)) return;
el.style.height = '';
} }
const calcOmit = () => { const calcOmit = () => {

View File

@ -128,14 +128,14 @@ export default defineComponent({
return children; return children;
}; };
function onBeforeLeave(element: Element) { function onBeforeLeave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.top = `${el.offsetTop}px`; el.style.top = `${el.offsetTop}px`;
el.style.left = `${el.offsetLeft}px`; el.style.left = `${el.offsetLeft}px`;
} }
function onLeaveCancelled(element: Element) { function onLeaveCancelled(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.top = ''; el.style.top = '';
el.style.left = ''; el.style.left = '';
} }

View File

@ -157,7 +157,12 @@ const ilFilesObserver = new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(), (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
); );
const sortModeSelect = ref('+createdAt');
watch(folder, () => emit('cd', folder.value)); watch(folder, () => emit('cd', folder.value));
watch(sortModeSelect, () => {
fetch();
});
function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) { function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
addFile(file, true); addFile(file, true);
@ -558,6 +563,7 @@ async function fetch() {
folderId: folder.value ? folder.value.id : null, folderId: folder.value ? folder.value.id : null,
type: props.type, type: props.type,
limit: filesMax + 1, limit: filesMax + 1,
sort: sortModeSelect.value,
}).then(fetchedFiles => { }).then(fetchedFiles => {
if (fetchedFiles.length === filesMax + 1) { if (fetchedFiles.length === filesMax + 1) {
moreFiles.value = true; moreFiles.value = true;
@ -607,6 +613,7 @@ function fetchMoreFiles() {
type: props.type, type: props.type,
untilId: files.value.at(-1)?.id, untilId: files.value.at(-1)?.id,
limit: max + 1, limit: max + 1,
sort: sortModeSelect.value,
}).then(files => { }).then(files => {
if (files.length === max + 1) { if (files.length === max + 1) {
moreFiles.value = true; moreFiles.value = true;
@ -642,6 +649,43 @@ function getMenu() {
type: 'label', type: 'label',
}); });
menu.push({
type: 'parent',
text: i18n.ts.sort,
icon: 'ti ti-arrows-sort',
children: [{
text: `${i18n.ts.registeredDate} (${i18n.ts.descendingOrder})`,
icon: 'ti ti-sort-descending-letters',
action: () => { sortModeSelect.value = '+createdAt'; },
active: sortModeSelect.value === '+createdAt',
}, {
text: `${i18n.ts.registeredDate} (${i18n.ts.ascendingOrder})`,
icon: 'ti ti-sort-ascending-letters',
action: () => { sortModeSelect.value = '-createdAt'; },
active: sortModeSelect.value === '-createdAt',
}, {
text: `${i18n.ts.size} (${i18n.ts.descendingOrder})`,
icon: 'ti ti-sort-descending-letters',
action: () => { sortModeSelect.value = '+size'; },
active: sortModeSelect.value === '+size',
}, {
text: `${i18n.ts.size} (${i18n.ts.ascendingOrder})`,
icon: 'ti ti-sort-ascending-letters',
action: () => { sortModeSelect.value = '-size'; },
active: sortModeSelect.value === '-size',
}, {
text: `${i18n.ts.name} (${i18n.ts.descendingOrder})`,
icon: 'ti ti-sort-descending-letters',
action: () => { sortModeSelect.value = '+name'; },
active: sortModeSelect.value === '+name',
}, {
text: `${i18n.ts.name} (${i18n.ts.ascendingOrder})`,
icon: 'ti ti-sort-ascending-letters',
action: () => { sortModeSelect.value = '-name'; },
active: sortModeSelect.value === '-name',
}],
});
if (folder.value) { if (folder.value) {
menu.push({ menu.push({
text: i18n.ts.renameFolder, text: i18n.ts.renameFolder,

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div ref="rootEl" :class="$style.root"> <div ref="rootEl" :class="$style.root">
<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody"> <header :class="$style.header" class="_button" @click="showBody = !showBody">
<div :class="$style.title"><div><slot name="header"></slot></div></div> <div :class="$style.title"><div><slot name="header"></slot></div></div>
<div :class="$style.divider"></div> <div :class="$style.divider"></div>
<button class="_button" :class="$style.button"> <button class="_button" :class="$style.button">
@ -32,21 +32,23 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, shallowRef, watch } from 'vue'; import { onMounted, ref, shallowRef, watch } from 'vue';
import tinycolor from 'tinycolor2';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { getBgColor } from '@/scripts/get-bg-color.js';
const miLocalStoragePrefix = 'ui:folder:' as const; const miLocalStoragePrefix = 'ui:folder:' as const;
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
expanded?: boolean; expanded?: boolean;
persistKey?: string; persistKey?: string | null;
}>(), { }>(), {
expanded: true, expanded: true,
persistKey: null,
}); });
const rootEl = shallowRef<HTMLDivElement>(); const rootEl = shallowRef<HTMLElement>();
const bg = ref<string>(); const parentBg = ref<string | null>(null);
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded); const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
watch(showBody, () => { watch(showBody, () => {
@ -55,47 +57,34 @@ watch(showBody, () => {
} }
}); });
function enter(element: Element) { function enter(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = '0'; el.style.height = '0';
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = elementHeight + 'px'; el.style.height = `${elementHeight}px`;
} }
function afterEnter(element: Element) { function afterEnter(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.height = 'unset'; el.style.height = '';
} }
function leave(element: Element) { function leave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px'; el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = '0'; el.style.height = '0';
} }
function afterLeave(element: Element) { function afterLeave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.height = 'unset'; el.style.height = '';
} }
onMounted(() => { onMounted(() => {
function getParentBg(el?: HTMLElement | null): string { parentBg.value = getBgColor(rootEl.value?.parentElement);
if (el == null || el.tagName === 'BODY') return 'var(--MI_THEME-bg)';
const background = el.style.background || el.style.backgroundColor;
if (background) {
return background;
} else {
return getParentBg(el.parentElement);
}
}
const rawBg = getParentBg(rootEl.value);
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
_bg.setAlpha(0.85);
bg.value = _bg.toRgbString();
}); });
</script> </script>
@ -121,6 +110,7 @@ onMounted(() => {
top: var(--MI-stickyTop, 0px); top: var(--MI-stickyTop, 0px);
-webkit-backdrop-filter: var(--MI-blur, blur(8px)); -webkit-backdrop-filter: var(--MI-blur, blur(8px));
backdrop-filter: var(--MI-blur, blur(20px)); backdrop-filter: var(--MI-blur, blur(20px));
background-color: color(from v-bind("parentBg ?? 'var(--bg)'") srgb r g b / 0.85);
} }
.title { .title {

View File

@ -56,8 +56,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, shallowRef, ref } from 'vue'; import { nextTick, onMounted, ref, shallowRef } from 'vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { getBgColor } from '@/scripts/get-bg-color.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
defaultOpen?: boolean; defaultOpen?: boolean;
@ -69,40 +70,35 @@ const props = withDefaults(defineProps<{
withSpacer: true, withSpacer: true,
}); });
const getBgColor = (el: HTMLElement) => {
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
};
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const bgSame = ref(false); const bgSame = ref(false);
const opened = ref(props.defaultOpen); const opened = ref(props.defaultOpen);
const openedAtLeastOnce = ref(props.defaultOpen); const openedAtLeastOnce = ref(props.defaultOpen);
function enter(el) { function enter(el: Element) {
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0; el.style.height = '0';
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
} }
function afterEnter(el) { function afterEnter(el: Element) {
el.style.height = null; if (!(el instanceof HTMLElement)) return;
el.style.height = '';
} }
function leave(el) { function leave(el: Element) {
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px'; el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = 0; el.style.height = '0';
} }
function afterLeave(el) { function afterLeave(el: Element) {
el.style.height = null; if (!(el instanceof HTMLElement)) return;
el.style.height = '';
} }
function toggle() { function toggle() {
@ -117,7 +113,7 @@ function toggle() {
onMounted(() => { onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);
const parentBg = getBgColor(rootEl.value!.parentElement!); const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel'); const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
bgSame.value = parentBg === myBg; bgSame.value = parentBg === myBg;
}); });

View File

@ -53,7 +53,7 @@ export type Tab = {
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, watch, nextTick, shallowRef } from 'vue'; import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -120,14 +120,14 @@ function onTabWheel(ev: WheelEvent) {
let entering = false; let entering = false;
async function enter(element: Element) { async function enter(el: Element) {
if (!(el instanceof HTMLElement)) return;
entering = true; entering = true;
const el = element as HTMLElement;
const elementWidth = el.getBoundingClientRect().width; const elementWidth = el.getBoundingClientRect().width;
el.style.width = '0'; el.style.width = '0';
el.style.paddingLeft = '0'; el.style.paddingLeft = '0';
el.offsetWidth; // force reflow el.offsetWidth; // reflow
el.style.width = elementWidth + 'px'; el.style.width = `${elementWidth}px`;
el.style.paddingLeft = ''; el.style.paddingLeft = '';
nextTick(() => { nextTick(() => {
entering = false; entering = false;
@ -136,22 +136,23 @@ async function enter(element: Element) {
setTimeout(renderTab, 170); setTimeout(renderTab, 170);
} }
function afterEnter(element: Element) { function afterEnter(el: Element) {
//el.style.width = ''; if (!(el instanceof HTMLElement)) return;
// element.style.width = '';
} }
async function leave(element: Element) { async function leave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
const elementWidth = el.getBoundingClientRect().width; const elementWidth = el.getBoundingClientRect().width;
el.style.width = elementWidth + 'px'; el.style.width = `${elementWidth}px`;
el.style.paddingLeft = ''; el.style.paddingLeft = '';
el.offsetWidth; // force reflow el.offsetWidth; // reflow
el.style.width = '0'; el.style.width = '0';
el.style.paddingLeft = '0'; el.style.paddingLeft = '0';
} }
function afterLeave(element: Element) { function afterLeave(el: Element) {
const el = element as HTMLElement; if (!(el instanceof HTMLElement)) return;
el.style.width = ''; el.style.width = '';
} }

View File

@ -4,19 +4,11 @@
*/ */
import { Directive } from 'vue'; import { Directive } from 'vue';
import { getBgColor } from '@/scripts/get-bg-color.js';
export default { export default {
mounted(src, binding, vn) { mounted(src, binding, vn) {
const getBgColor = (el: HTMLElement) => { const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
};
const parentBg = getBgColor(src.parentElement);
const myBg = window.getComputedStyle(src).backgroundColor; const myBg = window.getComputedStyle(src).backgroundColor;

View File

@ -4,19 +4,11 @@
*/ */
import { Directive } from 'vue'; import { Directive } from 'vue';
import { getBgColor } from '@/scripts/get-bg-color.js';
export default { export default {
mounted(src, binding, vn) { mounted(src, binding, vn) {
const getBgColor = (el: HTMLElement) => { const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
};
const parentBg = getBgColor(src.parentElement);
const myBg = window.getComputedStyle(src).backgroundColor; const myBg = window.getComputedStyle(src).backgroundColor;

View File

@ -4,19 +4,11 @@
*/ */
import { Directive } from 'vue'; import { Directive } from 'vue';
import { getBgColor } from '@/scripts/get-bg-color.js';
export default { export default {
mounted(src, binding, vn) { mounted(src, binding, vn) {
const getBgColor = (el: HTMLElement) => { const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
};
const parentBg = getBgColor(src.parentElement);
const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'); const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel');

View File

@ -5,10 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header> <template #header><MkPageHeader/></template>
<MkPageHeader/> <MkSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
</template>
<MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
<div :class="$style.root"> <div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<div :class="$style.text"> <div :class="$style.text">
@ -16,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.nothing }} {{ i18n.ts.nothing }}
</div> </div>
</div> </div>
</MKSpacer> </MkSpacer>
<MkSpacer v-else :contentMax="800"> <MkSpacer v-else :contentMax="800">
<div class="_gaps_m" style="text-align: center;"> <div class="_gaps_m" style="text-align: center;">
<div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div> <div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div>

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <MkSpacer v-if="error != null" :contentMax="1200">
<div :class="$style.root"> <div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<p :class="$style.text"> <p :class="$style.text">
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.nothing }} {{ i18n.ts.nothing }}
</p> </p>
</div> </div>
</MKSpacer> </MkSpacer>
<MkSpacer v-else-if="list" :contentMax="700" :class="$style.main"> <MkSpacer v-else-if="list" :contentMax="700" :class="$style.main">
<div v-if="list" class="members _margin"> <div v-if="list" class="members _margin">
<div :class="$style.member_text">{{ i18n.ts.members }}</div> <div :class="$style.member_text">{{ i18n.ts.members }}</div>
@ -50,7 +50,7 @@ const props = defineProps<{
}>(); }>();
const list = ref<Misskey.entities.UserList | null>(null); const list = ref<Misskey.entities.UserList | null>(null);
const error = ref(); const error = ref<unknown | null>(null);
const users = ref<Misskey.entities.UserDetailed[]>([]); const users = ref<Misskey.entities.UserDetailed[]>([]);
function fetchList(): void { function fetchList(): void {

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template>
<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <MkSpacer v-if="error != null" :contentMax="1200">
<div :class="$style.root"> <div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<p :class="$style.text"> <p :class="$style.text">
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ error }} {{ error }}
</p> </p>
</div> </div>
</MKSpacer> </MkSpacer>
<MkSpacer v-else-if="tab === 'users'" :contentMax="1200"> <MkSpacer v-else-if="tab === 'users'" :contentMax="1200">
<div class="_gaps_s"> <div class="_gaps_s">
<div v-if="role">{{ role.description }}</div> <div v-if="role">{{ role.description }}</div>
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkSpacer> </MkSpacer>
<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700"> <MkSpacer v-else-if="tab === 'timeline'" :contentMax="700">
<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/> <MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/>
<div v-else-if="!visible" class="_fullinfo"> <div v-else-if="!visible" class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div> <div>{{ i18n.ts.nothing }}</div>
@ -47,23 +47,24 @@ import { instanceName } from '@@/js/config.js';
import { serverErrorImageUrl, infoImageUrl } from '@/instance.js'; import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
role: string; roleId: string;
initialTab?: string; initialTab?: string;
}>(), { }>(), {
initialTab: 'users', initialTab: 'users',
}); });
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const tab = ref(props.initialTab); const tab = ref(props.initialTab);
const role = ref<Misskey.entities.Role>(); const role = ref<Misskey.entities.Role | null>(null);
const error = ref(); const error = ref<string | null>(null);
const visible = ref(false); const visible = ref(false);
watch(() => props.role, () => { watch(() => props.roleId, () => {
misskeyApi('roles/show', { misskeyApi('roles/show', {
roleId: props.role, roleId: props.roleId,
}).then(res => { }).then(res => {
role.value = res; role.value = res;
document.title = `${role.value.name} | ${instanceName}`; error.value = null;
visible.value = res.isExplorable && res.isPublic; visible.value = res.isExplorable && res.isPublic;
}).catch((err) => { }).catch((err) => {
if (err.code === 'NO_SUCH_ROLE') { if (err.code === 'NO_SUCH_ROLE') {
@ -71,7 +72,6 @@ watch(() => props.role, () => {
} else { } else {
error.value = i18n.ts.somethingHappened; error.value = i18n.ts.somethingHappened;
} }
document.title = `${error.value} | ${instanceName}`;
}); });
}, { immediate: true }); }, { immediate: true });
@ -79,7 +79,7 @@ const users = computed(() => ({
endpoint: 'roles/users' as const, endpoint: 'roles/users' as const,
limit: 30, limit: 30,
params: { params: {
roleId: props.role, roleId: props.roleId,
}, },
})); }));
@ -94,7 +94,7 @@ const headerTabs = computed(() => [{
}]); }]);
definePageMetadata(() => ({ definePageMetadata(() => ({
title: role.value ? role.value.name : i18n.ts.role, title: role.value ? role.value.name : (error.value ?? i18n.ts.role),
icon: 'ti ti-badge', icon: 'ti ti-badge',
})); }));
</script> </script>

View File

@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="_gaps_m"> <div class="_gaps_m">
<MkSelect v-model="type"> <MkSelect v-model="type">
<option value="all">{{ i18n.ts.all }}</option> <option v-for="type in props.configurableTypes ?? notificationConfigTypes" :key="type" :value="type">{{ notificationConfigTypesI18nMap[type] }}</option>
<option value="following">{{ i18n.ts.following }}</option>
<option value="follower">{{ i18n.ts.followers }}</option>
<option value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
<option value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option>
<option value="list">{{ i18n.ts.userList }}</option>
<option value="never">{{ i18n.ts.none }}</option>
</MkSelect> </MkSelect>
<MkSelect v-if="type === 'list'" v-model="userListId"> <MkSelect v-if="type === 'list'" v-model="userListId">
@ -21,31 +15,61 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect> </MkSelect>
<div class="_buttons"> <div class="_buttons">
<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> <MkButton inline primary :disabled="type === 'list' && userListId === null" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts">
const notificationConfigTypes = [
'all',
'following',
'follower',
'mutualFollow',
'followingOrFollower',
'list',
'never'
] as const;
export type NotificationConfig = {
type: Exclude<typeof notificationConfigTypes[number], 'list'>;
} | {
type: 'list';
userListId: string;
};
</script>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { ref } from 'vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
value: any; value: NotificationConfig;
userLists: Misskey.entities.UserList[]; userLists: Misskey.entities.UserList[];
configurableTypes?: NotificationConfig['type'][]; // If not specified, all types are configurable
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update', result: any): void; (ev: 'update', result: NotificationConfig): void;
}>(); }>();
const notificationConfigTypesI18nMap: Record<typeof notificationConfigTypes[number], string> = {
all: i18n.ts.all,
following: i18n.ts.following,
follower: i18n.ts.followers,
mutualFollow: i18n.ts.mutualFollow,
followingOrFollower: i18n.ts.followingOrFollower,
list: i18n.ts.userList,
never: i18n.ts.none,
};
const type = ref(props.value.type); const type = ref(props.value.type);
const userListId = ref(props.value.userListId); const userListId = ref(props.value.type === 'list' ? props.value.userListId : null);
function save() { function save() {
emit('update', { type: type.value, userListId: userListId.value }); emit('update', type.value === 'list' ? { type: type.value, userListId: userListId.value! } : { type: type.value });
} }
</script> </script>

View File

@ -22,7 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only
}} }}
</template> </template>
<XNotificationConfig :userLists="userLists" :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }" @update="(res) => updateReceiveConfig(type, res)"/> <XNotificationConfig
:userLists="userLists"
:value="$i.notificationRecieveConfig[type] ?? { type: 'all' }"
:configurableTypes="onlyOnOrOffNotificationTypes.includes(type) ? ['all', 'never'] : undefined"
@update="(res) => updateReceiveConfig(type, res)"
/>
</MkFolder> </MkFolder>
</div> </div>
</FormSection> </FormSection>
@ -58,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef, computed } from 'vue'; import { shallowRef, computed } from 'vue';
import XNotificationConfig from './notifications.notification-config.vue'; import XNotificationConfig, { type NotificationConfig } from './notifications.notification-config.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
@ -73,7 +78,9 @@ import { notificationTypes } from '@@/js/const.js';
const $i = signinRequired(); const $i = signinRequired();
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[]; const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login'] satisfies (typeof notificationTypes[number])[] as string[];
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer); const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
@ -88,7 +95,7 @@ async function readAllNotifications() {
await os.apiWithDialog('notifications/mark-all-as-read'); await os.apiWithDialog('notifications/mark-all-as-read');
} }
async function updateReceiveConfig(type, value) { async function updateReceiveConfig(type: typeof notificationTypes[number], value: NotificationConfig) {
await os.apiWithDialog('i/update', { await os.apiWithDialog('i/update', {
notificationRecieveConfig: { notificationRecieveConfig: {
...$i.notificationRecieveConfig, ...$i.notificationRecieveConfig,

View File

@ -217,7 +217,7 @@ const routes: RouteDef[] = [{
component: page(() => import('@/pages/theme-editor.vue')), component: page(() => import('@/pages/theme-editor.vue')),
loginRequired: true, loginRequired: true,
}, { }, {
path: '/roles/:role', path: '/roles/:roleId',
component: page(() => import('@/pages/role.vue')), component: page(() => import('@/pages/role.vue')),
}, { }, {
path: '/user-tags/:tag', path: '/user-tags/:tag',

View File

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import tinycolor from 'tinycolor2';
export const getBgColor = (elem?: Element | null | undefined): string | null => {
if (elem == null) return null;
const { backgroundColor: bg } = window.getComputedStyle(elem);
if (bg && tinycolor(bg).getAlpha() !== 0) {
return bg;
}
return getBgColor(elem.parentElement);
};