fix comf
This commit is contained in:
@ -122,7 +122,7 @@
|
||||
"langmap": "0.0.16",
|
||||
"mfm-js": "0.21.0",
|
||||
"mime-types": "2.1.34",
|
||||
"misskey-js": "0.0.13",
|
||||
"misskey-js": "0.0.14",
|
||||
"mocha": "8.4.0",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"multer": "1.4.4",
|
||||
|
@ -1,3 +1,3 @@
|
||||
export function isDuplicateKeyValueError(e: Error): boolean {
|
||||
return e.message.startsWith('duplicate key value');
|
||||
export function isDuplicateKeyValueError(e: unknown | Error): boolean {
|
||||
return (e as any).message && (e as Error).message.startsWith('duplicate key value');
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
|
||||
// Authentication
|
||||
authenticate(body['i']).then(([user, app]) => {
|
||||
// API invoking
|
||||
call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => {
|
||||
call(endpoint.name, user, app, body, ctx).then((res: any) => {
|
||||
reply(res);
|
||||
}).catch((e: ApiError) => {
|
||||
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import * as Koa from 'koa';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { limiter } from './limiter';
|
||||
import { User } from '@/models/entities/user';
|
||||
@ -12,7 +13,7 @@ const accessDenied = {
|
||||
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e',
|
||||
};
|
||||
|
||||
export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => {
|
||||
export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
|
||||
const isSecure = user != null && token == null;
|
||||
|
||||
const ep = endpoints.find(e => e.name === endpoint);
|
||||
@ -76,9 +77,20 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
|
||||
});
|
||||
}
|
||||
|
||||
// Cast non JSON input
|
||||
if (ep.meta.requireFile && ep.meta.params) {
|
||||
const body = (ctx!.request as any).body;
|
||||
for (const k of Object.keys(ep.meta.params)) {
|
||||
const param = ep.meta.params[k];
|
||||
if (['Boolean', 'Number'].includes(param.validator.name) && typeof body[k] === 'string') {
|
||||
body[k] = JSON.parse(body[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API invoking
|
||||
const before = performance.now();
|
||||
return await ep.exec(data, user, token, file).catch((e: Error) => {
|
||||
return await ep.exec(data, user, token, ctx?.file).catch((e: Error) => {
|
||||
if (e instanceof ApiError) {
|
||||
throw e;
|
||||
} else {
|
||||
|
@ -39,15 +39,13 @@ export const meta = {
|
||||
},
|
||||
|
||||
isSensitive: {
|
||||
validator: $.optional.either($.bool, $.str),
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
transform: (v: any): boolean => v === true || v === 'true',
|
||||
},
|
||||
|
||||
force: {
|
||||
validator: $.optional.either($.bool, $.str),
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
transform: (v: any): boolean => v === true || v === 'true',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -105,7 +105,10 @@ export interface NoteStreamTypes {
|
||||
};
|
||||
reacted: {
|
||||
reaction: string;
|
||||
emoji?: Emoji;
|
||||
emoji?: {
|
||||
name: string;
|
||||
url: string;
|
||||
} | null;
|
||||
userId: User['id'];
|
||||
};
|
||||
unreacted: {
|
||||
|
@ -21,6 +21,7 @@ html
|
||||
meta(name='referrer' content='origin')
|
||||
meta(name='theme-color' content='#a7dc4e')
|
||||
meta(name='theme-color-orig' content='#a7dc4e')
|
||||
meta(property='twitter:card' content='summary')
|
||||
meta(property='og:site_name' content= instanceName || 'Misskey')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
@ -42,7 +43,9 @@ html
|
||||
block meta
|
||||
|
||||
block og
|
||||
meta(property='og:image' content=img)
|
||||
meta(property='og:title' content= title || 'Misskey')
|
||||
meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
|
||||
meta(property='og:image' content= img)
|
||||
|
||||
style
|
||||
include ../style.css
|
||||
|
@ -16,6 +16,3 @@ block og
|
||||
meta(property='og:description' content= channel.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= channel.bannerUrl)
|
||||
|
||||
block meta
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
@ -26,8 +26,6 @@ block meta
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:clip-id' content=clip.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
@ -25,8 +25,6 @@ block meta
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
@ -26,9 +26,7 @@ block meta
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:note-id' content=note.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
@ -26,8 +26,6 @@ block meta
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:page-id' content=page.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
@ -25,8 +25,6 @@ block meta
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
if profile.twitter
|
||||
meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
|
||||
|
||||
|
@ -59,7 +59,7 @@ class NotificationManager {
|
||||
|
||||
if (exist) {
|
||||
// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
|
||||
if (reason != 'mention') {
|
||||
if (reason !== 'mention') {
|
||||
exist.reason = reason;
|
||||
}
|
||||
} else {
|
||||
@ -201,7 +201,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
mentionedUsers.push(await Users.findOneOrFail(data.reply.userId));
|
||||
}
|
||||
|
||||
if (data.visibility == 'specified') {
|
||||
if (data.visibility === 'specified') {
|
||||
if (data.visibleUsers == null) throw new Error('invalid param');
|
||||
|
||||
for (const u of data.visibleUsers) {
|
||||
@ -301,7 +301,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
if (Users.isRemoteUser(user)) activeUsersChart.update(user);
|
||||
|
||||
// 未読通知を作成
|
||||
if (data.visibility == 'specified') {
|
||||
if (data.visibility === 'specified') {
|
||||
if (data.visibleUsers == null) throw new Error('invalid param');
|
||||
|
||||
for (const u of data.visibleUsers) {
|
||||
@ -439,7 +439,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
async function renderNoteOrRenoteActivity(data: Option, note: Note) {
|
||||
if (data.localOnly) return null;
|
||||
|
||||
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
|
||||
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)
|
||||
? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note)
|
||||
: renderCreate(await renderNote(note, false), note);
|
||||
|
||||
@ -478,7 +478,7 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
|
||||
userId: user.id,
|
||||
localOnly: data.localOnly!,
|
||||
visibility: data.visibility as any,
|
||||
visibleUserIds: data.visibility == 'specified'
|
||||
visibleUserIds: data.visibility === 'specified'
|
||||
? data.visibleUsers
|
||||
? data.visibleUsers.map(u => u.id)
|
||||
: []
|
||||
@ -502,7 +502,7 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
|
||||
insert.mentions = mentionedUsers.map(u => u.id);
|
||||
const profiles = await UserProfiles.find({ userId: In(insert.mentions) });
|
||||
insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => {
|
||||
const profile = profiles.find(p => p.userId == u.id);
|
||||
const profile = profiles.find(p => p.userId === u.id);
|
||||
const url = profile != null ? profile.url : null;
|
||||
return {
|
||||
uri: u.uri,
|
||||
|
@ -39,7 +39,7 @@ export default async function(user: User, note: Note, quiet = false) {
|
||||
let renote: Note | undefined;
|
||||
|
||||
// if deletd note is renote
|
||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) {
|
||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
||||
renote = await Notes.findOne({
|
||||
id: note.renoteId,
|
||||
});
|
||||
|
@ -76,7 +76,7 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
|
||||
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||
const decodedReaction = decodeReaction(reaction);
|
||||
|
||||
let emoji = await Emojis.findOne({
|
||||
const emoji = await Emojis.findOne({
|
||||
where: {
|
||||
name: decodedReaction.name,
|
||||
host: decodedReaction.host,
|
||||
|
@ -52,7 +52,7 @@ export default async function(
|
||||
|
||||
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
|
||||
for (const antenna of myAntennas) {
|
||||
if (await checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) {
|
||||
if (await checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) {
|
||||
readAntennaNotes.push(note);
|
||||
}
|
||||
}
|
||||
|
@ -114,9 +114,9 @@ export async function sendEmail(to: string, subject: string, html: string, text:
|
||||
</html>`,
|
||||
});
|
||||
|
||||
logger.info('Message sent: %s', info.messageId);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
throw e;
|
||||
logger.info(`Message sent: ${info.messageId}`);
|
||||
} catch (err) {
|
||||
logger.error(err as Error);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
@ -4967,10 +4967,10 @@ minizlib@^2.0.0, minizlib@^2.1.1:
|
||||
minipass "^3.0.0"
|
||||
yallist "^4.0.0"
|
||||
|
||||
misskey-js@0.0.13:
|
||||
version "0.0.13"
|
||||
resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970"
|
||||
integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ==
|
||||
misskey-js@0.0.14:
|
||||
version "0.0.14"
|
||||
resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d"
|
||||
integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww==
|
||||
dependencies:
|
||||
autobind-decorator "^2.4.0"
|
||||
eventemitter3 "^4.0.7"
|
||||
|
@ -18,6 +18,7 @@ module.exports = {
|
||||
// data の禁止理由: 抽象的すぎるため
|
||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
"id-denylist": ["error", "window", "data", "e"],
|
||||
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
|
||||
"vue/attributes-order": ["error", {
|
||||
"alphabetical": false
|
||||
}],
|
||||
|
@ -69,7 +69,7 @@
|
||||
"langmap": "0.0.16",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.21.0",
|
||||
"misskey-js": "0.0.13",
|
||||
"misskey-js": "0.0.14",
|
||||
"mocha": "8.4.0",
|
||||
"ms": "2.1.3",
|
||||
"nested-property": "4.0.0",
|
||||
|
51
packages/client/src/components/chart-tooltip.vue
Normal file
51
packages/client/src/components/chart-tooltip.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')">
|
||||
<div v-if="title" class="qpcyisrl">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div v-for="x in series" class="series">
|
||||
<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span>
|
||||
<span>{{ x.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</MkTooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkTooltip from './ui/tooltip.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
showing: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
title: string;
|
||||
series: {
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
text: string;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qpcyisrl {
|
||||
> .title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
> .series {
|
||||
> .color {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
|
||||
import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
@ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale';
|
||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import MkChartTooltip from '@/components/chart-tooltip.vue';
|
||||
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
@ -137,6 +138,43 @@ export default defineComponent({
|
||||
}));
|
||||
};
|
||||
|
||||
const tooltipShowing = ref(false);
|
||||
const tooltipX = ref(0);
|
||||
const tooltipY = ref(0);
|
||||
const tooltipTitle = ref(null);
|
||||
const tooltipSeries = ref(null);
|
||||
let disposeTooltipComponent;
|
||||
|
||||
os.popup(MkChartTooltip, {
|
||||
showing: tooltipShowing,
|
||||
x: tooltipX,
|
||||
y: tooltipY,
|
||||
title: tooltipTitle,
|
||||
series: tooltipSeries,
|
||||
}, {}).then(({ dispose }) => {
|
||||
disposeTooltipComponent = dispose;
|
||||
});
|
||||
|
||||
function externalTooltipHandler(context) {
|
||||
if (context.tooltip.opacity === 0) {
|
||||
tooltipShowing.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
tooltipTitle.value = context.tooltip.title[0];
|
||||
tooltipSeries.value = context.tooltip.body.map((b, i) => ({
|
||||
backgroundColor: context.tooltip.labelColors[i].backgroundColor,
|
||||
borderColor: context.tooltip.labelColors[i].borderColor,
|
||||
text: b.lines[0],
|
||||
}));
|
||||
|
||||
const rect = context.chart.canvas.getBoundingClientRect();
|
||||
|
||||
tooltipShowing.value = true;
|
||||
tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
|
||||
tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
@ -212,7 +250,15 @@ export default defineComponent({
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
hoverRadius: 5,
|
||||
hoverBorderWidth: 2,
|
||||
},
|
||||
},
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: props.detailed,
|
||||
@ -222,10 +268,12 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
@ -684,6 +732,10 @@ export default defineComponent({
|
||||
fetchAndRender();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (disposeTooltipComponent) disposeTooltipComponent();
|
||||
});
|
||||
|
||||
return {
|
||||
chartEl,
|
||||
fetching,
|
||||
|
@ -117,7 +117,7 @@ export default defineComponent({
|
||||
text: computed(() => {
|
||||
return props.textConverter(finalValue.value);
|
||||
}),
|
||||
source: thumbEl,
|
||||
targetElement: thumbEl,
|
||||
}, {}, 'closed');
|
||||
|
||||
const style = document.createElement('style');
|
||||
|
@ -20,45 +20,33 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, toRefs } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, Ref } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import Ripple from '@/components/ripple.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
const props = defineProps<{
|
||||
modelValue: boolean | Ref<boolean>;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
setup(props, context) {
|
||||
const button = ref<HTMLElement>();
|
||||
const checked = toRefs(props).modelValue;
|
||||
const toggle = () => {
|
||||
if (props.disabled) return;
|
||||
context.emit('update:modelValue', !checked.value);
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void;
|
||||
}>();
|
||||
|
||||
if (!checked.value) {
|
||||
const rect = button.value.getBoundingClientRect();
|
||||
const x = rect.left + (button.value.offsetWidth / 2);
|
||||
const y = rect.top + (button.value.offsetHeight / 2);
|
||||
os.popup(Ripple, { x, y, particle: false }, {}, 'end');
|
||||
}
|
||||
};
|
||||
let button = $ref<HTMLElement>();
|
||||
const checked = toRefs(props).modelValue;
|
||||
const toggle = () => {
|
||||
if (props.disabled) return;
|
||||
emit('update:modelValue', !checked.value);
|
||||
|
||||
return {
|
||||
button,
|
||||
checked,
|
||||
toggle,
|
||||
};
|
||||
},
|
||||
});
|
||||
if (!checked.value) {
|
||||
const rect = button.getBoundingClientRect();
|
||||
const x = rect.left + (button.offsetWidth / 2);
|
||||
const y = rect.top + (button.offsetHeight / 2);
|
||||
os.popup(Ripple, { x, y, particle: false }, {}, 'end');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -23,8 +23,9 @@ const props = withDefaults(defineProps<{
|
||||
behavior: null,
|
||||
});
|
||||
|
||||
const navHook = inject('navHook', null);
|
||||
const sideViewHook = inject('sideViewHook', null);
|
||||
type Navigate = (path: string, record?: boolean) => void;
|
||||
const navHook = inject<null | Navigate>('navHook', null);
|
||||
const sideViewHook = inject<null | Navigate>('sideViewHook', null);
|
||||
|
||||
const active = $computed(() => {
|
||||
if (props.activeClass == null) return false;
|
||||
|
@ -153,7 +153,7 @@ export default defineComponent({
|
||||
showing,
|
||||
reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
|
||||
emojis: props.notification.note.emojis,
|
||||
source: reactionRef.value.$el,
|
||||
targetElement: reactionRef.value.$el,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
|
@ -135,7 +135,10 @@ let showPreview = $ref(false);
|
||||
let cw = $ref<string | null>(null);
|
||||
let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
|
||||
let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]);
|
||||
let visibleUsers = $ref(props.initialVisibleUsers ?? []);
|
||||
let visibleUsers = $ref([]);
|
||||
if (props.initialVisibleUsers) {
|
||||
props.initialVisibleUsers.forEach(pushVisibleUser);
|
||||
}
|
||||
let autocomplete = $ref(null);
|
||||
let draghover = $ref(false);
|
||||
let quoteId = $ref(null);
|
||||
@ -262,12 +265,12 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
|
||||
os.api('users/show', {
|
||||
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId)
|
||||
}).then(users => {
|
||||
visibleUsers.push(...users);
|
||||
users.forEach(pushVisibleUser);
|
||||
});
|
||||
|
||||
if (props.reply.userId !== $i.id) {
|
||||
os.api('users/show', { userId: props.reply.userId }).then(user => {
|
||||
visibleUsers.push(user);
|
||||
pushVisibleUser(user);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -275,7 +278,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
|
||||
|
||||
if (props.specified) {
|
||||
visibility = 'specified';
|
||||
visibleUsers.push(props.specified);
|
||||
pushVisibleUser(props.specified);
|
||||
}
|
||||
|
||||
// keep cw when reply
|
||||
@ -397,9 +400,15 @@ function setVisibility() {
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
function pushVisibleUser(user) {
|
||||
if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
|
||||
visibleUsers.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
function addVisibleUser() {
|
||||
os.selectUser().then(user => {
|
||||
visibleUsers.push(user);
|
||||
pushVisibleUser(user);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||
<div class="beeadbfb">
|
||||
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
|
||||
<div class="name">{{ reaction.replace('@.', '') }}</div>
|
||||
@ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue';
|
||||
const props = defineProps<{
|
||||
reaction: string;
|
||||
emojis: any[]; // TODO
|
||||
source: any; // TODO
|
||||
targetElement: HTMLElement;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||
<div class="bqxuuuey">
|
||||
<div class="reaction">
|
||||
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
|
||||
@ -26,11 +26,11 @@ const props = defineProps<{
|
||||
users: any[]; // TODO
|
||||
count: number;
|
||||
emojis: any[]; // TODO
|
||||
source: any; // TODO
|
||||
targetElement: HTMLElement;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
@ -101,7 +101,7 @@ export default defineComponent({
|
||||
emojis: props.note.emojis,
|
||||
users,
|
||||
count: props.count,
|
||||
source: buttonRef.value
|
||||
targetElement: buttonRef.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
|
@ -52,7 +52,7 @@ export default defineComponent({
|
||||
showing,
|
||||
users,
|
||||
count: props.count,
|
||||
source: buttonRef.value
|
||||
targetElement: buttonRef.value
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :target-element="targetElement" :max-width="250" @closed="emit('closed')">
|
||||
<div class="beaffaef">
|
||||
<div v-for="u in users" :key="u.id" class="user">
|
||||
<MkAvatar class="avatar" :user="u"/>
|
||||
@ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue';
|
||||
const props = defineProps<{
|
||||
users: any[]; // TODO
|
||||
count: number;
|
||||
source: any; // TODO
|
||||
targetElement: HTMLElement;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
@ -1,88 +1,71 @@
|
||||
<template>
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" appear>
|
||||
<div class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||
<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||
<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount } from 'vue';
|
||||
import contains from '@/scripts/contains';
|
||||
import MkMenu from './menu.vue';
|
||||
import { MenuItem } from './types/menu.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkMenu,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
ev: {
|
||||
required: true
|
||||
},
|
||||
viaKeyboard: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
emits: ['closed'],
|
||||
data() {
|
||||
return {
|
||||
zIndex: os.claimZIndex('high'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'esc': () => this.$emit('closed'),
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
let left = this.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
let top = this.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
const props = defineProps<{
|
||||
items: MenuItem[];
|
||||
ev: MouseEvent;
|
||||
}>();
|
||||
|
||||
const width = this.$el.offsetWidth;
|
||||
const height = this.$el.offsetHeight;
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
}>();
|
||||
|
||||
if (left + width - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - width + window.pageXOffset;
|
||||
}
|
||||
let rootEl = $ref<HTMLDivElement>();
|
||||
|
||||
if (top + height - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - height + window.pageYOffset;
|
||||
}
|
||||
let zIndex = $ref<number>(os.claimZIndex('high'));
|
||||
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
onMounted(() => {
|
||||
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
}
|
||||
const width = rootEl.offsetWidth;
|
||||
const height = rootEl.offsetHeight;
|
||||
|
||||
this.$el.style.top = top + 'px';
|
||||
this.$el.style.left = left + 'px';
|
||||
if (left + width - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - width + window.pageXOffset;
|
||||
}
|
||||
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.addEventListener('mousedown', this.onMousedown);
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.removeEventListener('mousedown', this.onMousedown);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onMousedown(e) {
|
||||
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed');
|
||||
},
|
||||
if (top + height - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - height + window.pageYOffset;
|
||||
}
|
||||
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
}
|
||||
|
||||
rootEl.style.top = `${top}px`;
|
||||
rootEl.style.left = `${left}px`;
|
||||
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.addEventListener('mousedown', onMousedown);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.removeEventListener('mousedown', onMousedown);
|
||||
}
|
||||
});
|
||||
|
||||
function onMousedown(e: Event) {
|
||||
if (!contains(rootEl, e.target) && (rootEl != e.target)) emit('closed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div ref="items" v-hotkey="keymap"
|
||||
<div ref="itemsEl" v-hotkey="keymap"
|
||||
class="rrevdjwt"
|
||||
:class="{ center: align === 'center', asDrawer }"
|
||||
:style="{ width: (width && !asDrawer) ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }"
|
||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
||||
@contextmenu.self="e => e.preventDefault()"
|
||||
>
|
||||
<template v-for="(item, i) in items2">
|
||||
@ -28,6 +28,9 @@
|
||||
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
|
||||
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</button>
|
||||
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item">
|
||||
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
|
||||
</span>
|
||||
<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
|
||||
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
|
||||
@ -41,114 +44,78 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, unref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, watch } from 'vue';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||
import contains from '@/scripts/contains';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
viaKeyboard: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
asDrawer: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
align: {
|
||||
type: String,
|
||||
requried: false
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
maxHeight: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
items2: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'up|k|shift+tab': this.focusUp,
|
||||
'down|j|tab': this.focusDown,
|
||||
'esc': this.close,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
items: {
|
||||
handler() {
|
||||
const items = ref(unref(this.items).filter(item => item !== undefined));
|
||||
const props = defineProps<{
|
||||
items: MenuItem[];
|
||||
viaKeyboard?: boolean;
|
||||
asDrawer?: boolean;
|
||||
align?: 'center' | string;
|
||||
width?: number;
|
||||
maxHeight?: number;
|
||||
}>();
|
||||
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
const item = items.value[i];
|
||||
|
||||
if (item && item.then) { // if item is Promise
|
||||
items.value[i] = { type: 'pending' };
|
||||
item.then(actualItem => {
|
||||
items.value[i] = actualItem;
|
||||
});
|
||||
}
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
this.items2 = items;
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.viaKeyboard) {
|
||||
this.$nextTick(() => {
|
||||
focusNext(this.$refs.items.children[0], true, false);
|
||||
let itemsEl = $ref<HTMLDivElement>();
|
||||
|
||||
let items2: InnerMenuItem[] = $ref([]);
|
||||
|
||||
let keymap = $computed(() => ({
|
||||
'up|k|shift+tab': focusUp,
|
||||
'down|j|tab': focusDown,
|
||||
'esc': close,
|
||||
}));
|
||||
|
||||
watch(() => props.items, () => {
|
||||
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
if (item && 'then' in item) { // if item is Promise
|
||||
items[i] = { type: 'pending' };
|
||||
item.then(actualItem => {
|
||||
items2[i] = actualItem;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.contextmenuEvent) {
|
||||
this.$el.style.top = this.contextmenuEvent.pageY + 'px';
|
||||
this.$el.style.left = this.contextmenuEvent.pageX + 'px';
|
||||
items2 = items as InnerMenuItem[];
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.addEventListener('mousedown', this.onMousedown);
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.removeEventListener('mousedown', this.onMousedown);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clicked(fn, ev) {
|
||||
fn(ev);
|
||||
this.close();
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
focusUp() {
|
||||
focusPrev(document.activeElement);
|
||||
},
|
||||
focusDown() {
|
||||
focusNext(document.activeElement);
|
||||
},
|
||||
onMousedown(e) {
|
||||
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
|
||||
},
|
||||
onMounted(() => {
|
||||
if (props.viaKeyboard) {
|
||||
nextTick(() => {
|
||||
focusNext(itemsEl.children[0], true, false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function clicked(fn: MenuAction, ev: MouseEvent) {
|
||||
fn(ev);
|
||||
close();
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
function focusUp() {
|
||||
focusPrev(document.activeElement);
|
||||
}
|
||||
|
||||
function focusDown() {
|
||||
focusNext(document.activeElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,44 +1,28 @@
|
||||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="$refs.modal.close()" @closed="$emit('closed')">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="$refs.modal.close()"/>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkModal from './modal.vue';
|
||||
import MkMenu from './menu.vue';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkModal,
|
||||
MkMenu,
|
||||
},
|
||||
defineProps<{
|
||||
items: MenuItem[];
|
||||
align?: 'center' | string;
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
src?: any;
|
||||
}>();
|
||||
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
align: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
viaKeyboard: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
src: {
|
||||
required: false
|
||||
},
|
||||
},
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
}>();
|
||||
|
||||
emits: ['close', 'closed'],
|
||||
});
|
||||
let modal = $ref<InstanceType<typeof MkModal>>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -12,9 +12,11 @@ import * as os from '@/os';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showing: boolean;
|
||||
source: HTMLElement;
|
||||
targetElement?: HTMLElement;
|
||||
x?: number;
|
||||
y?: number;
|
||||
text?: string;
|
||||
maxWidth?; number;
|
||||
maxWidth?: number;
|
||||
}>(), {
|
||||
maxWidth: 250,
|
||||
});
|
||||
@ -29,13 +31,25 @@ const zIndex = os.claimZIndex('high');
|
||||
const setPosition = () => {
|
||||
if (el.value == null) return;
|
||||
|
||||
const rect = props.source.getBoundingClientRect();
|
||||
|
||||
const contentWidth = el.value.offsetWidth;
|
||||
const contentHeight = el.value.offsetHeight;
|
||||
|
||||
let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2);
|
||||
let top = rect.top + window.pageYOffset - contentHeight;
|
||||
let left: number;
|
||||
let top: number;
|
||||
|
||||
let rect: DOMRect;
|
||||
|
||||
if (props.targetElement) {
|
||||
rect = props.targetElement.getBoundingClientRect();
|
||||
|
||||
left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
|
||||
top = rect.top + window.pageYOffset - contentHeight;
|
||||
|
||||
el.value.style.transformOrigin = 'center bottom';
|
||||
} else {
|
||||
left = props.x;
|
||||
top = props.y - contentHeight;
|
||||
}
|
||||
|
||||
left -= (el.value.offsetWidth / 2);
|
||||
|
||||
@ -43,9 +57,14 @@ const setPosition = () => {
|
||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
||||
}
|
||||
|
||||
// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
|
||||
if (top - window.pageYOffset < 0) {
|
||||
top = rect.top + window.pageYOffset + props.source.offsetHeight;
|
||||
el.value.style.transformOrigin = 'center top';
|
||||
if (props.targetElement) {
|
||||
top = rect.top + window.pageYOffset + props.targetElement.offsetHeight;
|
||||
el.value.style.transformOrigin = 'center top';
|
||||
} else {
|
||||
top = props.y;
|
||||
}
|
||||
}
|
||||
|
||||
el.value.style.left = left + 'px';
|
||||
@ -54,11 +73,6 @@ const setPosition = () => {
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (props.source == null) {
|
||||
emit('closed');
|
||||
return;
|
||||
}
|
||||
|
||||
setPosition();
|
||||
|
||||
let loopHandler;
|
||||
@ -101,6 +115,6 @@ onMounted(() => {
|
||||
border-radius: 4px;
|
||||
border: solid 0.5px var(--divider);
|
||||
pointer-events: none;
|
||||
transform-origin: center bottom;
|
||||
transform-origin: center center;
|
||||
}
|
||||
</style>
|
||||
|
@ -48,7 +48,7 @@ export default {
|
||||
popup(import('@/components/ui/tooltip.vue'), {
|
||||
showing,
|
||||
text: self.text,
|
||||
source: el
|
||||
targetElement: el,
|
||||
}, {}, 'closed');
|
||||
|
||||
self._close = () => {
|
||||
@ -56,8 +56,8 @@ export default {
|
||||
};
|
||||
};
|
||||
|
||||
el.addEventListener('selectstart', e => {
|
||||
e.preventDefault();
|
||||
el.addEventListener('selectstart', ev => {
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
el.addEventListener(start, () => {
|
||||
|
@ -14,7 +14,7 @@ if (localStorage.getItem('accounts') != null) {
|
||||
//#endregion
|
||||
|
||||
import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue';
|
||||
import * as compareVersions from 'compare-versions';
|
||||
import compareVersions from 'compare-versions';
|
||||
|
||||
import widgets from '@/widgets';
|
||||
import directives from '@/directives';
|
||||
|
@ -7,8 +7,10 @@ import * as Misskey from 'misskey-js';
|
||||
import { apiUrl, url } from '@/config';
|
||||
import MkPostFormDialog from '@/components/post-form-dialog.vue';
|
||||
import MkWaitingDialog from '@/components/waiting-dialog.vue';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import { resolve } from '@/router';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
export const pendingApiRequestsCount = ref(0);
|
||||
|
||||
@ -470,7 +472,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
|
||||
});
|
||||
}
|
||||
|
||||
export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: {
|
||||
export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement, options?: {
|
||||
align?: string;
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
@ -494,7 +496,7 @@ export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?
|
||||
});
|
||||
}
|
||||
|
||||
export function contextMenu(items: any[], ev: MouseEvent) {
|
||||
export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
return new Promise((resolve, reject) => {
|
||||
let dispose;
|
||||
@ -541,7 +543,7 @@ export const uploads = ref<{
|
||||
img: string;
|
||||
}[]>([]);
|
||||
|
||||
export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> {
|
||||
export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> {
|
||||
if (folder && typeof folder == 'object') folder = folder.id;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -559,6 +561,8 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey
|
||||
|
||||
uploads.value.push(ctx);
|
||||
|
||||
console.log(keepOriginal);
|
||||
|
||||
const data = new FormData();
|
||||
data.append('i', $i.token);
|
||||
data.append('force', 'true');
|
||||
|
@ -115,7 +115,7 @@ const pagination = {
|
||||
offsetMode: true,
|
||||
params: computed(() => ({
|
||||
sort: sort,
|
||||
host: host != '' ? host : null,
|
||||
host: host !== '' ? host : null,
|
||||
...(
|
||||
state === 'federating' ? { federating: true } :
|
||||
state === 'subscribing' ? { subscribing: true } :
|
||||
@ -157,11 +157,10 @@ defineExpose({
|
||||
|
||||
> .instance {
|
||||
padding: 16px;
|
||||
border: solid 1px var(--divider);
|
||||
border-radius: 6px;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
border: solid 1px var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@
|
||||
<template #label>Moderation</template>
|
||||
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
|
||||
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
|
||||
<MkButton @click="refreshMetadata">Refresh metadata</MkButton>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
@ -111,6 +112,7 @@ import MkChart from '@/components/chart.vue';
|
||||
import MkObjectView from '@/components/object-view.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkLink from '@/components/link.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
@ -155,6 +157,15 @@ async function toggleSuspend(v) {
|
||||
});
|
||||
}
|
||||
|
||||
function refreshMetadata() {
|
||||
os.api('admin/federation/refresh-remote-instance-metadata', {
|
||||
host: instance.host,
|
||||
});
|
||||
os.alert({
|
||||
text: 'Refresh requested',
|
||||
});
|
||||
}
|
||||
|
||||
fetch();
|
||||
|
||||
defineExpose({
|
||||
|
@ -28,6 +28,7 @@
|
||||
<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
|
||||
<template #suffixIcon><i class="fas fa-folder-open"></i></template>
|
||||
</FormLink>
|
||||
<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
@ -36,18 +37,21 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as symbols from '@/symbols';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
// TODO: render chart
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormLink,
|
||||
FormSwitch,
|
||||
FormSection,
|
||||
MkKeyValue,
|
||||
FormSplit,
|
||||
@ -79,7 +83,8 @@ export default defineComponent({
|
||||
l: 0.5
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'),
|
||||
},
|
||||
|
||||
async created() {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ref } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { i18n } from '@/i18n';
|
||||
@ -6,12 +7,14 @@ import { DriveFile } from 'misskey-js/built/entities';
|
||||
|
||||
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
||||
|
||||
const chooseFileFromPc = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = multiple;
|
||||
input.onchange = () => {
|
||||
const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder));
|
||||
const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value));
|
||||
|
||||
Promise.all(promises).then(driveFiles => {
|
||||
res(multiple ? driveFiles : driveFiles[0]);
|
||||
@ -74,6 +77,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
||||
text: label,
|
||||
type: 'label'
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal
|
||||
}, {
|
||||
text: i18n.ts.upload,
|
||||
icon: 'fas fa-upload',
|
||||
action: chooseFileFromPc
|
||||
|
@ -43,6 +43,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'account',
|
||||
default: 'yyyy-MM-dd HH-mm-ss [{{number}}]'
|
||||
},
|
||||
keepOriginalUploading: {
|
||||
where: 'account',
|
||||
default: false
|
||||
},
|
||||
memo: {
|
||||
where: 'account',
|
||||
default: null
|
||||
|
20
packages/client/src/types/menu.ts
Normal file
20
packages/client/src/types/menu.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { Ref } from 'vue';
|
||||
|
||||
export type MenuAction = (ev: MouseEvent) => void;
|
||||
|
||||
export type MenuDivider = null;
|
||||
export type MenuNull = undefined;
|
||||
export type MenuLabel = { type: 'label', text: string };
|
||||
export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
|
||||
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean };
|
||||
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
|
||||
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
|
||||
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
|
||||
|
||||
export type MenuPending = { type: 'pending' };
|
||||
|
||||
type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;
|
||||
type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton>;
|
||||
export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
|
||||
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;
|
@ -4,7 +4,7 @@
|
||||
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
||||
<button v-if="history.length > 0" class="_button" @click="back()"><i class="fas fa-chevron-left"></i></button>
|
||||
<button v-else class="_button" style="pointer-events: none;"><!-- マージンのバランスを取るためのダミー --></button>
|
||||
<span class="title">{{ pageInfo.title }}</span>
|
||||
<span class="title" v-text="pageInfo?.title" />
|
||||
<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
|
||||
</header>
|
||||
<MkHeader class="pageHeader" :info="pageInfo"/>
|
||||
@ -13,99 +13,89 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { provide } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { resolve } from '@/router';
|
||||
import { url } from '@/config';
|
||||
import { resolve, router } from '@/router';
|
||||
import { url as root } from '@/config';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
provide() {
|
||||
return {
|
||||
navHook: (path) => {
|
||||
this.navigate(path);
|
||||
}
|
||||
};
|
||||
},
|
||||
provide('navHook', navigate);
|
||||
|
||||
data() {
|
||||
return {
|
||||
path: null,
|
||||
component: null,
|
||||
props: {},
|
||||
pageInfo: null,
|
||||
history: [],
|
||||
};
|
||||
},
|
||||
let path: string | null = $ref(null);
|
||||
let component: ReturnType<typeof resolve>['component'] | null = $ref(null);
|
||||
let props: any | null = $ref(null);
|
||||
let pageInfo: any | null = $ref(null);
|
||||
let history: string[] = $ref([]);
|
||||
|
||||
computed: {
|
||||
url(): string {
|
||||
return url + this.path;
|
||||
}
|
||||
},
|
||||
let url = $computed(() => `${root}${path}`);
|
||||
|
||||
methods: {
|
||||
changePage(page) {
|
||||
if (page == null) return;
|
||||
if (page[symbols.PAGE_INFO]) {
|
||||
this.pageInfo = page[symbols.PAGE_INFO];
|
||||
}
|
||||
},
|
||||
|
||||
navigate(path, record = true) {
|
||||
if (record && this.path) this.history.push(this.path);
|
||||
this.path = path;
|
||||
const { component, props } = resolve(path);
|
||||
this.component = component;
|
||||
this.props = props;
|
||||
},
|
||||
|
||||
back() {
|
||||
this.navigate(this.history.pop(), false);
|
||||
},
|
||||
|
||||
close() {
|
||||
this.path = null;
|
||||
this.component = null;
|
||||
this.props = {};
|
||||
},
|
||||
|
||||
onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: this.path,
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: this.$ts.showInPage,
|
||||
action: () => {
|
||||
this.$router.push(this.path);
|
||||
this.close();
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: this.$ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(this.path);
|
||||
this.close();
|
||||
}
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: this.$ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(this.url, '_blank');
|
||||
this.close();
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: this.$ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(this.url);
|
||||
}
|
||||
}], ev);
|
||||
}
|
||||
function changePage(page) {
|
||||
if (page == null) return;
|
||||
if (page[symbols.PAGE_INFO]) {
|
||||
pageInfo = page[symbols.PAGE_INFO];
|
||||
}
|
||||
}
|
||||
|
||||
function navigate(_path: string, record = true) {
|
||||
if (record && path) history.push($$(path).value);
|
||||
path = _path;
|
||||
const resolved = resolve(path);
|
||||
component = resolved.component;
|
||||
props = resolved.props;
|
||||
}
|
||||
|
||||
function back() {
|
||||
const prev = history.pop();
|
||||
if (prev) navigate(prev, false);
|
||||
}
|
||||
|
||||
function close() {
|
||||
path = null;
|
||||
component = null;
|
||||
props = {};
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: path || '',
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: () => {
|
||||
if (path) router.push(path);
|
||||
close();
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
if (path) os.pageWindow(path);
|
||||
close();
|
||||
}
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(url, '_blank');
|
||||
close();
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(url);
|
||||
}
|
||||
}], ev);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
navigate,
|
||||
back,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<XSideView v-if="isDesktop" ref="side" class="side"/>
|
||||
<XSideView v-if="isDesktop" ref="sideEl" class="side"/>
|
||||
|
||||
<div v-if="isDesktop" ref="widgetsEl" class="widgets">
|
||||
<XWidgets @mounted="attachSticky"/>
|
||||
@ -31,9 +31,9 @@
|
||||
<div v-if="isMobile" class="buttons">
|
||||
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||
<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
|
||||
<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||
<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||
<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
|
||||
<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
|
||||
<button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button>
|
||||
</div>
|
||||
|
||||
<transition :name="$store.state.animation ? 'menuDrawer-back' : ''">
|
||||
@ -64,155 +64,133 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue';
|
||||
import { instanceName } from '@/config';
|
||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||
import XSidebar from '@/ui/_common_/sidebar.vue';
|
||||
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import XSideView from './classic.side.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as EventEmitter from 'eventemitter3';
|
||||
import { menuDef } from '@/menu';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
|
||||
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/sidebar.vue'));
|
||||
|
||||
const DESKTOP_THRESHOLD = 1100;
|
||||
const MOBILE_THRESHOLD = 500;
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XCommon,
|
||||
XSidebar,
|
||||
XDrawerMenu,
|
||||
XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')),
|
||||
XSideView, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
|
||||
},
|
||||
|
||||
setup() {
|
||||
const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
|
||||
const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD);
|
||||
window.addEventListener('resize', () => {
|
||||
isMobile.value = window.innerWidth <= MOBILE_THRESHOLD;
|
||||
});
|
||||
|
||||
const pageInfo = ref();
|
||||
const widgetsEl = ref<HTMLElement>();
|
||||
const widgetsShowing = ref(false);
|
||||
|
||||
const sideViewController = new EventEmitter();
|
||||
|
||||
provide('sideViewHook', isDesktop.value ? (url) => {
|
||||
sideViewController.emit('navigate', url);
|
||||
} : null);
|
||||
|
||||
const menuIndicated = computed(() => {
|
||||
for (const def in menuDef) {
|
||||
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
|
||||
if (menuDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const drawerMenuShowing = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
watch(route, () => {
|
||||
drawerMenuShowing.value = false;
|
||||
});
|
||||
|
||||
document.documentElement.style.overflowY = 'scroll';
|
||||
|
||||
if (defaultStore.state.widgets.length === 0) {
|
||||
defaultStore.set('widgets', [{
|
||||
name: 'calendar',
|
||||
id: 'a', place: 'right', data: {}
|
||||
}, {
|
||||
name: 'notifications',
|
||||
id: 'b', place: 'right', data: {}
|
||||
}, {
|
||||
name: 'trends',
|
||||
id: 'c', place: 'right', data: {}
|
||||
}]);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isDesktop.value) {
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
|
||||
}, { passive: true });
|
||||
}
|
||||
});
|
||||
|
||||
const changePage = (page) => {
|
||||
if (page == null) return;
|
||||
if (page[symbols.PAGE_INFO]) {
|
||||
pageInfo.value = page[symbols.PAGE_INFO];
|
||||
document.title = `${pageInfo.value.title} | ${instanceName}`;
|
||||
}
|
||||
};
|
||||
|
||||
const onContextmenu = (ev) => {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(ev.target)) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
const path = route.path;
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: 'fas fa-columns',
|
||||
text: i18n.ts.openInSideView,
|
||||
action: () => {
|
||||
this.$refs.side.navigate(path);
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(path);
|
||||
}
|
||||
}], ev);
|
||||
};
|
||||
|
||||
const attachSticky = (el) => {
|
||||
const sticky = new StickySidebar(widgetsEl.value);
|
||||
window.addEventListener('scroll', () => {
|
||||
sticky.calc(window.scrollY);
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
return {
|
||||
pageInfo,
|
||||
isDesktop,
|
||||
isMobile,
|
||||
widgetsEl,
|
||||
widgetsShowing,
|
||||
drawerMenuShowing,
|
||||
menuIndicated,
|
||||
wallpaper: localStorage.getItem('wallpaper') != null,
|
||||
changePage,
|
||||
top: () => {
|
||||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
onTransition: () => {
|
||||
if (window._scroll) window._scroll();
|
||||
},
|
||||
post: os.post,
|
||||
onContextmenu,
|
||||
attachSticky,
|
||||
};
|
||||
},
|
||||
const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
|
||||
const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD);
|
||||
window.addEventListener('resize', () => {
|
||||
isMobile.value = window.innerWidth <= MOBILE_THRESHOLD;
|
||||
});
|
||||
|
||||
const pageInfo = ref();
|
||||
const widgetsEl = $ref<HTMLElement>();
|
||||
const widgetsShowing = ref(false);
|
||||
|
||||
let sideEl = $ref<InstanceType<typeof XSideView>>();
|
||||
|
||||
provide('sideViewHook', isDesktop.value ? (url) => {
|
||||
sideEl.navigate(url);
|
||||
} : null);
|
||||
|
||||
const menuIndicated = computed(() => {
|
||||
for (const def in menuDef) {
|
||||
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
|
||||
if (menuDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const drawerMenuShowing = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
watch(route, () => {
|
||||
drawerMenuShowing.value = false;
|
||||
});
|
||||
|
||||
document.documentElement.style.overflowY = 'scroll';
|
||||
|
||||
if (defaultStore.state.widgets.length === 0) {
|
||||
defaultStore.set('widgets', [{
|
||||
name: 'calendar',
|
||||
id: 'a', place: 'right', data: {}
|
||||
}, {
|
||||
name: 'notifications',
|
||||
id: 'b', place: 'right', data: {}
|
||||
}, {
|
||||
name: 'trends',
|
||||
id: 'c', place: 'right', data: {}
|
||||
}]);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isDesktop.value) {
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
|
||||
}, { passive: true });
|
||||
}
|
||||
});
|
||||
|
||||
const changePage = (page) => {
|
||||
if (page == null) return;
|
||||
if (page[symbols.PAGE_INFO]) {
|
||||
pageInfo.value = page[symbols.PAGE_INFO];
|
||||
document.title = `${pageInfo.value.title} | ${instanceName}`;
|
||||
}
|
||||
};
|
||||
|
||||
const onContextmenu = (ev) => {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(ev.target)) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection()?.toString() !== '') return;
|
||||
const path = route.path;
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: 'fas fa-columns',
|
||||
text: i18n.ts.openInSideView,
|
||||
action: () => {
|
||||
sideEl.navigate(path);
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(path);
|
||||
}
|
||||
}], ev);
|
||||
};
|
||||
|
||||
const attachSticky = (el) => {
|
||||
const sticky = new StickySidebar(widgetsEl);
|
||||
window.addEventListener('scroll', () => {
|
||||
sticky.calc(window.scrollY);
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
function top() {
|
||||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function onTransition() {
|
||||
if (window._scroll) window._scroll();
|
||||
}
|
||||
|
||||
const wallpaper = localStorage.getItem('wallpaper') != null;
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -54,13 +54,13 @@ const charts = ref([]);
|
||||
const fetching = ref(true);
|
||||
|
||||
const fetch = async () => {
|
||||
const instances = await os.api('federation/instances', {
|
||||
const fetchedInstances = await os.api('federation/instances', {
|
||||
sort: '+lastCommunicatedAt',
|
||||
limit: 5
|
||||
});
|
||||
const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
|
||||
instances.value = instances;
|
||||
charts.value = charts;
|
||||
const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
|
||||
instances.value = fetchedInstances;
|
||||
charts.value = fetchedCharts;
|
||||
fetching.value = false;
|
||||
};
|
||||
|
||||
|
@ -4139,10 +4139,10 @@ minimist@^1.2.0, minimist@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
misskey-js@0.0.13:
|
||||
version "0.0.13"
|
||||
resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970"
|
||||
integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ==
|
||||
misskey-js@0.0.14:
|
||||
version "0.0.14"
|
||||
resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d"
|
||||
integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww==
|
||||
dependencies:
|
||||
autobind-decorator "^2.4.0"
|
||||
eventemitter3 "^4.0.7"
|
||||
|
@ -37,6 +37,7 @@ module.exports = {
|
||||
]
|
||||
}],
|
||||
*/
|
||||
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
|
||||
'no-multi-spaces': ['error'],
|
||||
'no-var': ['error'],
|
||||
'prefer-arrow-callback': ['error'],
|
||||
@ -56,7 +57,7 @@ module.exports = {
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'space-infix-ops': ['error'],
|
||||
'space-before-blocks': ['error', 'always'],
|
||||
'@typescript-eslint/no-unnecessary-condition': ['error'],
|
||||
'@typescript-eslint/no-unnecessary-condition': ['warn'],
|
||||
'@typescript-eslint/no-var-requires': ['warn'],
|
||||
'@typescript-eslint/no-inferrable-types': ['warn'],
|
||||
'@typescript-eslint/no-empty-function': ['off'],
|
||||
|
Reference in New Issue
Block a user