Merge branch 'notification-read-api' into swn
This commit is contained in:
@@ -11,52 +11,50 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "13.1.0",
|
||||
"@fortawesome/fontawesome-free": "6.0.0",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/glob": "7.2.0",
|
||||
"@types/gulp": "4.0.9",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/is-url": "1.2.30",
|
||||
"@types/katex": "0.11.1",
|
||||
"@types/matter-js": "0.17.6",
|
||||
"@types/mocha": "8.2.3",
|
||||
"@types/matter-js": "0.17.7",
|
||||
"@types/mocha": "9.1.0",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/parse5": "6.0.3",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.4.2",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/request-stats": "3.0.0",
|
||||
"@types/seedrandom": "2.4.28",
|
||||
"@types/throttle-debounce": "2.1.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/webpack": "5.28.0",
|
||||
"@types/webpack-stream": "3.2.12",
|
||||
"@types/websocket": "1.0.4",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.2.2",
|
||||
"@typescript-eslint/parser": "5.10.0",
|
||||
"@vue/compiler-sfc": "3.2.29",
|
||||
"@typescript-eslint/parser": "5.12.0",
|
||||
"@vue/compiler-sfc": "3.2.31",
|
||||
"abort-controller": "3.0.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "5.0.1",
|
||||
"autwh": "0.1.0",
|
||||
"blurhash": "1.1.4",
|
||||
"broadcast-channel": "4.9.0",
|
||||
"chart.js": "3.7.0",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.10.0",
|
||||
"chart.js": "3.7.1",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-gradient": "0.2.1",
|
||||
"chartjs-plugin-zoom": "1.2.0",
|
||||
"compare-versions": "4.1.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "6.5.1",
|
||||
"cssnano": "5.0.15",
|
||||
"css-loader": "6.6.0",
|
||||
"cssnano": "5.0.17",
|
||||
"date-fns": "2.28.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "8.7.0",
|
||||
"eslint-plugin-vue": "8.3.0",
|
||||
"eslint": "8.9.0",
|
||||
"eslint-plugin-vue": "8.4.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.2",
|
||||
"glob": "7.2.0",
|
||||
@@ -70,15 +68,15 @@
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.21.0",
|
||||
"misskey-js": "0.0.14",
|
||||
"mocha": "8.4.0",
|
||||
"mocha": "9.2.0",
|
||||
"ms": "2.1.3",
|
||||
"nested-property": "4.0.0",
|
||||
"parse5": "6.0.1",
|
||||
"photoswipe": "git+https://github.com/dimsemenov/photoswipe#v5-beta",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss": "8.4.5",
|
||||
"postcss": "8.4.6",
|
||||
"postcss-loader": "6.2.1",
|
||||
"prismjs": "1.26.0",
|
||||
"prismjs": "1.27.0",
|
||||
"private-ip": "2.3.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
@@ -87,11 +85,10 @@
|
||||
"querystring": "0.2.1",
|
||||
"random-seed": "0.3.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"request-stats": "3.0.0",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.49.0",
|
||||
"sass-loader": "12.4.0",
|
||||
"sass": "1.49.8",
|
||||
"sass-loader": "12.6.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
@@ -101,9 +98,8 @@
|
||||
"three": "0.136.0",
|
||||
"throttle-debounce": "3.0.1",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "9.2.6",
|
||||
"ts-node": "10.4.0",
|
||||
"ts-node": "10.5.0",
|
||||
"tsc-alias": "1.5.0",
|
||||
"tsconfig-paths": "3.12.0",
|
||||
"twemoji-parser": "13.1.0",
|
||||
@@ -111,25 +107,23 @@
|
||||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
"vanilla-tilt": "1.7.2",
|
||||
"vue": "3.2.29",
|
||||
"vue": "3.2.31",
|
||||
"vue-loader": "17.0.0",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vue-router": "4.0.5",
|
||||
"vue-router": "4.0.12",
|
||||
"vue-style-loader": "4.1.3",
|
||||
"vue-svg-loader": "0.17.0-beta.2",
|
||||
"vuedraggable": "4.0.1",
|
||||
"web-push": "3.4.5",
|
||||
"webpack": "5.66.0",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack": "5.69.1",
|
||||
"webpack-cli": "4.9.2",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.4.2"
|
||||
"ws": "8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.79",
|
||||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"@typescript-eslint/eslint-plugin": "5.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.12.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "9.3.1",
|
||||
"cypress": "9.5.0",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"start-server-and-test": "1.14.0"
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'left'" :inner-margin="16" @closed="emit('closed')">
|
||||
<div v-if="title" class="qpcyisrl">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div v-for="x in series" class="series">
|
||||
|
@@ -29,6 +29,7 @@ import {
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import MkChartTooltip from '@/components/chart-tooltip.vue';
|
||||
@@ -49,6 +50,7 @@ Chart.register(
|
||||
SubTitle,
|
||||
Filler,
|
||||
zoomPlugin,
|
||||
gradient,
|
||||
);
|
||||
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
@@ -61,9 +63,17 @@ const alpha = (hex, a) => {
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
};
|
||||
|
||||
const colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560'];
|
||||
const colors = {
|
||||
blue: '#008FFB',
|
||||
green: '#00E396',
|
||||
yellow: '#FEB019',
|
||||
red: '#FF4560',
|
||||
purple: '#e300db',
|
||||
orange: '#fe6919',
|
||||
};
|
||||
const colorSets = [colors.blue, colors.green, colors.yellow, colors.red, colors.purple];
|
||||
const getColor = (i) => {
|
||||
return colors[i % colors.length];
|
||||
return colorSets[i % colorSets.length];
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
@@ -95,6 +105,11 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
bar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
aspectRatio: {
|
||||
type: Number,
|
||||
required: false,
|
||||
@@ -186,22 +201,37 @@ export default defineComponent({
|
||||
// フォントカラー
|
||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
const maxes = data.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
|
||||
|
||||
chartInstance = new Chart(chartEl.value, {
|
||||
type: 'line',
|
||||
type: props.bar ? 'bar' : 'line',
|
||||
data: {
|
||||
labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
|
||||
datasets: data.series.map((x, i) => ({
|
||||
parsing: false,
|
||||
label: x.name,
|
||||
data: x.data.slice().reverse(),
|
||||
tension: 0.3,
|
||||
pointRadius: 0,
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
borderWidth: props.bar ? 0 : 2,
|
||||
borderColor: x.color ? x.color : getColor(i),
|
||||
borderDash: x.borderDash || [],
|
||||
borderJoinStyle: 'round',
|
||||
backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
|
||||
borderRadius: props.bar ? 3 : undefined,
|
||||
backgroundColor: props.bar ? (x.color ? x.color : getColor(i)) : alpha(x.color ? x.color : getColor(i), 0.1),
|
||||
gradient: props.bar ? undefined : {
|
||||
backgroundColor: {
|
||||
axis: 'y',
|
||||
colors: {
|
||||
0: alpha(x.color ? x.color : getColor(i), 0),
|
||||
[maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.15),
|
||||
},
|
||||
},
|
||||
},
|
||||
barPercentage: 0.9,
|
||||
categoryPercentage: 0.9,
|
||||
fill: x.type === 'area',
|
||||
clip: 8,
|
||||
hidden: !!x.hidden,
|
||||
})),
|
||||
},
|
||||
@@ -210,7 +240,7 @@ export default defineComponent({
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
right: 8,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
@@ -218,6 +248,8 @@ export default defineComponent({
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
stacked: props.stacked,
|
||||
offset: false,
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: props.span === 'day' ? 'month' : 'day',
|
||||
@@ -228,6 +260,8 @@ export default defineComponent({
|
||||
},
|
||||
ticks: {
|
||||
display: props.detailed,
|
||||
maxRotation: 0,
|
||||
autoSkipPadding: 16,
|
||||
},
|
||||
adapters: {
|
||||
date: {
|
||||
@@ -239,12 +273,14 @@ export default defineComponent({
|
||||
y: {
|
||||
position: 'left',
|
||||
stacked: props.stacked,
|
||||
suggestedMax: 100,
|
||||
grid: {
|
||||
color: gridColor,
|
||||
borderColor: 'rgb(0, 0, 0, 0)',
|
||||
},
|
||||
ticks: {
|
||||
display: props.detailed,
|
||||
//mirror: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -275,7 +311,7 @@ export default defineComponent({
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
zoom: {
|
||||
zoom: props.detailed ? {
|
||||
pan: {
|
||||
enabled: true,
|
||||
},
|
||||
@@ -301,7 +337,8 @@ export default defineComponent({
|
||||
max: 'original',
|
||||
},
|
||||
}
|
||||
},
|
||||
} : undefined,
|
||||
gradient,
|
||||
},
|
||||
},
|
||||
plugins: [{
|
||||
@@ -332,31 +369,71 @@ export default defineComponent({
|
||||
// TODO
|
||||
};
|
||||
|
||||
const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => {
|
||||
const fetchFederationChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Instances',
|
||||
name: 'Sub',
|
||||
type: 'area',
|
||||
data: format(total
|
||||
? raw.instance.total
|
||||
: sum(raw.instance.inc, negate(raw.instance.dec))
|
||||
),
|
||||
data: format(raw.sub),
|
||||
color: colors.orange,
|
||||
}, {
|
||||
name: 'Pub',
|
||||
type: 'area',
|
||||
data: format(raw.pub),
|
||||
color: colors.purple,
|
||||
}, {
|
||||
name: 'Received',
|
||||
type: 'area',
|
||||
data: format(raw.inboxInstances),
|
||||
color: colors.blue,
|
||||
}, {
|
||||
name: 'Delivered',
|
||||
type: 'area',
|
||||
data: format(raw.deliveredInstances),
|
||||
color: colors.green,
|
||||
}, {
|
||||
name: 'Stalled',
|
||||
type: 'area',
|
||||
data: format(raw.stalled),
|
||||
color: colors.red,
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchApRequestChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/ap-request', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'In',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: format(raw.inboxReceived)
|
||||
}, {
|
||||
name: 'Out (succ)',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: format(raw.deliverSucceeded)
|
||||
}, {
|
||||
name: 'Out (fail)',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: format(raw.deliverFailed)
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const fetchNotesChart = async (type: string): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
borderDash: [5, 5],
|
||||
data: format(type == 'combined'
|
||||
? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
|
||||
: sum(raw[type].inc, negate(raw[type].dec))
|
||||
),
|
||||
color: '#888888',
|
||||
}, {
|
||||
name: 'Renotes',
|
||||
type: 'area',
|
||||
@@ -364,6 +441,7 @@ export default defineComponent({
|
||||
? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
|
||||
: raw[type].diffs.renote
|
||||
),
|
||||
color: colors.green,
|
||||
}, {
|
||||
name: 'Replies',
|
||||
type: 'area',
|
||||
@@ -371,6 +449,7 @@ export default defineComponent({
|
||||
? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
|
||||
: raw[type].diffs.reply
|
||||
),
|
||||
color: colors.yellow,
|
||||
}, {
|
||||
name: 'Normal',
|
||||
type: 'area',
|
||||
@@ -378,6 +457,15 @@ export default defineComponent({
|
||||
? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
|
||||
: raw[type].diffs.normal
|
||||
),
|
||||
color: colors.blue,
|
||||
}, {
|
||||
name: 'With file',
|
||||
type: 'area',
|
||||
data: format(type == 'combined'
|
||||
? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile)
|
||||
: raw[type].diffs.withFile
|
||||
),
|
||||
color: colors.purple,
|
||||
}],
|
||||
};
|
||||
};
|
||||
@@ -433,17 +521,50 @@ export default defineComponent({
|
||||
const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
data: format(sum(raw.local.users, raw.remote.users)),
|
||||
}, {
|
||||
name: 'Local',
|
||||
name: 'Read & Write',
|
||||
type: 'area',
|
||||
data: format(raw.local.users),
|
||||
data: format(raw.readWrite),
|
||||
color: colors.orange,
|
||||
}, {
|
||||
name: 'Remote',
|
||||
name: 'Write',
|
||||
type: 'area',
|
||||
data: format(raw.remote.users),
|
||||
data: format(raw.write),
|
||||
color: colors.blue,
|
||||
}, {
|
||||
name: 'Read',
|
||||
type: 'area',
|
||||
data: format(raw.read),
|
||||
color: '#888888',
|
||||
}, {
|
||||
name: '< Week',
|
||||
type: 'area',
|
||||
data: format(raw.registeredWithinWeek),
|
||||
color: colors.green,
|
||||
}, {
|
||||
name: '< Month',
|
||||
type: 'area',
|
||||
data: format(raw.registeredWithinMonth),
|
||||
color: colors.yellow,
|
||||
}, {
|
||||
name: '< Year',
|
||||
type: 'area',
|
||||
data: format(raw.registeredWithinYear),
|
||||
color: colors.red,
|
||||
}, {
|
||||
name: '> Week',
|
||||
type: 'area',
|
||||
data: format(raw.registeredOutsideWeek),
|
||||
color: colors.yellow,
|
||||
}, {
|
||||
name: '> Month',
|
||||
type: 'area',
|
||||
data: format(raw.registeredOutsideMonth),
|
||||
color: colors.red,
|
||||
}, {
|
||||
name: '> Year',
|
||||
type: 'area',
|
||||
data: format(raw.registeredOutsideYear),
|
||||
color: colors.purple,
|
||||
}],
|
||||
};
|
||||
};
|
||||
@@ -484,26 +605,6 @@ export default defineComponent({
|
||||
};
|
||||
};
|
||||
|
||||
const fetchDriveTotalChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
data: format(sum(raw.local.totalSize, raw.remote.totalSize)),
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
data: format(raw.local.totalSize),
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
data: format(raw.remote.totalSize),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchDriveFilesChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
@@ -539,25 +640,6 @@ export default defineComponent({
|
||||
};
|
||||
};
|
||||
|
||||
const fetchDriveFilesTotalChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
data: format(sum(raw.local.totalCount, raw.remote.totalCount)),
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
data: format(raw.local.totalCount),
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
data: format(raw.remote.totalCount),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||
return {
|
||||
@@ -670,20 +752,43 @@ export default defineComponent({
|
||||
series: [...(props.args.withoutAll ? [] : [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
borderDash: [5, 5],
|
||||
data: format(sum(raw.inc, negate(raw.dec))),
|
||||
color: '#888888',
|
||||
}]), {
|
||||
name: 'With file',
|
||||
type: 'area',
|
||||
data: format(raw.diffs.withFile),
|
||||
color: colors.purple,
|
||||
}, {
|
||||
name: 'Renotes',
|
||||
type: 'area',
|
||||
data: format(raw.diffs.renote),
|
||||
color: colors.green,
|
||||
}, {
|
||||
name: 'Replies',
|
||||
type: 'area',
|
||||
data: format(raw.diffs.reply),
|
||||
color: colors.yellow,
|
||||
}, {
|
||||
name: 'Normal',
|
||||
type: 'area',
|
||||
data: format(raw.diffs.normal),
|
||||
color: colors.blue,
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchPerUserDriveChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Inc',
|
||||
type: 'area',
|
||||
data: format(raw.incSize),
|
||||
}, {
|
||||
name: 'Dec',
|
||||
type: 'area',
|
||||
data: format(raw.decSize),
|
||||
}],
|
||||
};
|
||||
};
|
||||
@@ -691,8 +796,8 @@ export default defineComponent({
|
||||
const fetchAndRender = async () => {
|
||||
const fetchData = () => {
|
||||
switch (props.src) {
|
||||
case 'federation-instances': return fetchFederationInstancesChart(false);
|
||||
case 'federation-instances-total': return fetchFederationInstancesChart(true);
|
||||
case 'federation': return fetchFederationChart();
|
||||
case 'ap-request': return fetchApRequestChart();
|
||||
case 'users': return fetchUsersChart(false);
|
||||
case 'users-total': return fetchUsersChart(true);
|
||||
case 'active-users': return fetchActiveUsersChart();
|
||||
@@ -701,9 +806,7 @@ export default defineComponent({
|
||||
case 'remote-notes': return fetchNotesChart('remote');
|
||||
case 'notes-total': return fetchNotesTotalChart();
|
||||
case 'drive': return fetchDriveChart();
|
||||
case 'drive-total': return fetchDriveTotalChart();
|
||||
case 'drive-files': return fetchDriveFilesChart();
|
||||
case 'drive-files-total': return fetchDriveFilesTotalChart();
|
||||
|
||||
case 'instance-requests': return fetchInstanceRequestsChart();
|
||||
case 'instance-users': return fetchInstanceUsersChart(false);
|
||||
@@ -718,6 +821,7 @@ export default defineComponent({
|
||||
case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);
|
||||
|
||||
case 'per-user-notes': return fetchPerUserNotesChart();
|
||||
case 'per-user-drive': return fetchPerUserDriveChart();
|
||||
}
|
||||
};
|
||||
fetching.value = true;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()">
|
||||
<div ref="emojis" class="emojis">
|
||||
<section class="result">
|
||||
@@ -81,7 +81,7 @@ import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import Ripple from '@/components/ripple.vue';
|
||||
import * as os from '@/os';
|
||||
import { isTouchUsing } from '@/scripts/touch';
|
||||
import { isMobile } from '@/scripts/is-mobile';
|
||||
import { deviceKind } from '@/scripts/device-kind';
|
||||
import { emojiCategories, instance } from '@/instance';
|
||||
import XSection from './emoji-picker.section.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
@@ -105,15 +105,16 @@ const emojis = ref<HTMLDivElement>();
|
||||
|
||||
const {
|
||||
reactions: pinned,
|
||||
reactionPickerSize,
|
||||
reactionPickerWidth,
|
||||
reactionPickerHeight,
|
||||
disableShowingAnimatedImages,
|
||||
recentlyUsedEmojis,
|
||||
} = defaultStore.reactiveState;
|
||||
|
||||
const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1);
|
||||
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
|
||||
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
|
||||
const big = props.asReactionPicker ? isTouchUsing : false;
|
||||
const customEmojiCategories = emojiCategories;
|
||||
const customEmojis = instance.emojis;
|
||||
const q = ref<string | null>(null);
|
||||
@@ -263,7 +264,7 @@ watch(q, () => {
|
||||
});
|
||||
|
||||
function focus() {
|
||||
if (!isMobile && !isTouchUsing) {
|
||||
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
|
||||
search.value?.focus({
|
||||
preventScroll: true
|
||||
});
|
||||
@@ -345,13 +346,20 @@ defineExpose({
|
||||
<style lang="scss" scoped>
|
||||
.omfetrab {
|
||||
$pad: 8px;
|
||||
--eachSize: 40px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.big {
|
||||
--eachSize: 44px;
|
||||
&.s1 {
|
||||
--eachSize: 40px;
|
||||
}
|
||||
|
||||
&.s2 {
|
||||
--eachSize: 45px;
|
||||
}
|
||||
|
||||
&.s3 {
|
||||
--eachSize: 50px;
|
||||
}
|
||||
|
||||
&.w1 {
|
||||
@@ -369,6 +377,16 @@ defineExpose({
|
||||
--columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
&.w4 {
|
||||
width: calc((var(--eachSize) * 8) + (#{$pad} * 2));
|
||||
--columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
&.w5 {
|
||||
width: calc((var(--eachSize) * 9) + (#{$pad} * 2));
|
||||
--columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
&.h1 {
|
||||
height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
|
||||
}
|
||||
@@ -381,6 +399,10 @@ defineExpose({
|
||||
height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
|
||||
}
|
||||
|
||||
&.h4 {
|
||||
height: calc((var(--eachSize) * 10) + (#{$pad} * 2));
|
||||
}
|
||||
|
||||
&.asDrawer {
|
||||
width: 100% !important;
|
||||
|
||||
|
@@ -7,6 +7,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { deviceKind } from '@/scripts/device-kind';
|
||||
import { defineComponent, inject, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -35,7 +36,7 @@ export default defineComponent({
|
||||
const margin = ref(0);
|
||||
const shouldSpacerMin = inject('shouldSpacerMin', false);
|
||||
const adjust = (rect: { width: number; height: number; }) => {
|
||||
if (shouldSpacerMin) {
|
||||
if (shouldSpacerMin || deviceKind === 'smartphone') {
|
||||
margin.value = props.marginMin;
|
||||
return;
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@
|
||||
<div class="selects" style="display: flex;">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<optgroup :label="$ts.federation">
|
||||
<option value="federation-instances">{{ $ts._charts.federationInstancesIncDec }}</option>
|
||||
<option value="federation-instances-total">{{ $ts._charts.federationInstancesTotal }}</option>
|
||||
<option value="federation">{{ $ts._charts.federation }}</option>
|
||||
<option value="ap-request">{{ $ts._charts.apRequest }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.users">
|
||||
<option value="users">{{ $ts._charts.usersIncDec }}</option>
|
||||
@@ -19,9 +19,7 @@
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.drive">
|
||||
<option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
|
||||
<option value="drive-files-total">{{ $ts._charts.filesTotal }}</option>
|
||||
<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
|
||||
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
||||
@@ -61,7 +59,7 @@ export default defineComponent({
|
||||
|
||||
setup() {
|
||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = ref('notes');
|
||||
const chartSrc = ref('active-users');
|
||||
|
||||
return {
|
||||
chartSrc,
|
||||
|
@@ -7,15 +7,27 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { instanceName } from '@/config';
|
||||
|
||||
const props = defineProps<{
|
||||
instance: any; // TODO
|
||||
instance?: {
|
||||
faviconUrl?: string
|
||||
name: string
|
||||
themeColor?: string
|
||||
}
|
||||
}>();
|
||||
|
||||
const themeColor = props.instance.themeColor || '#777777';
|
||||
// if no instance data is given, this is for the local instance
|
||||
const instance = props.instance ?? {
|
||||
faviconUrl: '/favicon.ico',
|
||||
name: instanceName,
|
||||
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
|
||||
};
|
||||
|
||||
const themeColor = instance.themeColor ?? '#777777';
|
||||
|
||||
const bg = {
|
||||
background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
|
||||
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@@ -154,11 +154,13 @@ const props = defineProps<{
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
||||
const note = $ref(JSON.parse(JSON.stringify(props.note)));
|
||||
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
note.renote != null &&
|
||||
note.text == null &&
|
||||
note.fileIds.length === 0 &&
|
||||
note.poll == null
|
||||
);
|
||||
|
||||
const el = ref<HTMLElement>();
|
||||
@@ -166,8 +168,8 @@ const menuButton = ref<HTMLElement>();
|
||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||
const renoteTime = ref<HTMLElement>();
|
||||
const reactButton = ref<HTMLElement>();
|
||||
let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note);
|
||||
const isMyRenote = $i && ($i.id === props.note.userId);
|
||||
let appearNote = $ref(isRenote ? note.renote as misskey.entities.Note : note);
|
||||
const isMyRenote = $i && ($i.id === note.userId);
|
||||
const showContent = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||
@@ -188,8 +190,9 @@ const keymap = {
|
||||
};
|
||||
|
||||
useNoteCapture({
|
||||
appearNote: $$(appearNote),
|
||||
rootEl: el,
|
||||
note: $$(appearNote),
|
||||
isDeletedRef: isDeleted,
|
||||
});
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
@@ -237,12 +240,12 @@ function onContextmenu(ev: MouseEvent): void {
|
||||
ev.preventDefault();
|
||||
react();
|
||||
} else {
|
||||
os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
|
||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton }), ev).then(focus);
|
||||
}
|
||||
}
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, {
|
||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
|
||||
viaKeyboard
|
||||
}).then(focus);
|
||||
}
|
||||
@@ -255,7 +258,7 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.api('notes/delete', {
|
||||
noteId: props.note.id
|
||||
noteId: note.id
|
||||
});
|
||||
isDeleted.value = true;
|
||||
}
|
||||
|
@@ -138,11 +138,13 @@ const props = defineProps<{
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
||||
const note = $ref(JSON.parse(JSON.stringify(props.note)));
|
||||
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
note.renote != null &&
|
||||
note.text == null &&
|
||||
note.fileIds.length === 0 &&
|
||||
note.poll == null
|
||||
);
|
||||
|
||||
const el = ref<HTMLElement>();
|
||||
@@ -150,8 +152,8 @@ const menuButton = ref<HTMLElement>();
|
||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||
const renoteTime = ref<HTMLElement>();
|
||||
const reactButton = ref<HTMLElement>();
|
||||
let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note);
|
||||
const isMyRenote = $i && ($i.id === props.note.userId);
|
||||
let appearNote = $ref(isRenote ? note.renote as misskey.entities.Note : note);
|
||||
const isMyRenote = $i && ($i.id === note.userId);
|
||||
const showContent = ref(false);
|
||||
const collapsed = ref(appearNote.cw == null && appearNote.text != null && (
|
||||
(appearNote.text.split('\n').length > 9) ||
|
||||
@@ -176,8 +178,9 @@ const keymap = {
|
||||
};
|
||||
|
||||
useNoteCapture({
|
||||
appearNote: $$(appearNote),
|
||||
rootEl: el,
|
||||
note: $$(appearNote),
|
||||
isDeletedRef: isDeleted,
|
||||
});
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
@@ -225,12 +228,12 @@ function onContextmenu(ev: MouseEvent): void {
|
||||
ev.preventDefault();
|
||||
react();
|
||||
} else {
|
||||
os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
|
||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton }), ev).then(focus);
|
||||
}
|
||||
}
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, {
|
||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
|
||||
viaKeyboard
|
||||
}).then(focus);
|
||||
}
|
||||
@@ -243,7 +246,7 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.api('notes/delete', {
|
||||
noteId: props.note.id
|
||||
noteId: note.id
|
||||
});
|
||||
isDeleted.value = true;
|
||||
}
|
||||
|
@@ -341,7 +341,10 @@ function addTag(tag: string) {
|
||||
}
|
||||
|
||||
function focus() {
|
||||
textareaEl.focus();
|
||||
if (textareaEl) {
|
||||
textareaEl.focus();
|
||||
textareaEl.setSelectionRange(textareaEl.value.length, textareaEl.value.length);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseFileFrom(ev) {
|
||||
|
@@ -14,6 +14,7 @@ import { nextTick, onMounted, computed, ref, watch, provide } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import { isTouchUsing } from '@/scripts/touch';
|
||||
import { defaultStore } from '@/store';
|
||||
import { deviceKind } from '@/scripts/device-kind';
|
||||
|
||||
function getFixedContainer(el: Element | null): Element | null {
|
||||
if (el == null || el.tagName === 'BODY') return null;
|
||||
@@ -62,7 +63,7 @@ const content = ref<HTMLElement>();
|
||||
const zIndex = os.claimZIndex(props.zPriority);
|
||||
const type = computed(() => {
|
||||
if (props.preferType === 'auto') {
|
||||
if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) {
|
||||
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
||||
return 'drawer';
|
||||
} else {
|
||||
return props.src != null ? 'popup' : 'dialog';
|
||||
@@ -101,7 +102,6 @@ const align = () => {
|
||||
if (type.value === 'drawer') return;
|
||||
|
||||
const popover = content.value!;
|
||||
|
||||
if (popover == null) return;
|
||||
|
||||
const rect = props.src.getBoundingClientRect();
|
||||
@@ -130,20 +130,23 @@ const align = () => {
|
||||
left = window.innerWidth - width;
|
||||
}
|
||||
|
||||
const underSpace = (window.innerHeight - MARGIN) - top;
|
||||
const upperSpace = (rect.top - MARGIN);
|
||||
|
||||
// 画面から縦にはみ出る場合
|
||||
if (top + height > (window.innerHeight - MARGIN)) {
|
||||
if (props.noOverlap) {
|
||||
const underSpace = (window.innerHeight - MARGIN) - top;
|
||||
const upperSpace = (rect.top - MARGIN);
|
||||
if (underSpace >= (upperSpace / 3)) {
|
||||
maxHeight.value = underSpace;
|
||||
maxHeight.value = underSpace;
|
||||
} else {
|
||||
maxHeight.value = upperSpace;
|
||||
maxHeight.value = upperSpace;
|
||||
top = (upperSpace + MARGIN) - height;
|
||||
}
|
||||
} else {
|
||||
top = (window.innerHeight - MARGIN) - height;
|
||||
}
|
||||
} else {
|
||||
maxHeight.value = underSpace;
|
||||
}
|
||||
} else {
|
||||
// 画面から横にはみ出る場合
|
||||
@@ -151,20 +154,23 @@ const align = () => {
|
||||
left = window.innerWidth - width + window.pageXOffset - 1;
|
||||
}
|
||||
|
||||
const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
|
||||
const upperSpace = (rect.top - MARGIN);
|
||||
|
||||
// 画面から縦にはみ出る場合
|
||||
if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
|
||||
if (props.noOverlap) {
|
||||
const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
|
||||
const upperSpace = (rect.top - MARGIN);
|
||||
if (underSpace >= (upperSpace / 3)) {
|
||||
maxHeight.value = underSpace;
|
||||
maxHeight.value = underSpace;
|
||||
} else {
|
||||
maxHeight.value = upperSpace;
|
||||
maxHeight.value = upperSpace;
|
||||
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
|
||||
}
|
||||
} else {
|
||||
top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
|
||||
}
|
||||
} else {
|
||||
maxHeight.value = underSpace;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -17,8 +17,12 @@ const props = withDefaults(defineProps<{
|
||||
y?: number;
|
||||
text?: string;
|
||||
maxWidth?: number;
|
||||
direction?: 'top' | 'bottom' | 'right' | 'left';
|
||||
innerMargin?: number;
|
||||
}>(), {
|
||||
maxWidth: 250,
|
||||
direction: 'top',
|
||||
innerMargin: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -34,49 +38,154 @@ const setPosition = () => {
|
||||
const contentWidth = el.value.offsetWidth;
|
||||
const contentHeight = el.value.offsetHeight;
|
||||
|
||||
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);
|
||||
const calcPosWhenTop = () => {
|
||||
let left: number;
|
||||
let top: number;
|
||||
|
||||
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
||||
}
|
||||
|
||||
// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
|
||||
if (top - window.pageYOffset < 0) {
|
||||
if (props.targetElement) {
|
||||
top = rect.top + window.pageYOffset + props.targetElement.offsetHeight;
|
||||
el.value.style.transformOrigin = 'center top';
|
||||
left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
|
||||
top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
|
||||
} else {
|
||||
left = props.x;
|
||||
top = (props.y - contentHeight) - props.innerMargin;
|
||||
}
|
||||
|
||||
left -= (el.value.offsetWidth / 2);
|
||||
|
||||
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
}
|
||||
|
||||
const calcPosWhenBottom = () => {
|
||||
let left: number;
|
||||
let top: number;
|
||||
|
||||
if (props.targetElement) {
|
||||
left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
|
||||
top = (rect.top + window.pageYOffset + props.targetElement.offsetHeight) + props.innerMargin;
|
||||
} else {
|
||||
left = props.x;
|
||||
top = (props.y) + props.innerMargin;
|
||||
}
|
||||
|
||||
left -= (el.value.offsetWidth / 2);
|
||||
|
||||
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
}
|
||||
|
||||
const calcPosWhenLeft = () => {
|
||||
let left: number;
|
||||
let top: number;
|
||||
|
||||
if (props.targetElement) {
|
||||
left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
|
||||
top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
|
||||
} else {
|
||||
left = (props.x - contentWidth) - props.innerMargin;
|
||||
top = props.y;
|
||||
}
|
||||
|
||||
top -= (el.value.offsetHeight / 2);
|
||||
|
||||
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
}
|
||||
|
||||
const calcPosWhenRight = () => {
|
||||
let left: number;
|
||||
let top: number;
|
||||
|
||||
if (props.targetElement) {
|
||||
left = (rect.left + window.pageXOffset) + props.innerMargin;
|
||||
top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
|
||||
} else {
|
||||
left = props.x + props.innerMargin;
|
||||
top = props.y;
|
||||
}
|
||||
|
||||
top -= (el.value.offsetHeight / 2);
|
||||
|
||||
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
}
|
||||
|
||||
const calc = (): {
|
||||
left: number;
|
||||
top: number;
|
||||
transformOrigin: string;
|
||||
} => {
|
||||
switch (props.direction) {
|
||||
case 'top': {
|
||||
const [left, top] = calcPosWhenTop();
|
||||
|
||||
// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
|
||||
if (top - window.pageYOffset < 0) {
|
||||
const [left, top] = calcPosWhenBottom();
|
||||
return { left, top, transformOrigin: 'center top' };
|
||||
}
|
||||
|
||||
return { left, top, transformOrigin: 'center bottom' };
|
||||
}
|
||||
|
||||
case 'bottom': {
|
||||
const [left, top] = calcPosWhenBottom();
|
||||
// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
|
||||
return { left, top, transformOrigin: 'center top' };
|
||||
}
|
||||
|
||||
case 'left': {
|
||||
const [left, top] = calcPosWhenLeft();
|
||||
|
||||
// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
|
||||
if (left - window.pageXOffset < 0) {
|
||||
const [left, top] = calcPosWhenRight();
|
||||
return { left, top, transformOrigin: 'left center' };
|
||||
}
|
||||
|
||||
return { left, top, transformOrigin: 'right center' };
|
||||
}
|
||||
|
||||
case 'right': {
|
||||
const [left, top] = calcPosWhenRight();
|
||||
// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
|
||||
return { left, top, transformOrigin: 'left center' };
|
||||
}
|
||||
}
|
||||
|
||||
return null as never;
|
||||
}
|
||||
|
||||
const { left, top, transformOrigin } = calc();
|
||||
el.value.style.transformOrigin = transformOrigin;
|
||||
el.value.style.left = left + 'px';
|
||||
el.value.style.top = top + 'px';
|
||||
};
|
||||
|
||||
let loopHandler;
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setPosition();
|
||||
|
||||
let loopHandler;
|
||||
|
||||
const loop = () => {
|
||||
loopHandler = window.requestAnimationFrame(() => {
|
||||
setPosition();
|
||||
@@ -85,12 +194,12 @@ onMounted(() => {
|
||||
};
|
||||
|
||||
loop();
|
||||
|
||||
onUnmounted(() => {
|
||||
window.cancelAnimationFrame(loopHandler);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.cancelAnimationFrame(loopHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -1,34 +1,55 @@
|
||||
import { Directive } from 'vue';
|
||||
|
||||
const mountings = new Map<Element, {
|
||||
resize: ResizeObserver;
|
||||
intersection?: IntersectionObserver;
|
||||
fn: (w: number, h: number) => void;
|
||||
}>();
|
||||
|
||||
function calc(src: Element) {
|
||||
const info = mountings.get(src);
|
||||
const height = src.clientHeight;
|
||||
const width = src.clientWidth;
|
||||
|
||||
if (!info) return;
|
||||
|
||||
// アクティベート前などでsrcが描画されていない場合
|
||||
if (!height) {
|
||||
// IntersectionObserverで表示検出する
|
||||
if (!info.intersection) {
|
||||
info.intersection = new IntersectionObserver(entries => {
|
||||
if (entries.some(entry => entry.isIntersecting)) calc(src);
|
||||
});
|
||||
}
|
||||
info.intersection.observe(src);
|
||||
return;
|
||||
}
|
||||
if (info.intersection) {
|
||||
info.intersection.disconnect()
|
||||
delete info.intersection;
|
||||
};
|
||||
|
||||
info.fn(width, height);
|
||||
};
|
||||
|
||||
export default {
|
||||
mounted(src, binding, vn) {
|
||||
const calc = () => {
|
||||
const height = src.clientHeight;
|
||||
const width = src.clientWidth;
|
||||
|
||||
// 要素が(一時的に)DOMに存在しないときは計算スキップ
|
||||
if (height === 0) return;
|
||||
|
||||
binding.value(width, height);
|
||||
};
|
||||
|
||||
calc();
|
||||
|
||||
// Vue3では使えなくなった
|
||||
// 無くても大丈夫か...?
|
||||
// TODO: ↑大丈夫じゃなかったので解決策を探す
|
||||
//vn.context.$on('hook:activated', calc);
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
calc();
|
||||
const resize = new ResizeObserver((entries, observer) => {
|
||||
calc(src);
|
||||
});
|
||||
ro.observe(src);
|
||||
resize.observe(src);
|
||||
|
||||
src._get_size_ro_ = ro;
|
||||
mountings.set(src, { resize, fn: binding.value, });
|
||||
calc(src);
|
||||
},
|
||||
|
||||
unmounted(src, binding, vn) {
|
||||
binding.value(0, 0);
|
||||
src._get_size_ro_.unobserve(src);
|
||||
const info = mountings.get(src);
|
||||
if (!info) return;
|
||||
info.resize.disconnect();
|
||||
if (info.intersection) info.intersection.disconnect();
|
||||
mountings.delete(src);
|
||||
}
|
||||
} as Directive;
|
||||
} as Directive<Element, (w: number, h: number) => void>;
|
||||
|
@@ -1,68 +1,107 @@
|
||||
import { Directive } from 'vue';
|
||||
|
||||
type Value = { max?: number[]; min?: number[]; };
|
||||
|
||||
//const observers = new Map<Element, ResizeObserver>();
|
||||
const mountings = new Map<Element, {
|
||||
value: Value;
|
||||
resize: ResizeObserver;
|
||||
intersection?: IntersectionObserver;
|
||||
previousWidth: number;
|
||||
}>();
|
||||
|
||||
type ClassOrder = {
|
||||
add: string[];
|
||||
remove: string[];
|
||||
};
|
||||
|
||||
const cache = new Map<string, ClassOrder>();
|
||||
|
||||
function getClassOrder(width: number, queue: Value): ClassOrder {
|
||||
const getMaxClass = (v: number) => `max-width_${v}px`;
|
||||
const getMinClass = (v: number) => `min-width_${v}px`;
|
||||
|
||||
return {
|
||||
add: [
|
||||
...(queue.max ? queue.max.filter(v => width <= v).map(getMaxClass) : []),
|
||||
...(queue.min ? queue.min.filter(v => width >= v).map(getMinClass) : []),
|
||||
],
|
||||
remove: [
|
||||
...(queue.max ? queue.max.filter(v => width > v).map(getMaxClass) : []),
|
||||
...(queue.min ? queue.min.filter(v => width < v).map(getMinClass) : []),
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function applyClassOrder(el: Element, order: ClassOrder) {
|
||||
el.classList.add(...order.add);
|
||||
el.classList.remove(...order.remove);
|
||||
}
|
||||
|
||||
function getOrderName(width: number, queue: Value): string {
|
||||
return `${width}|${queue.max ? queue.max.join(',') : ''}|${queue.min ? queue.min.join(',') : ''}`;
|
||||
}
|
||||
|
||||
function calc(el: Element) {
|
||||
const info = mountings.get(el);
|
||||
const width = el.clientWidth;
|
||||
|
||||
if (!info || info.previousWidth === width) return;
|
||||
|
||||
// アクティベート前などでsrcが描画されていない場合
|
||||
if (!width) {
|
||||
// IntersectionObserverで表示検出する
|
||||
if (!info.intersection) {
|
||||
info.intersection = new IntersectionObserver(entries => {
|
||||
if (entries.some(entry => entry.isIntersecting)) calc(el);
|
||||
});
|
||||
}
|
||||
info.intersection.observe(el);
|
||||
return;
|
||||
}
|
||||
if (info.intersection) {
|
||||
info.intersection.disconnect()
|
||||
delete info.intersection;
|
||||
};
|
||||
|
||||
mountings.set(el, Object.assign(info, { previousWidth: width }));
|
||||
|
||||
const cached = cache.get(getOrderName(width, info.value));
|
||||
if (cached) {
|
||||
applyClassOrder(el, cached);
|
||||
} else {
|
||||
const order = getClassOrder(width, info.value);
|
||||
cache.set(getOrderName(width, info.value), order);
|
||||
applyClassOrder(el, order);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
mounted(src, binding, vn) {
|
||||
const query = binding.value;
|
||||
const resize = new ResizeObserver((entries, observer) => {
|
||||
calc(src);
|
||||
});
|
||||
|
||||
const addClass = (el: Element, cls: string) => {
|
||||
el.classList.add(cls);
|
||||
};
|
||||
mountings.set(src, {
|
||||
value: binding.value,
|
||||
resize,
|
||||
previousWidth: 0,
|
||||
});
|
||||
|
||||
const removeClass = (el: Element, cls: string) => {
|
||||
el.classList.remove(cls);
|
||||
};
|
||||
calc(src);
|
||||
resize.observe(src);
|
||||
},
|
||||
|
||||
const calc = () => {
|
||||
const width = src.clientWidth;
|
||||
|
||||
// 要素が(一時的に)DOMに存在しないときは計算スキップ
|
||||
if (width === 0) return;
|
||||
|
||||
if (query.max) {
|
||||
for (const v of query.max) {
|
||||
if (width <= v) {
|
||||
addClass(src, 'max-width_' + v + 'px');
|
||||
} else {
|
||||
removeClass(src, 'max-width_' + v + 'px');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (query.min) {
|
||||
for (const v of query.min) {
|
||||
if (width >= v) {
|
||||
addClass(src, 'min-width_' + v + 'px');
|
||||
} else {
|
||||
removeClass(src, 'min-width_' + v + 'px');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
calc();
|
||||
|
||||
window.addEventListener('resize', calc);
|
||||
|
||||
// Vue3では使えなくなった
|
||||
// 無くても大丈夫か...?
|
||||
// TODO: ↑大丈夫じゃなかったので解決策を探す
|
||||
//vn.context.$on('hook:activated', calc);
|
||||
|
||||
//const ro = new ResizeObserver((entries, observer) => {
|
||||
// calc();
|
||||
//});
|
||||
|
||||
//ro.observe(el);
|
||||
|
||||
// TODO: 新たにプロパティを作るのをやめMapを使う
|
||||
// ただメモリ的には↓の方が省メモリかもしれないので検討中
|
||||
//el._ro_ = ro;
|
||||
src._calc_ = calc;
|
||||
updated(src, binding, vn) {
|
||||
mountings.set(src, Object.assign({}, mountings.get(src), { value: binding.value }));
|
||||
calc(src);
|
||||
},
|
||||
|
||||
unmounted(src, binding, vn) {
|
||||
//el._ro_.unobserve(el);
|
||||
window.removeEventListener('resize', src._calc_);
|
||||
const info = mountings.get(src);
|
||||
if (!info) return;
|
||||
info.resize.disconnect();
|
||||
if (info.intersection) info.intersection.disconnect();
|
||||
mountings.delete(src);
|
||||
}
|
||||
} as Directive;
|
||||
} as Directive<Element, Value>;
|
||||
|
@@ -32,7 +32,7 @@ import { defaultStore, ColdDeviceStorage } from '@/store';
|
||||
import { fetchInstance, instance } from '@/instance';
|
||||
import { makeHotkey } from '@/scripts/hotkey';
|
||||
import { search } from '@/scripts/search';
|
||||
import { isMobile } from '@/scripts/is-mobile';
|
||||
import { deviceKind } from '@/scripts/device-kind';
|
||||
import { initializeSw } from '@/scripts/initialize-sw';
|
||||
import { reloadChannel } from '@/scripts/unison-reload';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
||||
@@ -92,11 +92,10 @@ window.addEventListener('resize', () => {
|
||||
//#endregion
|
||||
|
||||
// If mobile, insert the viewport meta tag
|
||||
if (isMobile || window.innerWidth <= 1024) {
|
||||
if (['smartphone', 'tablet'].includes(deviceKind)) {
|
||||
const viewport = document.getElementsByName('viewport').item(0);
|
||||
viewport.setAttribute('content',
|
||||
`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
|
||||
document.head.appendChild(viewport);
|
||||
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
|
||||
}
|
||||
|
||||
//#region Set lang attr
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="600" :margin-min="20">
|
||||
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
|
||||
<div class="_formRoot">
|
||||
<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
|
||||
<div class="content">
|
||||
@@ -65,35 +65,50 @@
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
|
||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { version, instanceName } from '@/config';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import MkInstanceStats from '@/components/instance-stats.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import * as symbols from '@/symbols';
|
||||
import { host } from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const stats = ref(null);
|
||||
let stats = $ref(null);
|
||||
let tab = $ref('overview');
|
||||
|
||||
const initStats = () => os.api('stats', {
|
||||
}).then((res) => {
|
||||
stats.value = res;
|
||||
stats = res;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.instanceInfo,
|
||||
icon: 'fas fa-info-circle',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
tabs: [{
|
||||
active: tab === 'overview',
|
||||
title: i18n.ts.overview,
|
||||
onClick: () => { tab = 'overview'; },
|
||||
}, {
|
||||
active: tab === 'charts',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'fas fa-chart-bar',
|
||||
onClick: () => { tab = 'charts'; },
|
||||
},],
|
||||
})),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -28,7 +28,7 @@
|
||||
<template #label>MIME type</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<MkPagination v-slot="{items}" ref="files" :pagination="pagination" class="urempief">
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief">
|
||||
<button v-for="file in items" :key="file.id" class="file _panel _button _gap" @click="show(file, $event)">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<div class="body">
|
||||
@@ -54,8 +54,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
@@ -65,80 +65,63 @@ import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkSelect,
|
||||
MkPagination,
|
||||
MkContainer,
|
||||
MkDriveFileThumbnail,
|
||||
},
|
||||
let q = $ref(null);
|
||||
let origin = $ref('local');
|
||||
let type = $ref(null);
|
||||
let searchHost = $ref('');
|
||||
const pagination = {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
type: (type && type !== '') ? type : null,
|
||||
origin: origin,
|
||||
hostname: (searchHost && searchHost !== '') ? searchHost : null,
|
||||
})),
|
||||
};
|
||||
|
||||
emits: ['info'],
|
||||
function clear() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.clearCachedFilesConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.files,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
text: this.$ts.clearCachedFiles,
|
||||
icon: 'fas fa-trash-alt',
|
||||
handler: this.clear
|
||||
}]
|
||||
},
|
||||
q: null,
|
||||
origin: 'local',
|
||||
type: null,
|
||||
searchHost: '',
|
||||
pagination: {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
type: (this.type && this.type !== '') ? this.type : null,
|
||||
origin: this.origin,
|
||||
hostname: (this.searchHost && this.searchHost !== '') ? this.searchHost : null,
|
||||
})),
|
||||
},
|
||||
os.apiWithDialog('admin/drive/clean-remote-files', {});
|
||||
});
|
||||
}
|
||||
|
||||
function show(file) {
|
||||
os.popup(import('./file-dialog.vue'), {
|
||||
fileId: file.id
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
function find() {
|
||||
os.api('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
|
||||
show(file);
|
||||
}).catch(err => {
|
||||
if (err.code === 'NO_SUCH_FILE') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.notFound
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
clear() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$ts.clearCachedFilesConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('admin/drive/clean-remote-files', {});
|
||||
});
|
||||
},
|
||||
|
||||
show(file, ev) {
|
||||
os.popup(import('./file-dialog.vue'), {
|
||||
fileId: file.id
|
||||
}, {}, 'closed');
|
||||
},
|
||||
|
||||
find() {
|
||||
os.api('admin/drive/show-file', this.q.startsWith('http://') || this.q.startsWith('https://') ? { url: this.q.trim() } : { fileId: this.q.trim() }).then(file => {
|
||||
this.show(file);
|
||||
}).catch(e => {
|
||||
if (e.code === 'NO_SUCH_FILE') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: this.$ts.notFound
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
bytes
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.files,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
text: i18n.ts.clearCachedFiles,
|
||||
icon: 'fas fa-trash-alt',
|
||||
handler: clear,
|
||||
}],
|
||||
})),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -25,6 +25,12 @@
|
||||
<template #label>{{ $ts.backgroundImageUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="themeColor" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-palette"></i></template>
|
||||
<template #label>{{ $ts.themeColor }}</template>
|
||||
<template #caption>#RRGGBB</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="tosUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ $ts.tosUrl }}</template>
|
||||
@@ -179,6 +185,7 @@ export default defineComponent({
|
||||
iconUrl: null,
|
||||
bannerUrl: null,
|
||||
backgroundImageUrl: null,
|
||||
themeColor: null,
|
||||
maxNoteTextLength: 0,
|
||||
enableLocalTimeline: false,
|
||||
enableGlobalTimeline: false,
|
||||
@@ -206,6 +213,7 @@ export default defineComponent({
|
||||
this.iconUrl = meta.iconUrl;
|
||||
this.bannerUrl = meta.bannerUrl;
|
||||
this.backgroundImageUrl = meta.backgroundImageUrl;
|
||||
this.themeColor = meta.themeColor;
|
||||
this.maintainerName = meta.maintainerName;
|
||||
this.maintainerEmail = meta.maintainerEmail;
|
||||
this.maxNoteTextLength = meta.maxNoteTextLength;
|
||||
@@ -233,6 +241,7 @@ export default defineComponent({
|
||||
iconUrl: this.iconUrl,
|
||||
bannerUrl: this.bannerUrl,
|
||||
backgroundImageUrl: this.backgroundImageUrl,
|
||||
themeColor: this.themeColor === '' ? null : this.themeColor,
|
||||
maintainerName: this.maintainerName,
|
||||
maintainerEmail: this.maintainerEmail,
|
||||
maxNoteTextLength: this.maxNoteTextLength,
|
||||
|
@@ -36,7 +36,7 @@
|
||||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="users" :pagination="pagination" class="users">
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
|
||||
<button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)">
|
||||
<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
@@ -61,9 +61,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
@@ -71,94 +70,79 @@ import { acct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { lookupUser } from '@/scripts/lookup-user';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkSelect,
|
||||
MkPagination,
|
||||
},
|
||||
let paginationComponent = $ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
emits: ['info'],
|
||||
let sort = $ref('+createdAt');
|
||||
let state = $ref('all');
|
||||
let origin = $ref('local');
|
||||
let searchUsername = $ref('');
|
||||
let searchHost = $ref('');
|
||||
const pagination = {
|
||||
endpoint: 'admin/show-users' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
sort: sort,
|
||||
state: state,
|
||||
origin: origin,
|
||||
username: searchUsername,
|
||||
hostname: searchHost,
|
||||
})),
|
||||
offsetMode: true
|
||||
};
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.users,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-search',
|
||||
text: this.$ts.search,
|
||||
handler: this.searchUser
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: this.$ts.addUser,
|
||||
handler: this.addUser
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-search',
|
||||
text: this.$ts.lookup,
|
||||
handler: this.lookupUser
|
||||
}],
|
||||
},
|
||||
sort: '+createdAt',
|
||||
state: 'all',
|
||||
origin: 'local',
|
||||
searchUsername: '',
|
||||
searchHost: '',
|
||||
pagination: {
|
||||
endpoint: 'admin/show-users' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
sort: this.sort,
|
||||
state: this.state,
|
||||
origin: this.origin,
|
||||
username: this.searchUsername,
|
||||
hostname: this.searchHost,
|
||||
})),
|
||||
offsetMode: true
|
||||
},
|
||||
}
|
||||
},
|
||||
function searchUser() {
|
||||
os.selectUser().then(user => {
|
||||
show(user);
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
lookupUser,
|
||||
async function addUser() {
|
||||
const { canceled: canceled1, result: username } = await os.inputText({
|
||||
title: i18n.ts.username,
|
||||
});
|
||||
if (canceled1) return;
|
||||
|
||||
searchUser() {
|
||||
os.selectUser().then(user => {
|
||||
this.show(user);
|
||||
});
|
||||
},
|
||||
const { canceled: canceled2, result: password } = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
async addUser() {
|
||||
const { canceled: canceled1, result: username } = await os.inputText({
|
||||
title: this.$ts.username,
|
||||
});
|
||||
if (canceled1) return;
|
||||
os.apiWithDialog('admin/accounts/create', {
|
||||
username: username,
|
||||
password: password,
|
||||
}).then(res => {
|
||||
paginationComponent.reload();
|
||||
});
|
||||
}
|
||||
|
||||
const { canceled: canceled2, result: password } = await os.inputText({
|
||||
title: this.$ts.password,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled2) return;
|
||||
function show(user) {
|
||||
os.pageWindow(`/user-info/${user.id}`);
|
||||
}
|
||||
|
||||
os.apiWithDialog('admin/accounts/create', {
|
||||
username: username,
|
||||
password: password,
|
||||
}).then(res => {
|
||||
this.$refs.users.reload();
|
||||
});
|
||||
},
|
||||
|
||||
show(user) {
|
||||
os.pageWindow(`/user-info/${user.id}`);
|
||||
},
|
||||
|
||||
acct
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.users,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-search',
|
||||
text: i18n.ts.search,
|
||||
handler: searchUser
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.addUser,
|
||||
handler: addUser
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-search',
|
||||
text: i18n.ts.lookup,
|
||||
handler: lookupUser
|
||||
}],
|
||||
})),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -19,7 +19,7 @@
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.statistics }}</template>
|
||||
<div ref="chart"></div>
|
||||
<MkChart src="per-user-drive" :args="{ user: $i }" span="day" :limit="7 * 5" :bar="true" :stacked="true" :detailed="false" :aspect-ratio="6"/>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
@@ -45,8 +45,7 @@ import * as os from '@/os';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as symbols from '@/symbols';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
// TODO: render chart
|
||||
import MkChart from '@/components/chart.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -55,6 +54,7 @@ export default defineComponent({
|
||||
FormSection,
|
||||
MkKeyValue,
|
||||
FormSplit,
|
||||
MkChart,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
@@ -12,6 +12,14 @@
|
||||
</template>
|
||||
</FormSelect>
|
||||
|
||||
<FormRadios v-model="overridedDeviceKind" class="_formBlock">
|
||||
<template #label>{{ $ts.overridedDeviceKind }}</template>
|
||||
<option :value="null">{{ $ts.auto }}</option>
|
||||
<option value="smartphone"><i class="fas fa-mobile-alt"/> {{ $ts.smartphone }}</option>
|
||||
<option value="tablet"><i class="fas fa-tablet-alt"/> {{ $ts.tablet }}</option>
|
||||
<option value="desktop"><i class="fas fa-desktop"/> {{ $ts.desktop }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormSwitch v-model="showFixedPostForm" class="_formBlock">{{ $ts.showFixedPostForm }}</FormSwitch>
|
||||
|
||||
<FormSection>
|
||||
@@ -127,6 +135,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
computed: {
|
||||
overridedDeviceKind: defaultStore.makeGetterSetter('overridedDeviceKind'),
|
||||
serverDisconnectedBehavior: defaultStore.makeGetterSetter('serverDisconnectedBehavior'),
|
||||
reduceAnimation: defaultStore.makeGetterSetter('animation', v => !v, v => !v),
|
||||
useBlurEffectForModal: defaultStore.makeGetterSetter('useBlurEffectForModal'),
|
||||
@@ -193,6 +202,10 @@ export default defineComponent({
|
||||
instanceTicker() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
overridedDeviceKind() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<template #caption>{{ $ts.makeReactionsPublicDescription }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSelect v-model="ffVisibility" class="_formBlock">
|
||||
<FormSelect v-model="ffVisibility" class="_formBlock" @update:modelValue="save()">
|
||||
<template #label>{{ $ts.ffVisibility }}</template>
|
||||
<option value="public">{{ $ts._ffVisibility.public }}</option>
|
||||
<option value="followers">{{ $ts._ffVisibility.followers }}</option>
|
||||
|
@@ -38,7 +38,7 @@
|
||||
</FormSlot>
|
||||
|
||||
<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
|
||||
|
||||
<FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }}</template></FormSwitch>
|
||||
<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
|
||||
|
||||
<FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.ts.alwaysMarkSensitive }}</FormSwitch>
|
||||
@@ -68,6 +68,7 @@ const profile = reactive({
|
||||
lang: $i.lang,
|
||||
isBot: $i.isBot,
|
||||
isCat: $i.isCat,
|
||||
showTimelineReplies: $i.showTimelineReplies,
|
||||
alwaysMarkNsfw: $i.alwaysMarkNsfw,
|
||||
});
|
||||
|
||||
@@ -97,6 +98,7 @@ function save() {
|
||||
lang: profile.lang || null,
|
||||
isBot: !!profile.isBot,
|
||||
isCat: !!profile.isCat,
|
||||
showTimelineReplies: !!profile.showTimelineReplies,
|
||||
alwaysMarkNsfw: !!profile.alwaysMarkNsfw,
|
||||
});
|
||||
}
|
||||
|
@@ -17,17 +17,26 @@
|
||||
<template #caption>{{ $ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ $ts.preview }}</button></template>
|
||||
</FromSlot>
|
||||
|
||||
<FormRadios v-model="reactionPickerWidth" class="_formBlock">
|
||||
<template #label>{{ $ts.width }}</template>
|
||||
<FormRadios v-model="reactionPickerSize" class="_formBlock">
|
||||
<template #label>{{ $ts.size }}</template>
|
||||
<option :value="1">{{ $ts.small }}</option>
|
||||
<option :value="2">{{ $ts.medium }}</option>
|
||||
<option :value="3">{{ $ts.large }}</option>
|
||||
</FormRadios>
|
||||
<FormRadios v-model="reactionPickerWidth" class="_formBlock">
|
||||
<template #label>{{ $ts.numberOfColumn }}</template>
|
||||
<option :value="1">5</option>
|
||||
<option :value="2">6</option>
|
||||
<option :value="3">7</option>
|
||||
<option :value="4">8</option>
|
||||
<option :value="5">9</option>
|
||||
</FormRadios>
|
||||
<FormRadios v-model="reactionPickerHeight" class="_formBlock">
|
||||
<template #label>{{ $ts.height }}</template>
|
||||
<option :value="1">{{ $ts.small }}</option>
|
||||
<option :value="2">{{ $ts.medium }}</option>
|
||||
<option :value="3">{{ $ts.large }}</option>
|
||||
<option :value="4">{{ $ts.large }}+</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormSwitch v-model="reactionPickerUseDrawerForMobile" class="_formBlock">
|
||||
@@ -60,6 +69,7 @@ import { i18n } from '@/i18n';
|
||||
|
||||
let reactions = $ref(JSON.parse(JSON.stringify(defaultStore.state.reactions)));
|
||||
|
||||
const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize'));
|
||||
const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
|
||||
const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight'));
|
||||
const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile'));
|
||||
|
@@ -81,18 +81,67 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.softMutedWords = this.$store.state.mutedWords.map(x => x.join(' ')).join('\n');
|
||||
this.hardMutedWords = this.$i.mutedWords.map(x => x.join(' ')).join('\n');
|
||||
const render = (mutedWords) => mutedWords.map(x => {
|
||||
if (Array.isArray(x)) {
|
||||
return x.join(' ');
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}).join('\n');
|
||||
|
||||
this.softMutedWords = render(this.$store.state.mutedWords);
|
||||
this.hardMutedWords = render(this.$i.mutedWords);
|
||||
|
||||
this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
|
||||
},
|
||||
|
||||
methods: {
|
||||
async save() {
|
||||
this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')));
|
||||
const parseMutes = (mutes, tab) => {
|
||||
// split into lines, remove empty lines and unnecessary whitespace
|
||||
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line != '');
|
||||
|
||||
// check each line if it is a RegExp or not
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const regexp = line.match(/^\/(.+)\/(.*)$/);
|
||||
if (regexp) {
|
||||
// check that the RegExp is valid
|
||||
try {
|
||||
new RegExp(regexp[1], regexp[2]);
|
||||
// note that regex lines will not be split by spaces!
|
||||
} catch (err) {
|
||||
// invalid syntax: do not save, do not reset changed flag
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: this.$ts.regexpError,
|
||||
text: this.$t('regexpErrorDescription', { tab, line: i + 1 }) + "\n" + err.toString()
|
||||
});
|
||||
// re-throw error so these invalid settings are not saved
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
lines[i] = line.split(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
let softMutes, hardMutes;
|
||||
try {
|
||||
softMutes = parseMutes(this.softMutedWords, this.$ts._wordMute.soft);
|
||||
hardMutes = parseMutes(this.hardMutedWords, this.$ts._wordMute.hard);
|
||||
} catch (err) {
|
||||
// already displayed error message in parseMutes
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.set('mutedWords', softMutes);
|
||||
await os.api('i/update', {
|
||||
mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
|
||||
mutedWords: hardMutes,
|
||||
});
|
||||
|
||||
this.changed = false;
|
||||
},
|
||||
|
||||
|
@@ -46,8 +46,10 @@ const keymap = {
|
||||
const tlComponent = $ref<InstanceType<typeof XTimeline>>();
|
||||
const rootEl = $ref<HTMLElement>();
|
||||
|
||||
let src = $ref<'home' | 'local' | 'social' | 'global'>(defaultStore.state.tl.src);
|
||||
let queue = $ref(0);
|
||||
const src = $computed(() => defaultStore.reactiveState.tl.value.src);
|
||||
|
||||
watch ($$(src), () => queue = 0);
|
||||
|
||||
function queueUpdated(q: number): void {
|
||||
queue = q;
|
||||
@@ -60,7 +62,7 @@ function top(): void {
|
||||
async function chooseList(ev: MouseEvent): Promise<void> {
|
||||
const lists = await os.api('users/lists/list');
|
||||
const items = lists.map(list => ({
|
||||
type: 'link',
|
||||
type: 'link' as const,
|
||||
text: list.name,
|
||||
to: `/timeline/list/${list.id}`,
|
||||
}));
|
||||
@@ -70,7 +72,7 @@ async function chooseList(ev: MouseEvent): Promise<void> {
|
||||
async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
||||
const antennas = await os.api('antennas/list');
|
||||
const items = antennas.map(antenna => ({
|
||||
type: 'link',
|
||||
type: 'link' as const,
|
||||
text: antenna.name,
|
||||
indicate: antenna.hasUnreadNote,
|
||||
to: `/timeline/antenna/${antenna.id}`,
|
||||
@@ -81,7 +83,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
||||
async function chooseChannel(ev: MouseEvent): Promise<void> {
|
||||
const channels = await os.api('channels/followed');
|
||||
const items = channels.map(channel => ({
|
||||
type: 'link',
|
||||
type: 'link' as const,
|
||||
text: channel.name,
|
||||
indicate: channel.hasUnreadNote,
|
||||
to: `/channels/${channel.id}`,
|
||||
@@ -89,9 +91,10 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
|
||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function saveSrc(): void {
|
||||
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global'): void {
|
||||
defaultStore.set('tl', {
|
||||
src: src,
|
||||
...defaultStore.state.tl,
|
||||
src: newSrc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,25 +138,25 @@ defineExpose({
|
||||
title: i18n.ts._timelines.home,
|
||||
icon: 'fas fa-home',
|
||||
iconOnly: true,
|
||||
onClick: () => { src = 'home'; saveSrc(); },
|
||||
onClick: () => { saveSrc('home'); },
|
||||
}, ...(isLocalTimelineAvailable ? [{
|
||||
active: src === 'local',
|
||||
title: i18n.ts._timelines.local,
|
||||
icon: 'fas fa-comments',
|
||||
iconOnly: true,
|
||||
onClick: () => { src = 'local'; saveSrc(); },
|
||||
onClick: () => { saveSrc('local'); },
|
||||
}, {
|
||||
active: src === 'social',
|
||||
title: i18n.ts._timelines.social,
|
||||
icon: 'fas fa-share-alt',
|
||||
iconOnly: true,
|
||||
onClick: () => { src = 'social'; saveSrc(); },
|
||||
onClick: () => { saveSrc('social'); },
|
||||
}] : []), ...(isGlobalTimelineAvailable ? [{
|
||||
active: src === 'global',
|
||||
title: i18n.ts._timelines.global,
|
||||
icon: 'fas fa-globe',
|
||||
iconOnly: true,
|
||||
onClick: () => { src = 'global'; saveSrc(); },
|
||||
onClick: () => { saveSrc('global'); },
|
||||
}] : [])],
|
||||
})),
|
||||
});
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<template #header><i class="fas fa-chart-bar" style="margin-right: 0.5em;"></i>{{ $ts.activity }}</template>
|
||||
|
||||
<div style="padding: 8px;">
|
||||
<MkChart src="per-user-notes" :args="{ user, withoutAll: true }" span="day" :limit="limit" :stacked="true" :detailed="false" :aspect-ratio="6"/>
|
||||
<MkChart src="per-user-notes" :args="{ user, withoutAll: true }" span="day" :limit="limit" :bar="true" :stacked="true" :detailed="false" :aspect-ratio="5"/>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
@@ -18,6 +18,6 @@ const props = withDefaults(defineProps<{
|
||||
user: misskey.entities.User;
|
||||
limit?: number;
|
||||
}>(), {
|
||||
limit: 40,
|
||||
limit: 50,
|
||||
});
|
||||
</script>
|
||||
|
@@ -1,23 +1,32 @@
|
||||
export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): boolean {
|
||||
export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||
// 自分自身
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
|
||||
const words = mutedWords
|
||||
// Clean up
|
||||
.map(xs => xs.filter(x => x !== ''))
|
||||
.filter(xs => xs.length > 0);
|
||||
|
||||
if (words.length > 0) {
|
||||
if (mutedWords.length > 0) {
|
||||
if (note.text == null) return false;
|
||||
|
||||
const matched = words.some(and =>
|
||||
and.every(keyword => {
|
||||
const regexp = keyword.match(/^\/(.+)\/(.*)$/);
|
||||
if (regexp) {
|
||||
const matched = mutedWords.some(filter => {
|
||||
if (Array.isArray(filter)) {
|
||||
// Clean up
|
||||
const filteredFilter = filter.filter(keyword => keyword !== '');
|
||||
if (filteredFilter.length === 0) return false;
|
||||
|
||||
return filteredFilter.every(keyword => note.text!.includes(keyword));
|
||||
} else {
|
||||
// represents RegExp
|
||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||
|
||||
// This should never happen due to input sanitisation.
|
||||
if (!regexp) return false;
|
||||
|
||||
try {
|
||||
return new RegExp(regexp[1], regexp[2]).test(note.text!);
|
||||
} catch (err) {
|
||||
// This should never happen due to input sanitisation.
|
||||
return false;
|
||||
}
|
||||
return note.text!.includes(keyword);
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
if (matched) return true;
|
||||
}
|
||||
|
10
packages/client/src/scripts/device-kind.ts
Normal file
10
packages/client/src/scripts/device-kind.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
|
||||
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
|
||||
|
||||
export const deviceKind = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind
|
||||
: isSmartphone ? 'smartphone'
|
||||
: isTablet ? 'tablet'
|
||||
: 'desktop';
|
@@ -1,2 +0,0 @@
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
export const isMobile = /mobile|iphone|ipad|android/.test(ua);
|
@@ -20,6 +20,7 @@ export const builtinThemes = [
|
||||
require('@/themes/l-apricot.json5'),
|
||||
require('@/themes/l-rainy.json5'),
|
||||
require('@/themes/l-vivid.json5'),
|
||||
require('@/themes/l-cherry.json5'),
|
||||
require('@/themes/l-sushi.json5'),
|
||||
|
||||
require('@/themes/d-dark.json5'),
|
||||
@@ -27,6 +28,7 @@ export const builtinThemes = [
|
||||
require('@/themes/d-astro.json5'),
|
||||
require('@/themes/d-future.json5'),
|
||||
require('@/themes/d-botanical.json5'),
|
||||
require('@/themes/d-cherry.json5'),
|
||||
require('@/themes/d-pumpkin.json5'),
|
||||
require('@/themes/d-black.json5'),
|
||||
] as Theme[];
|
||||
|
@@ -5,34 +5,35 @@ import { $i } from '@/account';
|
||||
|
||||
export function useNoteCapture(props: {
|
||||
rootEl: Ref<HTMLElement>;
|
||||
appearNote: Ref<misskey.entities.Note>;
|
||||
note: Ref<misskey.entities.Note>;
|
||||
isDeletedRef: Ref<boolean>;
|
||||
}) {
|
||||
const appearNote = props.appearNote;
|
||||
const note = props.note;
|
||||
const connection = $i ? stream : null;
|
||||
|
||||
function onStreamNoteUpdated(data): void {
|
||||
const { type, id, body } = data;
|
||||
|
||||
if (id !== appearNote.value.id) return;
|
||||
if (id !== note.value.id) return;
|
||||
|
||||
switch (type) {
|
||||
case 'reacted': {
|
||||
const reaction = body.reaction;
|
||||
|
||||
if (body.emoji) {
|
||||
const emojis = appearNote.value.emojis || [];
|
||||
const emojis = note.value.emojis || [];
|
||||
if (!emojis.includes(body.emoji)) {
|
||||
appearNote.value.emojis = [...emojis, body.emoji];
|
||||
note.value.emojis = [...emojis, body.emoji];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
|
||||
const currentCount = (appearNote.value.reactions || {})[reaction] || 0;
|
||||
const currentCount = (note.value.reactions || {})[reaction] || 0;
|
||||
|
||||
appearNote.value.reactions[reaction] = currentCount + 1;
|
||||
note.value.reactions[reaction] = currentCount + 1;
|
||||
|
||||
if ($i && (body.userId === $i.id)) {
|
||||
appearNote.value.myReaction = reaction;
|
||||
note.value.myReaction = reaction;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -41,12 +42,12 @@ export function useNoteCapture(props: {
|
||||
const reaction = body.reaction;
|
||||
|
||||
// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
|
||||
const currentCount = (appearNote.value.reactions || {})[reaction] || 0;
|
||||
const currentCount = (note.value.reactions || {})[reaction] || 0;
|
||||
|
||||
appearNote.value.reactions[reaction] = Math.max(0, currentCount - 1);
|
||||
note.value.reactions[reaction] = Math.max(0, currentCount - 1);
|
||||
|
||||
if ($i && (body.userId === $i.id)) {
|
||||
appearNote.value.myReaction = null;
|
||||
note.value.myReaction = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -54,7 +55,7 @@ export function useNoteCapture(props: {
|
||||
case 'pollVoted': {
|
||||
const choice = body.choice;
|
||||
|
||||
const choices = [...appearNote.value.poll.choices];
|
||||
const choices = [...note.value.poll.choices];
|
||||
choices[choice] = {
|
||||
...choices[choice],
|
||||
votes: choices[choice].votes + 1,
|
||||
@@ -63,12 +64,12 @@ export function useNoteCapture(props: {
|
||||
} : {})
|
||||
};
|
||||
|
||||
appearNote.value.poll.choices = choices;
|
||||
note.value.poll.choices = choices;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'deleted': {
|
||||
appearNote.value.deletedAt = new Date();
|
||||
props.isDeletedRef.value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +78,7 @@ export function useNoteCapture(props: {
|
||||
function capture(withHandler = false): void {
|
||||
if (connection) {
|
||||
// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
|
||||
connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: appearNote.value.id });
|
||||
connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id });
|
||||
if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +86,7 @@ export function useNoteCapture(props: {
|
||||
function decapture(withHandler = false): void {
|
||||
if (connection) {
|
||||
connection.send('un', {
|
||||
id: appearNote.value.id,
|
||||
id: note.value.id,
|
||||
});
|
||||
if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
|
||||
}
|
||||
|
@@ -106,6 +106,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
}
|
||||
},
|
||||
|
||||
overridedDeviceKind: {
|
||||
where: 'device',
|
||||
default: null as null | 'smartphone' | 'tablet' | 'desktop',
|
||||
},
|
||||
serverDisconnectedBehavior: {
|
||||
where: 'device',
|
||||
default: 'quiet' as 'quiet' | 'reload' | 'dialog'
|
||||
@@ -178,6 +182,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: 'remote' as 'none' | 'remote' | 'always'
|
||||
},
|
||||
reactionPickerSize: {
|
||||
where: 'device',
|
||||
default: 1
|
||||
},
|
||||
reactionPickerWidth: {
|
||||
where: 'device',
|
||||
default: 1
|
||||
|
20
packages/client/src/themes/d-cherry.json5
Normal file
20
packages/client/src/themes/d-cherry.json5
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
id: '679b3b87-a4e9-4789-8696-b56c15cc33b0',
|
||||
|
||||
name: 'Mi Cherry Dark',
|
||||
author: 'syuilo',
|
||||
|
||||
base: 'dark',
|
||||
|
||||
props: {
|
||||
accent: 'rgb(255, 89, 117)',
|
||||
bg: 'rgb(28, 28, 37)',
|
||||
fg: 'rgb(236, 239, 244)',
|
||||
panel: 'rgb(35, 35, 47)',
|
||||
renote: '@accent',
|
||||
link: '@accent',
|
||||
mention: '@accent',
|
||||
hashtag: '@accent',
|
||||
divider: 'rgb(63, 63, 80)',
|
||||
},
|
||||
}
|
21
packages/client/src/themes/l-cherry.json5
Normal file
21
packages/client/src/themes/l-cherry.json5
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
id: 'ac168876-f737-4074-a3fc-a370c732ef48',
|
||||
|
||||
name: 'Mi Cherry Light',
|
||||
author: 'syuilo',
|
||||
|
||||
base: 'light',
|
||||
|
||||
props: {
|
||||
accent: 'rgb(219, 96, 114)',
|
||||
bg: 'rgb(254, 248, 249)',
|
||||
fg: 'rgb(152, 13, 26)',
|
||||
panel: 'rgb(255, 255, 255)',
|
||||
renote: '@accent',
|
||||
link: 'rgb(156, 187, 5)',
|
||||
mention: '@accent',
|
||||
hashtag: '@accent',
|
||||
divider: 'rgba(134, 51, 51, 0.1)',
|
||||
inputBorderHover: 'rgb(238, 221, 222)',
|
||||
},
|
||||
}
|
@@ -305,7 +305,7 @@ export default defineComponent({
|
||||
|
||||
&.post:before {
|
||||
width: calc(100% - 28px);
|
||||
height: min-content;
|
||||
height: auto;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
@@ -276,7 +276,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
> * {
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
@@ -340,13 +340,14 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
|
||||
z-index: 1000;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 16px;
|
||||
padding: 16px 16px calc(env(safe-area-inset-bottom, 0px) + 16px) 16px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
-webkit-backdrop-filter: var(--blur, blur(32px));
|
||||
backdrop-filter: var(--blur, blur(32px));
|
||||
background-color: var(--header);
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .button {
|
||||
position: relative;
|
||||
@@ -392,7 +393,7 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
|
||||
}
|
||||
|
||||
> * {
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user