Merge remote-tracking branch 'misskey/master' into feature/misskey-2024.8
This commit is contained in:
commit
6151099f5b
81 changed files with 1428 additions and 567 deletions
|
@ -3,6 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { AISCRIPT_VERSION } from '@syuilo/aiscript';
|
||||
import type { entities } from 'misskey-js'
|
||||
|
||||
export function abuseUserReport() {
|
||||
|
@ -114,6 +115,40 @@ export function file(isSensitive = false) {
|
|||
};
|
||||
}
|
||||
|
||||
const script = `/// @ ${AISCRIPT_VERSION}
|
||||
|
||||
var name = ""
|
||||
|
||||
Ui:render([
|
||||
Ui:C:textInput({
|
||||
label: "Your name"
|
||||
onInput: @(v) { name = v }
|
||||
})
|
||||
Ui:C:button({
|
||||
text: "Hello"
|
||||
onClick: @() {
|
||||
Mk:dialog(null, \`Hello, {name}!\`)
|
||||
}
|
||||
})
|
||||
])
|
||||
`;
|
||||
|
||||
export function flash(): entities.Flash {
|
||||
return {
|
||||
id: 'someflashid',
|
||||
createdAt: '2016-12-28T22:49:51.000Z',
|
||||
updatedAt: '2016-12-28T22:49:51.000Z',
|
||||
userId: 'someuserid',
|
||||
user: userLite(),
|
||||
title: 'Some Play title',
|
||||
summary: 'Some Play summary',
|
||||
script,
|
||||
visibility: 'public',
|
||||
likedCount: 0,
|
||||
isLiked: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function folder(id = 'somefolderid', name = 'Some Folder', parentId: string | null = null): entities.DriveFolder {
|
||||
return {
|
||||
id,
|
||||
|
|
|
@ -398,6 +398,7 @@ function toStories(component: string): Promise<string> {
|
|||
glob('src/components/global/Mk*.vue'),
|
||||
glob('src/components/global/RouterView.vue'),
|
||||
glob('src/components/Mk[A-E]*.vue'),
|
||||
glob('src/components/MkFlashPreview.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
glob('src/components/MkSignupServerRules.vue'),
|
||||
glob('src/components/MkUserSetupDialog.vue'),
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
"@syuilo/aiscript": "0.19.0",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"@vitejs/plugin-vue": "5.1.0",
|
||||
"@vue/compiler-sfc": "3.4.34",
|
||||
"@vue/compiler-sfc": "3.4.37",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
|
||||
"astring": "1.8.6",
|
||||
"broadcast-channel": "7.0.0",
|
||||
|
@ -73,7 +73,7 @@
|
|||
"uuid": "10.0.0",
|
||||
"v-code-diff": "1.12.0",
|
||||
"vite": "5.3.5",
|
||||
"vue": "3.4.34",
|
||||
"vue": "3.4.37",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -112,7 +112,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"@vitest/coverage-v8": "1.6.0",
|
||||
"@vue/runtime-core": "3.4.34",
|
||||
"@vue/runtime-core": "3.4.37",
|
||||
"acorn": "8.12.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.13.1",
|
||||
|
|
|
@ -230,17 +230,18 @@ export async function mainBoot() {
|
|||
claimAchievement('client60min');
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
const lastUsed = miLocalStorage.getItem('lastUsed');
|
||||
if (lastUsed) {
|
||||
const lastUsedDate = parseInt(lastUsed, 10);
|
||||
// 二時間以上前なら
|
||||
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
|
||||
toast(i18n.tsx.welcomeBackWithName({
|
||||
name: $i.name || $i.username,
|
||||
}), true);
|
||||
}
|
||||
}
|
||||
miLocalStorage.setItem('lastUsed', Date.now().toString());
|
||||
// 邪魔
|
||||
//const lastUsed = miLocalStorage.getItem('lastUsed');
|
||||
//if (lastUsed) {
|
||||
// const lastUsedDate = parseInt(lastUsed, 10);
|
||||
// // 二時間以上前なら
|
||||
// if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
|
||||
// toast(i18n.tsx.welcomeBackWithName({
|
||||
// name: $i.name || $i.username,
|
||||
// }), true);
|
||||
// }
|
||||
//}
|
||||
//miLocalStorage.setItem('lastUsed', Date.now().toString());
|
||||
|
||||
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
|
||||
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
||||
|
|
|
@ -39,7 +39,7 @@ import * as os from '@/os.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
user: Misskey.entities.UserDetailed;
|
||||
user: Misskey.entities.UserLite;
|
||||
initialComment?: string;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkFlashPreview from './MkFlashPreview.vue';
|
||||
import { flash } from './../../.storybook/fakes.js';
|
||||
export const Public = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkFlashPreview,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkFlashPreview v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
flash: {
|
||||
...flash(),
|
||||
visibility: 'public',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
|
||||
}),
|
||||
],
|
||||
} satisfies StoryObj<typeof MkFlashPreview>;
|
||||
export const Private = {
|
||||
...Public,
|
||||
args: {
|
||||
flash: {
|
||||
...flash(),
|
||||
visibility: 'private',
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkFlashPreview>;
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel">
|
||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" :class="[{ gray: flash.visibility === 'private' }]">
|
||||
<article>
|
||||
<header>
|
||||
<h1 :title="flash.title">{{ flash.title }}</h1>
|
||||
|
@ -22,11 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { userName } from '@/filters/user.js';
|
||||
|
||||
const props = defineProps<{
|
||||
//flash: Misskey.entities.Flash;
|
||||
flash: any;
|
||||
flash: Misskey.entities.Flash;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
@ -91,6 +91,12 @@ const props = defineProps<{
|
|||
}
|
||||
}
|
||||
|
||||
&:global(.gray) {
|
||||
--c: var(--bg);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
}
|
||||
|
||||
|
|
|
@ -234,6 +234,7 @@ import { host } from '@/config.js';
|
|||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -285,14 +286,7 @@ if (noteViewInterruptors.length > 0) {
|
|||
});
|
||||
}
|
||||
|
||||
const isRenote = (
|
||||
note.value.renote != null &&
|
||||
note.value.reply == null &&
|
||||
note.value.text == null &&
|
||||
note.value.cw == null &&
|
||||
note.value.fileIds && note.value.fileIds.length === 0 &&
|
||||
note.value.poll == null
|
||||
);
|
||||
const isRenote = Misskey.note.isPureRenote(note.value);
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
|
@ -303,7 +297,7 @@ const reactButton = shallowRef<HTMLElement>();
|
|||
const quoteButton = shallowRef<HTMLElement>();
|
||||
const clipButton = shallowRef<HTMLElement>();
|
||||
const likeButton = shallowRef<HTMLElement>();
|
||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||
const appearNote = computed(() => getAppearNote(note.value));
|
||||
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(defaultStore.state.uncollapseCW);
|
||||
|
|
|
@ -266,6 +266,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
@ -299,14 +300,7 @@ if (noteViewInterruptors.length > 0) {
|
|||
});
|
||||
}
|
||||
|
||||
const isRenote = (
|
||||
note.value.renote != null &&
|
||||
note.value.reply == null &&
|
||||
note.value.text == null &&
|
||||
note.value.cw == null &&
|
||||
note.value.fileIds && note.value.fileIds.length === 0 &&
|
||||
note.value.poll == null
|
||||
);
|
||||
const isRenote = Misskey.note.isPureRenote(note.value);
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
|
@ -317,7 +311,7 @@ const reactButton = shallowRef<HTMLElement>();
|
|||
const quoteButton = shallowRef<HTMLElement>();
|
||||
const clipButton = shallowRef<HTMLElement>();
|
||||
const likeButton = shallowRef<HTMLElement>();
|
||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||
const appearNote = computed(() => getAppearNote(note.value));
|
||||
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(defaultStore.state.uncollapseCW);
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div :class="$style.preview">
|
||||
<div :class="$style.preview__content1">
|
||||
<div>
|
||||
<MkInput v-model="text">
|
||||
<template #label>Text</template>
|
||||
</MkInput>
|
||||
|
|
|
@ -4,25 +4,32 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkA v-adaptive-bg :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
|
||||
<div :class="$style.title">
|
||||
<span :class="$style.icon">
|
||||
<template v-if="role.iconUrl">
|
||||
<img :class="$style.badge" :src="role.iconUrl"/>
|
||||
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
|
||||
<template v-if="forModeration">
|
||||
<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--success)"></i>
|
||||
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--warn)"></i>
|
||||
</template>
|
||||
|
||||
<div v-adaptive-bg class="_panel" :class="$style.body">
|
||||
<div :class="$style.bodyTitle">
|
||||
<span :class="$style.bodyIcon">
|
||||
<template v-if="role.iconUrl">
|
||||
<img :class="$style.bodyBadge" :src="role.iconUrl"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
|
||||
<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
|
||||
<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
|
||||
</template>
|
||||
</span>
|
||||
<span :class="$style.bodyName">{{ role.name }}</span>
|
||||
<template v-if="detailed">
|
||||
<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
|
||||
<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
|
||||
<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
|
||||
<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
|
||||
</template>
|
||||
</span>
|
||||
<span :class="$style.name">{{ role.name }}</span>
|
||||
<template v-if="detailed">
|
||||
<span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
|
||||
<span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
|
||||
</template>
|
||||
</div>
|
||||
<div :class="$style.bodyDescription">{{ role.description }}</div>
|
||||
</div>
|
||||
<div :class="$style.description">{{ role.description }}</div>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
|
@ -42,34 +49,44 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: block;
|
||||
padding: 16px 20px;
|
||||
border-left: solid 6px var(--color);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: block;
|
||||
padding: 16px 20px;
|
||||
flex: 1;
|
||||
border-left: solid 6px var(--color);
|
||||
}
|
||||
|
||||
.bodyTitle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bodyIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
.bodyBadge {
|
||||
height: 1.3em;
|
||||
vertical-align: -20%;
|
||||
}
|
||||
|
||||
.name {
|
||||
.bodyName {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.users {
|
||||
.bodyUsers {
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.description {
|
||||
.bodyDescription {
|
||||
opacity: 0.7;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { host as hostRaw } from '@/config.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
|
||||
defineProps<{
|
||||
user: Misskey.entities.User;
|
||||
user: Misskey.entities.UserLite;
|
||||
detail?: boolean;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<MkInfo v-if="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
||||
<MkInfo v-if="['instance.actor', 'relay.actor'].includes(user.username)">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
||||
|
||||
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
|
||||
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
|
||||
<MkSpacer :contentMax="700" :marginMin="16">
|
||||
<div class="lxpfedzu">
|
||||
<div class="lxpfedzu _gaps">
|
||||
<div class="banner">
|
||||
<img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||
</div>
|
||||
|
@ -62,10 +62,10 @@ const narrow = ref(false);
|
|||
const view = ref(null);
|
||||
const el = ref<HTMLDivElement | null>(null);
|
||||
const pageProps = ref({});
|
||||
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
|
||||
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableMcaptcha && !instance.enableTurnstile;
|
||||
let noEmailServer = !instance.enableEmail;
|
||||
let noInquiryUrl = isEmpty(instance.inquiryUrl);
|
||||
const noMaintainerInformation = computed(() => isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail));
|
||||
const noBotProtection = computed(() => !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile && !instance.enableMcaptcha);
|
||||
const noEmailServer = computed(() => !instance.enableEmail);
|
||||
const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl));
|
||||
const thereIsUnresolvedAbuseReport = ref(false);
|
||||
const pendingUserApprovals = ref(false);
|
||||
const currentPage = computed(() => router.currentRef.value.child);
|
||||
|
@ -250,25 +250,22 @@ const menuDef = computed(() => [{
|
|||
}],
|
||||
}]);
|
||||
|
||||
watch(narrow.value, () => {
|
||||
if (currentPage.value?.route.name == null && !narrow.value) {
|
||||
router.push('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
ro.observe(el.value);
|
||||
|
||||
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||
if (el.value != null) {
|
||||
ro.observe(el.value);
|
||||
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||
}
|
||||
if (currentPage.value?.route.name == null && !narrow.value) {
|
||||
router.push('/admin/overview');
|
||||
router.replace('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||
if (el.value != null) {
|
||||
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||
}
|
||||
if (currentPage.value?.route.name == null && !narrow.value) {
|
||||
router.push('/admin/overview');
|
||||
router.replace('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -21,13 +21,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
].includes(log.type),
|
||||
[$style.logYellow]: [
|
||||
'markSensitiveDriveFile',
|
||||
'resetPassword'
|
||||
'resetPassword',
|
||||
'suspendRemoteInstance',
|
||||
].includes(log.type),
|
||||
[$style.logRed]: [
|
||||
'suspend',
|
||||
'approve',
|
||||
'deleteRole',
|
||||
'suspendRemoteInstance',
|
||||
'deleteGlobalAnnouncement',
|
||||
'deleteUserAnnouncement',
|
||||
'deleteCustomEmoji',
|
||||
|
@ -37,6 +37,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
'deleteAvatarDecoration',
|
||||
'deleteSystemWebhook',
|
||||
'deleteAbuseReportNotificationRecipient',
|
||||
'deleteAccount',
|
||||
'deletePage',
|
||||
'deleteFlash',
|
||||
'deleteGalleryPost',
|
||||
].includes(log.type)
|
||||
}"
|
||||
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||
|
@ -74,6 +78,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="log.type === 'createAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
|
||||
<span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteAccount'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span>
|
||||
<span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span>
|
||||
<span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span>
|
||||
</template>
|
||||
<template #icon>
|
||||
<MkAvatar :user="log.user" :class="$style.avatar"/>
|
||||
|
@ -148,7 +156,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateRemoteInstanceNote'">
|
||||
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
|
||||
</div>
|
||||
|
|
|
@ -34,11 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSelect>
|
||||
</div>
|
||||
<div :class="$style.inputs">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:modelValue="$refs.users.reload()">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
|
||||
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
|
|
|
@ -369,7 +369,6 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const flash = ref<Misskey.entities.Flash | null>(null);
|
||||
const visibility = ref<'private' | 'public'>('public');
|
||||
|
||||
if (props.id) {
|
||||
flash.value = await misskeyApi('flash/show', {
|
||||
|
@ -380,6 +379,7 @@ if (props.id) {
|
|||
const title = ref(flash.value?.title ?? 'New Play');
|
||||
const summary = ref(flash.value?.summary ?? '');
|
||||
const permissions = ref(flash.value?.permissions ?? []);
|
||||
const visibility = ref<'private' | 'public'>(flash.value?.visibility ?? 'public');
|
||||
const script = ref(flash.value?.script ?? PRESET_DEFAULT);
|
||||
|
||||
function selectPreset(ev: MouseEvent) {
|
||||
|
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
|
||||
<MkButton v-if="$i && $i.id !== flash.user.id" class="button" rounded @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef } from 'vue';
|
||||
import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef, defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { Interpreter, Parser, values } from '@syuilo/aiscript';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
@ -79,6 +80,7 @@ import { defaultStore } from '@/store.js';
|
|||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -229,6 +231,53 @@ async function run() {
|
|||
}
|
||||
}
|
||||
|
||||
function reportAbuse() {
|
||||
if (!flash.value) return;
|
||||
|
||||
const pageUrl = `${url}/play/${flash.value.id}`;
|
||||
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
||||
user: flash.value.user,
|
||||
initialComment: `Play: ${pageUrl}\n-----\n`,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
if (!flash.value) return;
|
||||
|
||||
const menu: MenuItem[] = [
|
||||
...($i && $i.id !== flash.value.userId ? [
|
||||
{
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: reportAbuse,
|
||||
},
|
||||
...($i.isModerator || $i.isAdmin ? [
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: () => os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled || !flash.value) return;
|
||||
|
||||
os.apiWithDialog('flash/delete', { flashId: flash.value.id });
|
||||
}),
|
||||
},
|
||||
] : []),
|
||||
] : []),
|
||||
];
|
||||
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (aiscript.value) aiscript.value.abort();
|
||||
started.value = false;
|
||||
|
|
|
@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
|
||||
<button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
|
||||
<button v-if="$i && $i.id !== post.user.id" v-click-anime class="_button" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
|
@ -62,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { computed, watch, ref, defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -79,6 +80,7 @@ import { $i } from '@/account.js';
|
|||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -153,13 +155,56 @@ function edit() {
|
|||
router.push(`/gallery/${post.value.id}/edit`);
|
||||
}
|
||||
|
||||
function reportAbuse() {
|
||||
if (!post.value) return;
|
||||
|
||||
const pageUrl = `${url}/gallery/${post.value.id}`;
|
||||
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
||||
user: post.value.user,
|
||||
initialComment: `Post: ${pageUrl}\n-----\n`,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
if (!post.value) return;
|
||||
|
||||
const menu: MenuItem[] = [
|
||||
...($i && $i.id !== post.value.userId ? [
|
||||
{
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: reportAbuse,
|
||||
},
|
||||
...($i.isModerator || $i.isAdmin ? [
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: () => os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled || !post.value) return;
|
||||
|
||||
os.apiWithDialog('gallery/posts/delete', { postId: post.value.id });
|
||||
}),
|
||||
},
|
||||
] : []),
|
||||
] : []),
|
||||
];
|
||||
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
watch(() => props.postId, fetchPost, { immediate: true });
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
}]);
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
|
|
15
packages/frontend/src/pages/page-editor/common.ts
Normal file
15
packages/frontend/src/pages/page-editor/common.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
export function getPageBlockList() {
|
||||
return [
|
||||
{ value: 'section', text: i18n.ts._pages.blocks.section },
|
||||
{ value: 'text', text: i18n.ts._pages.blocks.text },
|
||||
{ value: 'image', text: i18n.ts._pages.blocks.image },
|
||||
{ value: 'note', text: i18n.ts._pages.blocks.note },
|
||||
];
|
||||
}
|
|
@ -29,6 +29,7 @@ import * as os from '@/os.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { getPageBlockList } from '@/pages/page-editor/common.js';
|
||||
|
||||
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
|
||||
|
||||
|
@ -53,11 +54,9 @@ watch(children, () => {
|
|||
deep: true,
|
||||
});
|
||||
|
||||
const getPageBlockList = inject<(any) => any>('getPageBlockList');
|
||||
|
||||
async function rename() {
|
||||
const { canceled, result: title } = await os.inputText({
|
||||
title: 'Enter title',
|
||||
title: i18n.ts._pages.enterSectionTitle,
|
||||
default: props.modelValue.title,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
|
|
@ -76,6 +76,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { getPageBlockList } from '@/pages/page-editor/common.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initPageId?: string;
|
||||
|
@ -100,7 +101,6 @@ const alignCenter = ref(false);
|
|||
const hideTitleWhenPinned = ref(false);
|
||||
|
||||
provide('readonly', readonly.value);
|
||||
provide('getPageBlockList', getPageBlockList);
|
||||
|
||||
watch(eyeCatchingImageId, async () => {
|
||||
if (eyeCatchingImageId.value == null) {
|
||||
|
@ -215,15 +215,6 @@ async function add() {
|
|||
content.value.push({ id, type });
|
||||
}
|
||||
|
||||
function getPageBlockList() {
|
||||
return [
|
||||
{ value: 'section', text: i18n.ts._pages.blocks.section },
|
||||
{ value: 'text', text: i18n.ts._pages.blocks.text },
|
||||
{ value: 'image', text: i18n.ts._pages.blocks.image },
|
||||
{ value: 'note', text: i18n.ts._pages.blocks.note },
|
||||
];
|
||||
}
|
||||
|
||||
function setEyeCatchingImage(img) {
|
||||
selectFile(img.currentTarget ?? img.target, null).then(file => {
|
||||
eyeCatchingImageId.value = file.id;
|
||||
|
|
|
@ -62,8 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
</div>
|
||||
<div :class="$style.other">
|
||||
<MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA>
|
||||
<button v-tooltip="i18n.ts.copyLink" class="_button" :class="$style.generalActionButton" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
|
||||
<button v-if="$i" v-click-anime class="_button" :class="$style.generalActionButton" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.pageUser">
|
||||
|
@ -78,14 +80,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
||||
<div v-if="page.createdAt != page.updatedAt"><i class="ti ti-clock-edit"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
|
||||
</div>
|
||||
<div :class="$style.pageLinks">
|
||||
<MkA v-if="!$i || $i.id !== page.userId" :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
|
||||
<template v-if="$i && $i.id === page.userId">
|
||||
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA>
|
||||
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
|
||||
<button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
|
@ -104,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { computed, watch, ref, defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XPage from '@/components/page/page.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
@ -126,6 +120,10 @@ import { isSupportShare } from '@/scripts/navigator.js';
|
|||
import { instance } from '@/instance.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
pageName: string;
|
||||
|
@ -242,6 +240,69 @@ function pin(pin) {
|
|||
});
|
||||
}
|
||||
|
||||
function reportAbuse() {
|
||||
if (!page.value) return;
|
||||
|
||||
const pageUrl = `${url}/@${props.username}/pages/${props.pageName}`;
|
||||
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
||||
user: page.value.user,
|
||||
initialComment: `Page: ${pageUrl}\n-----\n`,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
if (!page.value) return;
|
||||
|
||||
const menu: MenuItem[] = [
|
||||
...($i && $i.id === page.value.userId ? [
|
||||
{
|
||||
icon: 'ti ti-code',
|
||||
text: i18n.ts._pages.viewSource,
|
||||
action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`),
|
||||
},
|
||||
...($i.pinnedPageId === page.value.id ? [{
|
||||
icon: 'ti ti-pinned-off',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => pin(false),
|
||||
}] : [{
|
||||
icon: 'ti ti-pin',
|
||||
text: i18n.ts.pin,
|
||||
action: () => pin(true),
|
||||
}]),
|
||||
] : []),
|
||||
...($i && $i.id !== page.value.userId ? [
|
||||
{
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: reportAbuse,
|
||||
},
|
||||
...($i.isModerator || $i.isAdmin ? [
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: () => os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled || !page.value) return;
|
||||
|
||||
os.apiWithDialog('pages/delete', { pageId: page.value.id });
|
||||
}),
|
||||
},
|
||||
] : []),
|
||||
] : []),
|
||||
];
|
||||
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
watch(() => path.value, fetchPage, { immediate: true });
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -197,9 +197,6 @@ const menuDef = computed(() => [{
|
|||
}],
|
||||
}]);
|
||||
|
||||
watch(narrow, () => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
ro.observe(el.value);
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, provide, shallowRef, ref } from 'vue';
|
||||
import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue';
|
||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
@ -54,15 +54,18 @@ import { deepMerge } from '@/scripts/merge.js';
|
|||
import { MenuItem } from '@/types/menu.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
|
||||
import type { BasicTimelineType } from '@/timelines.js';
|
||||
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
||||
const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
|
||||
type TimelinePageSrc = BasicTimelineType | `list:${string}`;
|
||||
|
||||
const queue = ref(0);
|
||||
const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
|
||||
const src = computed<'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`>({
|
||||
const src = computed<TimelinePageSrc>({
|
||||
get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
|
||||
set: (x) => saveSrc(x),
|
||||
});
|
||||
|
@ -201,7 +204,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
|
|||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`): void {
|
||||
function saveSrc(newSrc: TimelinePageSrc): void {
|
||||
const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
|
||||
|
||||
if (newSrc.startsWith('userList:')) {
|
||||
|
@ -242,6 +245,19 @@ function closeTutorial(): void {
|
|||
defaultStore.set('timelineTutorials', before);
|
||||
}
|
||||
|
||||
function switchTlIfNeeded() {
|
||||
if (isBasicTimeline(src.value) && !isAvailableBasicTimeline(src.value)) {
|
||||
src.value = availableBasicTimelines()[0];
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
switchTlIfNeeded();
|
||||
});
|
||||
onActivated(() => {
|
||||
switchTlIfNeeded();
|
||||
});
|
||||
|
||||
const headerActions = computed(() => {
|
||||
const tmp = [
|
||||
{
|
||||
|
|
|
@ -16,21 +16,57 @@ function containsFocusTrappedElements(el: HTMLElement): boolean {
|
|||
});
|
||||
}
|
||||
|
||||
function getZIndex(el: HTMLElement): number {
|
||||
const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10);
|
||||
if (isNaN(zIndex)) {
|
||||
return 0;
|
||||
}
|
||||
return zIndex;
|
||||
}
|
||||
|
||||
function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null {
|
||||
let highestZIndexElement: HTMLElement | null = null;
|
||||
let highestZIndex = -Infinity;
|
||||
|
||||
focusTrapElements.forEach((el) => {
|
||||
const zIndex = getZIndex(el);
|
||||
if (zIndex > highestZIndex) {
|
||||
highestZIndex = zIndex;
|
||||
highestZIndexElement = el;
|
||||
}
|
||||
});
|
||||
|
||||
return highestZIndexElement == null ? null : {
|
||||
el: highestZIndexElement,
|
||||
zIndex: highestZIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function releaseFocusTrap(el: HTMLElement): void {
|
||||
focusTrapElements.delete(el);
|
||||
if (el.inert === true) {
|
||||
el.inert = false;
|
||||
}
|
||||
|
||||
const highestZIndexElement = getHighestZIndexElement();
|
||||
|
||||
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)) {
|
||||
if (
|
||||
siblingEl !== el &&
|
||||
(
|
||||
highestZIndexElement == null ||
|
||||
siblingEl === highestZIndexElement.el ||
|
||||
siblingEl.contains(highestZIndexElement.el)
|
||||
)
|
||||
) {
|
||||
siblingEl.inert = false;
|
||||
} else if (
|
||||
focusTrapElements.size > 0 &&
|
||||
!containsFocusTrappedElements(siblingEl) &&
|
||||
!focusTrapElements.has(siblingEl) &&
|
||||
highestZIndexElement != null &&
|
||||
siblingEl !== highestZIndexElement.el &&
|
||||
!siblingEl.contains(highestZIndexElement.el) &&
|
||||
!ignoreElements.includes(siblingEl.tagName.toLowerCase())
|
||||
) {
|
||||
siblingEl.inert = true;
|
||||
|
@ -45,9 +81,29 @@ function releaseFocusTrap(el: HTMLElement): void {
|
|||
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void;
|
||||
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; };
|
||||
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void {
|
||||
const highestZIndexElement = getHighestZIndexElement();
|
||||
|
||||
const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex;
|
||||
const zIndex = getZIndex(el);
|
||||
|
||||
// If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead
|
||||
// Focus trapping for this element will be done in the release function
|
||||
if (!parent && zIndex < highestZIndex) {
|
||||
focusTrapElements.add(el);
|
||||
if (highestZIndexElement) {
|
||||
focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls);
|
||||
}
|
||||
return {
|
||||
release: () => {
|
||||
releaseFocusTrap(el);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (el.inert === true) {
|
||||
el.inert = false;
|
||||
}
|
||||
|
||||
if (el.parentElement != null && el !== document.body) {
|
||||
el.parentElement.childNodes.forEach((siblingNode) => {
|
||||
const siblingEl = getHTMLElementOrNull(siblingNode);
|
||||
|
|
10
packages/frontend/src/scripts/get-appear-note.ts
Normal file
10
packages/frontend/src/scripts/get-appear-note.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export function getAppearNote(note: Misskey.entities.Note) {
|
||||
return Misskey.note.isPureRenote(note) ? note.renote : note;
|
||||
}
|
|
@ -20,6 +20,7 @@ import { clipsCache, favoritedChannelsCache } from '@/cache.js';
|
|||
import { MenuItem } from '@/types/menu.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
|
||||
export async function getNoteClipMenu(props: {
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -34,14 +35,7 @@ export async function getNoteClipMenu(props: {
|
|||
}
|
||||
}
|
||||
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
||||
const appearNote = getAppearNote(props.note);
|
||||
|
||||
const clips = await clipsCache.fetch();
|
||||
const menu: MenuItem[] = [...clips.map(clip => ({
|
||||
|
@ -175,14 +169,7 @@ export function getNoteMenu(props: {
|
|||
isDeleted: Ref<boolean>;
|
||||
currentClip?: Misskey.entities.Clip;
|
||||
}) {
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
||||
const appearNote = getAppearNote(props.note);
|
||||
|
||||
const cleanups = [] as (() => void)[];
|
||||
|
||||
|
@ -270,6 +257,7 @@ export function getNoteMenu(props: {
|
|||
}
|
||||
|
||||
async function unclip(): Promise<void> {
|
||||
if (!props.currentClip) return;
|
||||
os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
|
||||
props.isDeleted.value = true;
|
||||
}
|
||||
|
@ -289,8 +277,8 @@ export function getNoteMenu(props: {
|
|||
|
||||
function share(): void {
|
||||
navigator.share({
|
||||
title: i18n.tsx.noteOf({ user: appearNote.user.name }),
|
||||
text: appearNote.text,
|
||||
title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }),
|
||||
text: appearNote.text ?? '',
|
||||
url: `${url}/notes/${appearNote.id}`,
|
||||
});
|
||||
}
|
||||
|
@ -543,14 +531,7 @@ export function getRenoteMenu(props: {
|
|||
renoteButton: ShallowRef<HTMLElement | undefined>;
|
||||
mock?: boolean;
|
||||
}) {
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
||||
const appearNote = getAppearNote(props.note);
|
||||
|
||||
const channelRenoteItems: MenuItem[] = [];
|
||||
const normalRenoteItems: MenuItem[] = [];
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js';
|
|||
import { $i } from '@/account.js';
|
||||
|
||||
export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
|
||||
if ($i && $i.id === user.id) return true;
|
||||
if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
|
||||
|
||||
if (user.followingVisibility === 'private') return false;
|
||||
if (user.followingVisibility === 'followers' && !user.isFollowing) return false;
|
||||
|
@ -15,7 +15,7 @@ export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): bo
|
|||
return true;
|
||||
}
|
||||
export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
|
||||
if ($i && $i.id === user.id) return true;
|
||||
if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
|
||||
|
||||
if (user.followersVisibility === 'private') return false;
|
||||
if (user.followersVisibility === 'followers' && !user.isFollowing) return false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue