enhance(client): improve user activity page
This commit is contained in:
		
							parent
							
								
									4ffbbbe6d8
								
							
						
					
					
						commit
						5320f23017
					
				
					 6 changed files with 386 additions and 68 deletions
				
			
		|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div class="ssazuxis"> | ||||
| 	<header class="_button" :style="{ background: bg }" @click="showBody = !showBody"> | ||||
| 		<div class="title"><slot name="header"></slot></div> | ||||
| 		<div class="title"><div><slot name="header"></slot></div></div> | ||||
| 		<div class="divider"></div> | ||||
| 		<button class="_button"> | ||||
| 			<template v-if="showBody"><i class="ti ti-chevron-up"></i></template> | ||||
|  | @ -127,14 +127,6 @@ export default defineComponent({ | |||
| 			place-content: center; | ||||
| 			margin: 0; | ||||
| 			padding: 12px 16px 12px 0; | ||||
| 
 | ||||
| 			> i { | ||||
| 				margin-right: 6px; | ||||
| 			} | ||||
| 
 | ||||
| 			&:empty { | ||||
| 				display: none; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .divider { | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ | |||
| 				<template #prefix><i class="ti ti-lock"></i></template> | ||||
| 				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> | ||||
| 			</MkInput> | ||||
| 			<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> | ||||
| 			<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> | ||||
| 		</div> | ||||
| 		<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> | ||||
| 			<div v-if="user && user.securityKeys" class="twofa-group tap-group"> | ||||
|  | @ -36,7 +36,7 @@ | |||
| 					<template #label>{{ i18n.ts.token }}</template> | ||||
| 					<template #prefix><i class="ti ti-123"></i></template> | ||||
| 				</MkInput> | ||||
| 				<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> | ||||
| 				<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  |  | |||
							
								
								
									
										174
									
								
								packages/frontend/src/pages/user/activity.following.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								packages/frontend/src/pages/user/activity.following.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,174 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 	<div v-show="!fetching" :class="$style.root" class="_panel"> | ||||
| 		<canvas ref="chartEl"></canvas> | ||||
| 		<MkChartLegend ref="legendEl" style="margin-top: 8px;"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||
| import { Chart, ChartDataset } from 'chart.js'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| import { satisfies } from 'compare-versions'; | ||||
| import * as os from '@/os'; | ||||
| 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 { chartLegend } from '@/scripts/chart-legend'; | ||||
| import MkChartLegend from '@/components/MkChartLegend.vue'; | ||||
| 
 | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	user: misskey.entities.User; | ||||
| }>(); | ||||
| 
 | ||||
| const chartEl = $shallowRef<HTMLCanvasElement>(null); | ||||
| let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); | ||||
| const now = new Date(); | ||||
| let chartInstance: Chart = null; | ||||
| const chartLimit = 50; | ||||
| let fetching = $ref(true); | ||||
| 
 | ||||
| const { handler: externalTooltipHandler } = useChartTooltip(); | ||||
| 
 | ||||
| async function renderChart() { | ||||
| 	if (chartInstance) { | ||||
| 		chartInstance.destroy(); | ||||
| 	} | ||||
| 
 | ||||
| 	const getDate = (ago: number) => { | ||||
| 		const y = now.getFullYear(); | ||||
| 		const m = now.getMonth(); | ||||
| 		const d = now.getDate(); | ||||
| 
 | ||||
| 		return new Date(y, m, d - ago); | ||||
| 	}; | ||||
| 
 | ||||
| 	const format = (arr) => { | ||||
| 		return arr.map((v, i) => ({ | ||||
| 			x: getDate(i).getTime(), | ||||
| 			y: v, | ||||
| 		})); | ||||
| 	}; | ||||
| 
 | ||||
| 	const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' }); | ||||
| 
 | ||||
| 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||
| 
 | ||||
| 	const colorFollowLocal = '#008FFB'; | ||||
| 	const colorFollowRemote = '#008FFB88'; | ||||
| 	const colorFollowedLocal = '#2ecc71'; | ||||
| 	const colorFollowedRemote = '#2ecc7188'; | ||||
| 
 | ||||
| 	function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { | ||||
| 		return Object.assign({ | ||||
| 			label: label, | ||||
| 			data: data, | ||||
| 			parsing: false, | ||||
| 			pointRadius: 0, | ||||
| 			borderWidth: 0, | ||||
| 			borderJoinStyle: 'round', | ||||
| 			borderRadius: 4, | ||||
| 			barPercentage: 0.9, | ||||
| 			fill: true, | ||||
| 		} satisfies ChartDataset, extra); | ||||
| 	} | ||||
| 
 | ||||
| 	chartInstance = new Chart(chartEl, { | ||||
| 		type: 'bar', | ||||
| 		data: { | ||||
| 			datasets: [ | ||||
| 				makeDataset('Follow (local)', format(raw.local.followings.inc).slice().reverse(), { backgroundColor: colorFollowLocal }), | ||||
| 				makeDataset('Follow (remote)', format(raw.remote.followings.inc).slice().reverse(), { backgroundColor: colorFollowRemote }), | ||||
| 				makeDataset('Followed (local)', format(raw.local.followers.inc).slice().reverse(), { backgroundColor: colorFollowedLocal }), | ||||
| 				makeDataset('Followed (remote)', format(raw.remote.followers.inc).slice().reverse(), { backgroundColor: colorFollowedRemote }), | ||||
| 			], | ||||
| 		}, | ||||
| 		options: { | ||||
| 			aspectRatio: 3, | ||||
| 			layout: { | ||||
| 				padding: { | ||||
| 					left: 0, | ||||
| 					right: 8, | ||||
| 					top: 0, | ||||
| 					bottom: 0, | ||||
| 				}, | ||||
| 			}, | ||||
| 			scales: { | ||||
| 				x: { | ||||
| 					type: 'time', | ||||
| 					offset: true, | ||||
| 					stacked: true, | ||||
| 					time: { | ||||
| 						stepSize: 1, | ||||
| 						unit: 'day', | ||||
| 						displayFormats: { | ||||
| 							day: 'M/d', | ||||
| 							month: 'Y/M', | ||||
| 						}, | ||||
| 					}, | ||||
| 					grid: { | ||||
| 						display: false, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						maxRotation: 0, | ||||
| 						autoSkipPadding: 8, | ||||
| 					}, | ||||
| 				}, | ||||
| 				y: { | ||||
| 					position: 'left', | ||||
| 					stacked: true, | ||||
| 					suggestedMax: 10, | ||||
| 					grid: { | ||||
| 						display: true, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						//mirror: true, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			interaction: { | ||||
| 				intersect: false, | ||||
| 				mode: 'index', | ||||
| 			}, | ||||
| 			plugins: { | ||||
| 				legend: { | ||||
| 					display: false, | ||||
| 				}, | ||||
| 				tooltip: { | ||||
| 					enabled: false, | ||||
| 					mode: 'index', | ||||
| 					animation: { | ||||
| 						duration: 0, | ||||
| 					}, | ||||
| 					external: externalTooltipHandler, | ||||
| 				}, | ||||
| 				gradient, | ||||
| 			}, | ||||
| 		}, | ||||
| 		plugins: [chartVLine(vLineColor), chartLegend(legendEl)], | ||||
| 	}); | ||||
| 
 | ||||
| 	fetching = false; | ||||
| } | ||||
| 
 | ||||
| onMounted(async () => { | ||||
| 	renderChart(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	padding: 20px; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										174
									
								
								packages/frontend/src/pages/user/activity.notes.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								packages/frontend/src/pages/user/activity.notes.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,174 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 	<div v-show="!fetching" :class="$style.root" class="_panel"> | ||||
| 		<canvas ref="chartEl"></canvas> | ||||
| 		<MkChartLegend ref="legendEl" style="margin-top: 8px;"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||
| import { Chart, ChartDataset } from 'chart.js'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| import { satisfies } from 'compare-versions'; | ||||
| import * as os from '@/os'; | ||||
| 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 { chartLegend } from '@/scripts/chart-legend'; | ||||
| import MkChartLegend from '@/components/MkChartLegend.vue'; | ||||
| 
 | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	user: misskey.entities.User; | ||||
| }>(); | ||||
| 
 | ||||
| const chartEl = $shallowRef<HTMLCanvasElement>(null); | ||||
| let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); | ||||
| const now = new Date(); | ||||
| let chartInstance: Chart = null; | ||||
| const chartLimit = 50; | ||||
| let fetching = $ref(true); | ||||
| 
 | ||||
| const { handler: externalTooltipHandler } = useChartTooltip(); | ||||
| 
 | ||||
| async function renderChart() { | ||||
| 	if (chartInstance) { | ||||
| 		chartInstance.destroy(); | ||||
| 	} | ||||
| 
 | ||||
| 	const getDate = (ago: number) => { | ||||
| 		const y = now.getFullYear(); | ||||
| 		const m = now.getMonth(); | ||||
| 		const d = now.getDate(); | ||||
| 
 | ||||
| 		return new Date(y, m, d - ago); | ||||
| 	}; | ||||
| 
 | ||||
| 	const format = (arr) => { | ||||
| 		return arr.map((v, i) => ({ | ||||
| 			x: getDate(i).getTime(), | ||||
| 			y: v, | ||||
| 		})); | ||||
| 	}; | ||||
| 
 | ||||
| 	const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); | ||||
| 
 | ||||
| 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||
| 
 | ||||
| 	const colorNormal = '#008FFB'; | ||||
| 	const colorReply = '#FEB019'; | ||||
| 	const colorRenote = '#00E396'; | ||||
| 	const colorFile = '#e300db'; | ||||
| 
 | ||||
| 	function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { | ||||
| 		return Object.assign({ | ||||
| 			label: label, | ||||
| 			data: data, | ||||
| 			parsing: false, | ||||
| 			pointRadius: 0, | ||||
| 			borderWidth: 0, | ||||
| 			borderJoinStyle: 'round', | ||||
| 			borderRadius: 4, | ||||
| 			barPercentage: 0.9, | ||||
| 			fill: true, | ||||
| 		} satisfies ChartDataset, extra); | ||||
| 	} | ||||
| 
 | ||||
| 	chartInstance = new Chart(chartEl, { | ||||
| 		type: 'bar', | ||||
| 		data: { | ||||
| 			datasets: [ | ||||
| 				makeDataset('Normal', format(raw.diffs.normal).slice().reverse(), { backgroundColor: colorNormal }), | ||||
| 				makeDataset('Reply', format(raw.diffs.reply).slice().reverse(), { backgroundColor: colorReply }), | ||||
| 				makeDataset('Renote', format(raw.diffs.renote).slice().reverse(), { backgroundColor: colorRenote }), | ||||
| 				makeDataset('File', format(raw.diffs.withFile).slice().reverse(), { backgroundColor: colorFile }), | ||||
| 			], | ||||
| 		}, | ||||
| 		options: { | ||||
| 			aspectRatio: 3, | ||||
| 			layout: { | ||||
| 				padding: { | ||||
| 					left: 0, | ||||
| 					right: 8, | ||||
| 					top: 0, | ||||
| 					bottom: 0, | ||||
| 				}, | ||||
| 			}, | ||||
| 			scales: { | ||||
| 				x: { | ||||
| 					type: 'time', | ||||
| 					offset: true, | ||||
| 					stacked: true, | ||||
| 					time: { | ||||
| 						stepSize: 1, | ||||
| 						unit: 'day', | ||||
| 						displayFormats: { | ||||
| 							day: 'M/d', | ||||
| 							month: 'Y/M', | ||||
| 						}, | ||||
| 					}, | ||||
| 					grid: { | ||||
| 						display: false, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						maxRotation: 0, | ||||
| 						autoSkipPadding: 8, | ||||
| 					}, | ||||
| 				}, | ||||
| 				y: { | ||||
| 					position: 'left', | ||||
| 					stacked: true, | ||||
| 					suggestedMax: 10, | ||||
| 					grid: { | ||||
| 						display: true, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						//mirror: true, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			interaction: { | ||||
| 				intersect: false, | ||||
| 				mode: 'index', | ||||
| 			}, | ||||
| 			plugins: { | ||||
| 				legend: { | ||||
| 					display: false, | ||||
| 				}, | ||||
| 				tooltip: { | ||||
| 					enabled: false, | ||||
| 					mode: 'index', | ||||
| 					animation: { | ||||
| 						duration: 0, | ||||
| 					}, | ||||
| 					external: externalTooltipHandler, | ||||
| 				}, | ||||
| 				gradient, | ||||
| 			}, | ||||
| 		}, | ||||
| 		plugins: [chartVLine(vLineColor), chartLegend(legendEl)], | ||||
| 	}); | ||||
| 
 | ||||
| 	fetching = false; | ||||
| } | ||||
| 
 | ||||
| onMounted(async () => { | ||||
| 	renderChart(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	padding: 20px; | ||||
| } | ||||
| </style> | ||||
|  | @ -10,7 +10,7 @@ | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import { Chart, ChartDataset } from 'chart.js'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
|  | @ -67,65 +67,33 @@ async function renderChart() { | |||
| 	const colorUser2 = '#3498db88'; | ||||
| 	const colorVisitor2 = '#2ecc7188'; | ||||
| 
 | ||||
| 	function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { | ||||
| 		return Object.assign({ | ||||
| 			label: label, | ||||
| 			data: data, | ||||
| 			parsing: false, | ||||
| 			pointRadius: 0, | ||||
| 			borderWidth: 0, | ||||
| 			borderJoinStyle: 'round', | ||||
| 			borderRadius: 4, | ||||
| 			barPercentage: 0.7, | ||||
| 			categoryPercentage: 0.7, | ||||
| 			fill: true, | ||||
| 		} satisfies ChartDataset, extra); | ||||
| 	} | ||||
| 
 | ||||
| 	chartInstance = new Chart(chartEl, { | ||||
| 		type: 'bar', | ||||
| 		data: { | ||||
| 			datasets: [{ | ||||
| 				parsing: false, | ||||
| 				label: 'UPV (user)', | ||||
| 				data: format(raw.upv.user).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorUser, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 0.7, | ||||
| 				fill: true, | ||||
| 				stack: 'u', | ||||
| 			}, { | ||||
| 				parsing: false, | ||||
| 				label: 'UPV (visitor)', | ||||
| 				data: format(raw.upv.visitor).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorVisitor, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 0.7, | ||||
| 				fill: true, | ||||
| 				stack: 'u', | ||||
| 			}, { | ||||
| 				parsing: false, | ||||
| 				label: 'NPV (user)', | ||||
| 				data: format(raw.pv.user).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorUser2, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 0.7, | ||||
| 				fill: true, | ||||
| 				stack: 'n', | ||||
| 			}, { | ||||
| 				parsing: false, | ||||
| 				label: 'NPV (visitor)', | ||||
| 				data: format(raw.pv.visitor).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorVisitor2, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 0.7, | ||||
| 				fill: true, | ||||
| 				stack: 'n', | ||||
| 			}], | ||||
| 			datasets: [ | ||||
| 				makeDataset('UPV (user)', format(raw.upv.user).slice().reverse(), { backgroundColor: colorUser, stack: 'u' }), | ||||
| 				makeDataset('UPV (visitor)', format(raw.upv.visitor).slice().reverse(), { backgroundColor: colorVisitor, stack: 'u' }), | ||||
| 				makeDataset('NPV (user)', format(raw.pv.user).slice().reverse(), { backgroundColor: colorUser2, stack: 'n' }), | ||||
| 				makeDataset('UPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }), | ||||
| 			], | ||||
| 		}, | ||||
| 		options: { | ||||
| 			aspectRatio: 2.5, | ||||
| 			aspectRatio: 3, | ||||
| 			layout: { | ||||
| 				padding: { | ||||
| 					left: 0, | ||||
|  |  | |||
|  | @ -2,11 +2,19 @@ | |||
| <MkSpacer :content-max="700"> | ||||
| 	<div class="_gaps"> | ||||
| 		<MkFolder class="item"> | ||||
| 			<template #header>Heatmap</template> | ||||
| 			<template #header><i class="ti ti-activity"></i> Heatmap</template> | ||||
| 			<XHeatmap :user="user" :src="'notes'"/> | ||||
| 		</MkFolder> | ||||
| 		<MkFolder class="item"> | ||||
| 			<template #header>PV</template> | ||||
| 			<template #header><i class="ti ti-pencil"></i> Notes</template> | ||||
| 			<XNotes :user="user"/> | ||||
| 		</MkFolder> | ||||
| 		<MkFolder class="item"> | ||||
| 			<template #header><i class="ti ti-users"></i> Following</template> | ||||
| 			<XFollowing :user="user"/> | ||||
| 		</MkFolder> | ||||
| 		<MkFolder class="item"> | ||||
| 			<template #header><i class="ti ti-eye"></i> PV</template> | ||||
| 			<XPv :user="user"/> | ||||
| 		</MkFolder> | ||||
| 	</div> | ||||
|  | @ -18,6 +26,8 @@ import { computed } from 'vue'; | |||
| import * as misskey from 'misskey-js'; | ||||
| import XHeatmap from './activity.heatmap.vue'; | ||||
| import XPv from './activity.pv.vue'; | ||||
| import XNotes from './activity.notes.vue'; | ||||
| import XFollowing from './activity.following.vue'; | ||||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue