Merge branch 'notification-read-api' into swn

This commit is contained in:
tamaina
2022-02-19 16:21:21 +09:00
515 changed files with 12078 additions and 10359 deletions

View File

@@ -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"
}

View File

@@ -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">

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>;

View File

@@ -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>;

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>

View File

@@ -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'],

View File

@@ -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: {

View File

@@ -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>

View File

@@ -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,
});
}

View File

@@ -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'));

View File

@@ -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;
},

View File

@@ -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'); },
}] : [])],
})),
});

View File

@@ -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>

View File

@@ -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;
}

View 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';

View File

@@ -1,2 +0,0 @@
const ua = navigator.userAgent.toLowerCase();
export const isMobile = /mobile|iphone|ipad|android/.test(ua);

View File

@@ -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[];

View File

@@ -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);
}

View File

@@ -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

View 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)',
},
}

View 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)',
},
}

View File

@@ -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%;
}

View File

@@ -276,7 +276,7 @@ export default defineComponent({
}
> * {
font-size: 22px;
font-size: 20px;
}
&:disabled {

View File

@@ -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