Merge branch 'develop' into mkjs-n
This commit is contained in:
@@ -45,7 +45,7 @@ fs.readFile(
|
||||
micromatch(Array.from(modules), [
|
||||
'../../assets/**',
|
||||
'../../fluent-emojis/**',
|
||||
'../../locales/**',
|
||||
'../../locales/ja-JP.yml',
|
||||
'../../misskey-assets/**',
|
||||
'assets/**',
|
||||
'public/**',
|
||||
|
@@ -17,13 +17,13 @@
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@rollup/plugin-alias": "5.0.0",
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.13.2",
|
||||
"@tabler/icons-webfont": "2.17.0",
|
||||
"@vitejs/plugin-vue": "4.2.1",
|
||||
"@vue-macros/reactivity-transform": "^0.3.5",
|
||||
"@vue/compiler-sfc": "3.2.47",
|
||||
"@vitejs/plugin-vue": "4.2.2",
|
||||
"@vue-macros/reactivity-transform": "0.3.6",
|
||||
"@vue/compiler-sfc": "3.3.1",
|
||||
"autosize": "5.0.2",
|
||||
"blurhash": "2.0.5",
|
||||
"broadcast-channel": "4.20.2",
|
||||
@@ -34,14 +34,14 @@
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"chromatic": "6.17.3",
|
||||
"compare-versions": "5.0.1",
|
||||
"chromatic": "6.17.4",
|
||||
"compare-versions": "5.0.3",
|
||||
"cropperjs": "2.0.0-beta.2",
|
||||
"date-fns": "2.30.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eventemitter3": "5.0.1",
|
||||
"gsap": "3.11.5",
|
||||
"idb-keyval": "6.2.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"is-file-animated": "1.0.2",
|
||||
"json5": "2.2.3",
|
||||
@@ -53,7 +53,7 @@
|
||||
"punycode": "2.3.0",
|
||||
"querystring": "0.2.1",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "3.21.3",
|
||||
"rollup": "3.21.6",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.10.0",
|
||||
"sass": "1.62.1",
|
||||
@@ -70,40 +70,40 @@
|
||||
"typescript": "5.0.4",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.3.4",
|
||||
"vue": "3.2.47",
|
||||
"vite": "4.3.5",
|
||||
"vue": "3.3.1",
|
||||
"vue-plyr": "7.0.0",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.0.7",
|
||||
"@storybook/addon-essentials": "7.0.7",
|
||||
"@storybook/addon-interactions": "7.0.7",
|
||||
"@storybook/addon-links": "7.0.7",
|
||||
"@storybook/addon-storysource": "7.0.7",
|
||||
"@storybook/addons": "7.0.7",
|
||||
"@storybook/blocks": "7.0.7",
|
||||
"@storybook/core-events": "7.0.7",
|
||||
"@storybook/addon-actions": "7.0.10",
|
||||
"@storybook/addon-essentials": "7.0.10",
|
||||
"@storybook/addon-interactions": "7.0.10",
|
||||
"@storybook/addon-links": "7.0.10",
|
||||
"@storybook/addon-storysource": "7.0.10",
|
||||
"@storybook/addons": "7.0.10",
|
||||
"@storybook/blocks": "7.0.10",
|
||||
"@storybook/core-events": "7.0.10",
|
||||
"@storybook/jest": "0.1.0",
|
||||
"@storybook/manager-api": "7.0.7",
|
||||
"@storybook/preview-api": "7.0.7",
|
||||
"@storybook/react": "7.0.7",
|
||||
"@storybook/react-vite": "7.0.7",
|
||||
"@storybook/manager-api": "7.0.10",
|
||||
"@storybook/preview-api": "7.0.10",
|
||||
"@storybook/react": "7.0.10",
|
||||
"@storybook/react-vite": "7.0.10",
|
||||
"@storybook/testing-library": "0.1.0",
|
||||
"@storybook/theming": "7.0.7",
|
||||
"@storybook/types": "7.0.7",
|
||||
"@storybook/vue3": "7.0.7",
|
||||
"@storybook/vue3-vite": "7.0.7",
|
||||
"@storybook/theming": "7.0.10",
|
||||
"@storybook/types": "7.0.10",
|
||||
"@storybook/vue3": "7.0.10",
|
||||
"@storybook/vue3-vite": "7.0.10",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/estree": "1.0.1",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/matter-js": "0.18.2",
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/matter-js": "0.18.3",
|
||||
"@types/micromatch": "4.0.2",
|
||||
"@types/node": "18.16.3",
|
||||
"@types/node": "20.1.3",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/seedrandom": "3.0.5",
|
||||
@@ -113,19 +113,19 @@
|
||||
"@types/uuid": "9.0.1",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.2",
|
||||
"@typescript-eslint/parser": "5.59.2",
|
||||
"@vitest/coverage-c8": "0.30.1",
|
||||
"@vue/runtime-core": "3.2.47",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"@vitest/coverage-c8": "0.31.0",
|
||||
"@vue/runtime-core": "3.3.1",
|
||||
"astring": "1.8.4",
|
||||
"chokidar-cli": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.11.0",
|
||||
"eslint": "8.39.0",
|
||||
"cypress": "12.12.0",
|
||||
"eslint": "8.40.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-vue": "9.11.0",
|
||||
"eslint-plugin-vue": "9.12.0",
|
||||
"fast-glob": "3.2.12",
|
||||
"happy-dom": "9.10.2",
|
||||
"happy-dom": "9.16.0",
|
||||
"micromatch": "3.1.10",
|
||||
"msw": "1.2.1",
|
||||
"msw-storybook-addon": "1.8.0",
|
||||
@@ -133,13 +133,13 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"storybook": "7.0.7",
|
||||
"storybook": "7.0.10",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.2",
|
||||
"vitest": "0.30.1",
|
||||
"vitest": "0.31.0",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.1.1",
|
||||
"vue-tsc": "1.6.3"
|
||||
"vue-eslint-parser": "9.2.1",
|
||||
"vue-tsc": "1.6.4"
|
||||
}
|
||||
}
|
||||
|
@@ -52,9 +52,12 @@
|
||||
|
||||
<MkFoldableSection class="item">
|
||||
<template #header>Retention rate</template>
|
||||
<div class="_panel" :class="$style.retention">
|
||||
<div class="_panel" :class="$style.retentionHeatmap">
|
||||
<MkRetentionHeatmap/>
|
||||
</div>
|
||||
<div class="_panel" :class="$style.retentionLine">
|
||||
<MkRetentionLineChart/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
|
||||
<MkFoldableSection class="item">
|
||||
@@ -86,6 +89,7 @@ import { i18n } from '@/i18n';
|
||||
import MkHeatmap from '@/components/MkHeatmap.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
||||
import { initChart } from '@/scripts/init-chart';
|
||||
|
||||
initChart();
|
||||
@@ -202,7 +206,12 @@ onMounted(() => {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.retention {
|
||||
.retentionHeatmap {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.retentionLine {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.children]: depth > 1 }]">
|
||||
<div :class="$style.main">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
|
||||
<div :class="$style.body">
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
@@ -62,6 +63,7 @@ if (props.detail) {
|
||||
.root {
|
||||
padding: 16px 32px;
|
||||
font-size: 0.9em;
|
||||
position: relative;
|
||||
|
||||
&.children {
|
||||
padding: 10px 0 0 16px;
|
||||
@@ -73,6 +75,16 @@ if (props.detail) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.colorBar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 5px;
|
||||
height: calc(100% - 8px);
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
|
@@ -40,7 +40,7 @@ async function renderChart() {
|
||||
|
||||
let raw = await os.api('retention', { });
|
||||
|
||||
raw = raw.slice(0, maxDays);
|
||||
raw = raw.slice(0, maxDays + 1);
|
||||
|
||||
const data = [];
|
||||
for (const record of raw) {
|
||||
@@ -90,8 +90,13 @@ async function renderChart() {
|
||||
borderRadius: 3,
|
||||
backgroundColor(c) {
|
||||
const value = c.dataset.data[c.dataIndex].v;
|
||||
const a = value / max(c.dataset.data[c.dataIndex].y);
|
||||
return alpha(color, a);
|
||||
const m = max(c.dataset.data[c.dataIndex].y);
|
||||
if (m === 0) {
|
||||
return alpha(color, 0);
|
||||
} else {
|
||||
const a = value / m;
|
||||
return alpha(color, a);
|
||||
}
|
||||
},
|
||||
fill: true,
|
||||
width(c) {
|
||||
@@ -129,6 +134,10 @@ async function renderChart() {
|
||||
autoSkip: false,
|
||||
callback: (value, index, values) => value,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Days later',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: 'time',
|
||||
@@ -166,7 +175,12 @@ async function renderChart() {
|
||||
},
|
||||
label(context) {
|
||||
const v = context.dataset.data[context.dataIndex];
|
||||
return [`Active: ${v.v} (${Math.round((v.v / max(v.y)) * 100)}%)`];
|
||||
const m = max(v.y);
|
||||
if (m === 0) {
|
||||
return [`Active: ${v.v} (-%)`];
|
||||
} else {
|
||||
return [`Active: ${v.v} (${Math.round((v.v / m) * 100)}%)`];
|
||||
}
|
||||
},
|
||||
},
|
||||
//mode: 'index',
|
||||
|
130
packages/frontend/src/components/MkRetentionLineChart.vue
Normal file
130
packages/frontend/src/components/MkRetentionLineChart.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<canvas ref="chartEl"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { defaultStore } from '@/store';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
import { chartVLine } from '@/scripts/chart-vline';
|
||||
import { alpha } from '@/scripts/color';
|
||||
import { initChart } from '@/scripts/init-chart';
|
||||
import * as os from '@/os';
|
||||
|
||||
initChart();
|
||||
|
||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
let chartInstance: Chart;
|
||||
|
||||
const getYYYYMMDD = (date: Date) => {
|
||||
const y = date.getFullYear().toString().padStart(2, '0');
|
||||
const m = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const d = date.getDate().toString().padStart(2, '0');
|
||||
return `${y}/${m}/${d}`;
|
||||
};
|
||||
|
||||
const getDate = (ymd: string) => {
|
||||
const [y, m, d] = ymd.split('-').map(x => parseInt(x, 10));
|
||||
const date = new Date(y, m + 1, d, 0, 0, 0, 0);
|
||||
return date;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
let raw = await os.api('retention', { });
|
||||
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
|
||||
const color = accent.toHex();
|
||||
|
||||
chartInstance = new Chart(chartEl.value, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: raw.map((record, i) => ({
|
||||
label: getYYYYMMDD(new Date(record.createdAt)),
|
||||
pointRadius: 0,
|
||||
borderWidth: 2,
|
||||
borderJoinStyle: 'round',
|
||||
borderColor: alpha(color, Math.min(1, (raw.length - (i - 1)) / raw.length)),
|
||||
fill: false,
|
||||
tension: 0.4,
|
||||
data: [{
|
||||
x: '0',
|
||||
y: 100,
|
||||
d: getYYYYMMDD(new Date(record.createdAt)),
|
||||
}, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
|
||||
x: (i + 1).toString(),
|
||||
y: (v / record.users) * 100,
|
||||
d: getYYYYMMDD(new Date(record.createdAt)),
|
||||
}))],
|
||||
})),
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Days later',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Rate (%)',
|
||||
},
|
||||
ticks: {
|
||||
callback: (value, index, values) => value + '%',
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
callbacks: {
|
||||
title(context) {
|
||||
const v = context[0].dataset.data[context[0].dataIndex];
|
||||
return `${v.x} days later`;
|
||||
},
|
||||
label(context) {
|
||||
const v = context.dataset.data[context.dataIndex];
|
||||
const p = Math.round(v.y) + '%';
|
||||
return `${v.d} ${p}`;
|
||||
},
|
||||
},
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [chartVLine(vLineColor)],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
@@ -40,10 +40,6 @@ import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done'): void;
|
||||
}>();
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
|
||||
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
|
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkUserSetupDialog_Privacy from './MkUserSetupDialog.Privacy.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog_Privacy,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog_Privacy v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog_Privacy>;
|
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
|
||||
<template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.hideOnlineStatus }}</template>
|
||||
<template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.noCrawle }}</template>
|
||||
<template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.preventAiLearning }}</template>
|
||||
<template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
||||
let isLocked = ref(false);
|
||||
let hideOnlineStatus = ref(false);
|
||||
let noCrawle = ref(false);
|
||||
let preventAiLearning = ref(true);
|
||||
|
||||
watch([isLocked, hideOnlineStatus, noCrawle, preventAiLearning], () => {
|
||||
os.api('i/update', {
|
||||
isLocked: !!isLocked.value,
|
||||
hideOnlineStatus: !!hideOnlineStatus.value,
|
||||
noCrawle: !!noCrawle.value,
|
||||
preventAiLearning: !!preventAiLearning.value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
@@ -37,10 +37,6 @@ import { chooseFileFromPc } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done'): void;
|
||||
}>();
|
||||
|
||||
const name = ref('');
|
||||
const description = ref('');
|
||||
|
||||
|
@@ -7,9 +7,17 @@
|
||||
@close="close(true)"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.initialAccountSetting }}</template>
|
||||
<template v-if="page === 1" #header>{{ i18n.ts._initialAccountSetting.profileSetting }}</template>
|
||||
<template v-else-if="page === 2" #header>{{ i18n.ts._initialAccountSetting.privacySetting }}</template>
|
||||
<template v-else-if="page === 3" #header>{{ i18n.ts.follow }}</template>
|
||||
<template v-else-if="page === 4" #header>{{ i18n.ts.pushNotification }}</template>
|
||||
<template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
|
||||
<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
|
||||
|
||||
<div style="overflow-x: clip;">
|
||||
<div :class="$style.progressBar">
|
||||
<div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div>
|
||||
</div>
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enter-active-class="$style.transition_x_enterActive"
|
||||
@@ -40,12 +48,22 @@
|
||||
<template v-else-if="page === 2">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<XFollow/>
|
||||
<XPrivacy/>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 3">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<XFollow/>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<MkButton primary rounded gradate style="margin: 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 4">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
@@ -58,7 +76,7 @@
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 4">
|
||||
<template v-else-if="page === 5">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
@@ -87,6 +105,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
|
||||
import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
|
||||
import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
import { host } from '@/config';
|
||||
@@ -134,6 +153,21 @@ async function close(skip: boolean) {
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.progressBarValue {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||
transition: all 0.5s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.centerPage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -142,4 +176,14 @@ async function close(skip: boolean) {
|
||||
padding-bottom: 30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pageFooter {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="lzyxtsnt">
|
||||
<ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
|
||||
<div>
|
||||
<ImgWithBlurhash v-if="image" style="max-width: 100%;" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,11 +17,3 @@ const props = defineProps<{
|
||||
|
||||
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lzyxtsnt {
|
||||
> img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -238,6 +238,7 @@ const patrons = [
|
||||
'ずも',
|
||||
'binvinyl',
|
||||
'渡志郎',
|
||||
'ぷーざ',
|
||||
];
|
||||
|
||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
@@ -46,7 +46,7 @@
|
||||
</MkInput>
|
||||
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
<MkNotes v-if="searchPagination" :key="searchQuery" :pagination="searchPagination"/>
|
||||
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
@@ -93,6 +93,7 @@ let channel = $ref(null);
|
||||
let favorited = $ref(false);
|
||||
let searchQuery = $ref('');
|
||||
let searchPagination = $ref();
|
||||
let searchKey = $ref('');
|
||||
const featuredPagination = $computed(() => ({
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
@@ -149,10 +150,12 @@ async function search() {
|
||||
endpoint: 'notes/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: searchQuery,
|
||||
query: query,
|
||||
channelId: channel.id,
|
||||
},
|
||||
};
|
||||
|
||||
searchKey = query;
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => {
|
||||
|
@@ -37,7 +37,7 @@ async function choose() {
|
||||
file = fileResponse[0];
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
fileId: fileResponse.id,
|
||||
fileId: file.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
98
packages/frontend/src/pages/search.note.vue
Normal file
98
packages/frontend/src/pages/search.note.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.options }}</template>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.specifyUser }}</template>
|
||||
<template v-if="user" #suffix>@{{ user.username }}</template>
|
||||
|
||||
<div style="text-align: center;" class="_gaps">
|
||||
<div v-if="user">@{{ user.username }}</div>
|
||||
<div>
|
||||
<MkButton v-if="user == null" primary rounded inline @click="selectUser">{{ i18n.ts.selectUser }}</MkButton>
|
||||
<MkButton v-else danger rounded inline @click="user = null">{{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</MkFolder>
|
||||
<div>
|
||||
<MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="notePagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkNotes :key="key" :pagination="notePagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { $i } from '@/account';
|
||||
import { instance } from '@/instance';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { useRouter } from '@/router';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let key = $ref(0);
|
||||
let searchQuery = $ref('');
|
||||
let searchOrigin = $ref('combined');
|
||||
let notePagination = $ref();
|
||||
let user = $ref(null);
|
||||
|
||||
function selectUser() {
|
||||
os.selectUser().then(_user => {
|
||||
user = _user;
|
||||
});
|
||||
}
|
||||
|
||||
async function search() {
|
||||
const query = searchQuery.toString().trim();
|
||||
|
||||
if (query == null || query === '') return;
|
||||
|
||||
if (query.startsWith('https://')) {
|
||||
const promise = os.api('ap/show', {
|
||||
uri: query,
|
||||
});
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
|
||||
const res = await promise;
|
||||
|
||||
if (res.type === 'User') {
|
||||
router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type === 'Note') {
|
||||
router.push(`/notes/${res.object.id}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
notePagination = {
|
||||
endpoint: 'notes/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: searchQuery,
|
||||
userId: user ? user.id : null,
|
||||
},
|
||||
};
|
||||
|
||||
key++;
|
||||
}
|
||||
</script>
|
77
packages/frontend/src/pages/search.user.vue
Normal file
77
packages/frontend/src/pages/search.user.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="searchOrigin" @update:model-value="search()">
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkRadios>
|
||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="userPagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkUserList :key="key" :pagination="userPagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted } from 'vue';
|
||||
import MkUserList from '@/components/MkUserList.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { $i } from '@/account';
|
||||
import { instance } from '@/instance';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { useRouter } from '@/router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let key = $ref('');
|
||||
let searchQuery = $ref('');
|
||||
let searchOrigin = $ref('combined');
|
||||
let userPagination = $ref();
|
||||
|
||||
async function search() {
|
||||
const query = searchQuery.toString().trim();
|
||||
|
||||
if (query == null || query === '') return;
|
||||
|
||||
if (query.startsWith('https://')) {
|
||||
const promise = os.api('ap/show', {
|
||||
uri: query,
|
||||
});
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
|
||||
const res = await promise;
|
||||
|
||||
if (res.type === 'User') {
|
||||
router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type === 'Note') {
|
||||
router.push(`/notes/${res.object.id}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
userPagination = {
|
||||
endpoint: 'users/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: searchQuery,
|
||||
origin: searchOrigin,
|
||||
},
|
||||
};
|
||||
|
||||
key = query;
|
||||
}
|
||||
</script>
|
@@ -1,133 +1,38 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="tab === 'note'" :content-max="800">
|
||||
<div v-if="notesSearchAvailable" class="_gaps">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="notePagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkNotes :key="key" :pagination="notePagination"/>
|
||||
</MkFoldableSection>
|
||||
<MkSpacer v-if="tab === 'note'" :content-max="800">
|
||||
<div v-if="notesSearchAvailable">
|
||||
<XNote/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'user'" :content-max="800">
|
||||
<div class="_gaps">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="searchOrigin" @update:model-value="search()">
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkRadios>
|
||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="userPagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkUserList :key="key" :pagination="userPagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<MkSpacer v-else-if="tab === 'user'" :content-max="800">
|
||||
<XUser/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkUserList from '@/components/MkUserList.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { computed, defineAsyncComponent, onMounted } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import * as os from '@/os';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { $i } from '@/account';
|
||||
import { instance } from '@/instance';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { useRouter } from '@/router';
|
||||
|
||||
const router = useRouter();
|
||||
const XNote = defineAsyncComponent(() => import('./search.note.vue'));
|
||||
const XUser = defineAsyncComponent(() => import('./search.user.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
query: string;
|
||||
channel?: string;
|
||||
type?: string;
|
||||
origin?: string;
|
||||
}>();
|
||||
|
||||
let key = $ref('');
|
||||
let tab = $ref('note');
|
||||
let searchQuery = $ref('');
|
||||
let searchOrigin = $ref('combined');
|
||||
let notePagination = $ref();
|
||||
let userPagination = $ref();
|
||||
|
||||
const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes));
|
||||
|
||||
onMounted(() => {
|
||||
tab = props.type ?? 'note';
|
||||
searchQuery = props.query ?? '';
|
||||
searchOrigin = props.origin ?? 'combined';
|
||||
});
|
||||
|
||||
async function search() {
|
||||
const query = searchQuery.toString().trim();
|
||||
|
||||
if (query == null || query === '') return;
|
||||
|
||||
if (query.startsWith('https://')) {
|
||||
const promise = os.api('ap/show', {
|
||||
uri: query,
|
||||
});
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
|
||||
const res = await promise;
|
||||
|
||||
if (res.type === 'User') {
|
||||
router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type === 'Note') {
|
||||
router.push(`/notes/${res.object.id}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab === 'note') {
|
||||
notePagination = {
|
||||
endpoint: 'notes/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: searchQuery,
|
||||
channelId: props.channel,
|
||||
},
|
||||
};
|
||||
} else if (tab === 'user') {
|
||||
userPagination = {
|
||||
endpoint: 'users/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: searchQuery,
|
||||
origin: searchOrigin,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
key = query;
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
@@ -141,7 +46,7 @@ const headerTabs = $computed(() => [{
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: searchQuery ? i18n.t('searchWith', { q: searchQuery }) : i18n.ts.search,
|
||||
title: i18n.ts.search,
|
||||
icon: 'ti ti-search',
|
||||
})));
|
||||
</script>
|
||||
|
@@ -24,9 +24,9 @@
|
||||
{{ i18n.ts.noCrawle }}
|
||||
<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="preventAiLarning" @update:model-value="save()">
|
||||
{{ i18n.ts.preventAiLarning }}<span class="_beta">{{ i18n.ts.beta }}</span>
|
||||
<template #caption>{{ i18n.ts.preventAiLarningDescription }}</template>
|
||||
<MkSwitch v-model="preventAiLearning" @update:model-value="save()">
|
||||
{{ i18n.ts.preventAiLearning }}<span class="_beta">{{ i18n.ts.beta }}</span>
|
||||
<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="isExplorable" @update:model-value="save()">
|
||||
{{ i18n.ts.makeExplorable }}
|
||||
@@ -75,7 +75,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
let isLocked = $ref($i.isLocked);
|
||||
let autoAcceptFollowed = $ref($i.autoAcceptFollowed);
|
||||
let noCrawle = $ref($i.noCrawle);
|
||||
let preventAiLarning = $ref($i.preventAiLarning);
|
||||
let preventAiLearning = $ref($i.preventAiLearning);
|
||||
let isExplorable = $ref($i.isExplorable);
|
||||
let hideOnlineStatus = $ref($i.hideOnlineStatus);
|
||||
let publicReactions = $ref($i.publicReactions);
|
||||
@@ -91,7 +91,7 @@ function save() {
|
||||
isLocked: !!isLocked,
|
||||
autoAcceptFollowed: !!autoAcceptFollowed,
|
||||
noCrawle: !!noCrawle,
|
||||
preventAiLarning: !!preventAiLarning,
|
||||
preventAiLearning: !!preventAiLearning,
|
||||
isExplorable: !!isExplorable,
|
||||
hideOnlineStatus: !!hideOnlineStatus,
|
||||
publicReactions: !!publicReactions,
|
||||
|
Reference in New Issue
Block a user