Merge branch 'develop' into mkjs-n

This commit is contained in:
tamaina 2023-05-12 07:03:20 +00:00
commit 64f4bb6a90
63 changed files with 1694 additions and 1197 deletions

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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