Merge branch 'develop'
This commit is contained in:
		
						commit
						938fcb3e5e
					
				
					 72 changed files with 2392 additions and 1037 deletions
				
			
		
							
								
								
									
										1
									
								
								.yarnrc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.yarnrc
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| network-timeout 600000 | ||||
|  | @ -22,7 +22,7 @@ RUN apk add --no-cache \ | |||
|     vips-dev \ | ||||
|     vips | ||||
| 
 | ||||
| COPY package.json yarn.lock ./ | ||||
| COPY package.json yarn.lock .yarnrc ./ | ||||
| RUN yarn install | ||||
| COPY . ./ | ||||
| RUN yarn build | ||||
|  |  | |||
|  | @ -719,6 +719,15 @@ quitFullView: "フルビュー解除" | |||
| addDescription: "説明を追加" | ||||
| userPagePinTip: "個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。" | ||||
| notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります" | ||||
| info: "情報" | ||||
| userInfo: "ユーザー情報" | ||||
| unknown: "不明" | ||||
| onlineStatus: "オンライン状態" | ||||
| hideOnlineStatus: "オンライン状態を隠す" | ||||
| hideOnlineStatusDescription: "オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。" | ||||
| online: "オンライン" | ||||
| active: "アクティブ" | ||||
| offline: "オフライン" | ||||
| 
 | ||||
| _email: | ||||
|   _follow: | ||||
|  |  | |||
							
								
								
									
										16
									
								
								migration/1618637372000-user-last-active-date.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								migration/1618637372000-user-last-active-date.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| import {MigrationInterface, QueryRunner} from "typeorm"; | ||||
| 
 | ||||
| export class userLastActiveDate1618637372000 implements MigrationInterface { | ||||
|     name = 'userLastActiveDate1618637372000' | ||||
| 
 | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "user" ADD "lastActiveDate" TIMESTAMP WITH TIME ZONE DEFAULT NULL`); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_seoignmeoprigmkpodgrjmkpormg" ON "user" ("lastActiveDate") `); | ||||
|     } | ||||
| 
 | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`DROP INDEX "IDX_seoignmeoprigmkpodgrjmkpormg"`); | ||||
|         await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "lastActiveDate"`); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										14
									
								
								migration/1618639857000-user-hide-online-status.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								migration/1618639857000-user-hide-online-status.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| import {MigrationInterface, QueryRunner} from "typeorm"; | ||||
| 
 | ||||
| export class userHideOnlineStatus1618639857000 implements MigrationInterface { | ||||
|     name = 'userHideOnlineStatus1618639857000' | ||||
| 
 | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
| 			await queryRunner.query(`ALTER TABLE "user" ADD "hideOnlineStatus" boolean NOT NULL DEFAULT false`); | ||||
|     } | ||||
| 
 | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hideOnlineStatus"`); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										92
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										92
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <syuilotan@yahoo.co.jp>", | ||||
| 	"version": "12.76.1", | ||||
| 	"version": "12.77.0", | ||||
| 	"codename": "indigo", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|  | @ -37,7 +37,7 @@ | |||
| 		"lodash": "^4.17.20" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@babel/plugin-transform-runtime": "7.13.10", | ||||
| 		"@babel/plugin-transform-runtime": "7.13.15", | ||||
| 		"@elastic/elasticsearch": "7.11.0", | ||||
| 		"@fortawesome/fontawesome-svg-core": "1.2.35", | ||||
| 		"@fortawesome/free-brands-svg-icons": "5.15.3", | ||||
|  | @ -49,7 +49,7 @@ | |||
| 		"@koa/router": "9.0.1", | ||||
| 		"@sentry/browser": "5.29.2", | ||||
| 		"@sentry/tracing": "5.29.2", | ||||
| 		"@sinonjs/fake-timers": "7.0.2", | ||||
| 		"@sinonjs/fake-timers": "7.0.5", | ||||
| 		"@syuilo/aiscript": "0.11.1", | ||||
| 		"@types/bcryptjs": "2.4.2", | ||||
| 		"@types/bull": "3.15.0", | ||||
|  | @ -62,7 +62,7 @@ | |||
| 		"@types/gulp-replace": "0.0.31", | ||||
| 		"@types/is-url": "1.2.28", | ||||
| 		"@types/js-yaml": "4.0.0", | ||||
| 		"@types/jsdom": "16.2.7", | ||||
| 		"@types/jsdom": "16.2.10", | ||||
| 		"@types/jsonld": "1.5.5", | ||||
| 		"@types/katex": "0.11.0", | ||||
| 		"@types/koa": "2.13.1", | ||||
|  | @ -77,10 +77,10 @@ | |||
| 		"@types/koa__multer": "2.0.2", | ||||
| 		"@types/koa__router": "8.0.4", | ||||
| 		"@types/markdown-it": "12.0.1", | ||||
| 		"@types/matter-js": "0.14.10", | ||||
| 		"@types/mocha": "8.2.1", | ||||
| 		"@types/node": "14.14.35", | ||||
| 		"@types/node-fetch": "2.5.8", | ||||
| 		"@types/matter-js": "0.14.11", | ||||
| 		"@types/mocha": "8.2.2", | ||||
| 		"@types/node": "14.14.41", | ||||
| 		"@types/node-fetch": "2.5.10", | ||||
| 		"@types/nodemailer": "6.4.1", | ||||
| 		"@types/nprogress": "0.2.0", | ||||
| 		"@types/oauth": "0.9.1", | ||||
|  | @ -97,7 +97,7 @@ | |||
| 		"@types/request-stats": "3.0.0", | ||||
| 		"@types/rimraf": "3.0.0", | ||||
| 		"@types/seedrandom": "2.4.28", | ||||
| 		"@types/sharp": "0.27.1", | ||||
| 		"@types/sharp": "0.28.0", | ||||
| 		"@types/sinonjs__fake-timers": "6.0.2", | ||||
| 		"@types/speakeasy": "2.0.5", | ||||
| 		"@types/throttle-debounce": "2.1.0", | ||||
|  | @ -105,39 +105,39 @@ | |||
| 		"@types/tmp": "0.2.0", | ||||
| 		"@types/uuid": "8.3.0", | ||||
| 		"@types/web-push": "3.3.0", | ||||
| 		"@types/webpack": "4.41.26", | ||||
| 		"@types/webpack-stream": "3.2.11", | ||||
| 		"@types/webpack": "5.28.0", | ||||
| 		"@types/webpack-stream": "3.2.12", | ||||
| 		"@types/websocket": "1.0.2", | ||||
| 		"@types/ws": "7.4.0", | ||||
| 		"@typescript-eslint/parser": "4.18.0", | ||||
| 		"@vue/compiler-sfc": "3.0.8", | ||||
| 		"@types/ws": "7.4.1", | ||||
| 		"@typescript-eslint/parser": "4.22.0", | ||||
| 		"@vue/compiler-sfc": "3.0.11", | ||||
| 		"abort-controller": "3.0.0", | ||||
| 		"apexcharts": "3.26.0", | ||||
| 		"autobind-decorator": "2.4.0", | ||||
| 		"autosize": "4.0.2", | ||||
| 		"autwh": "0.1.0", | ||||
| 		"aws-sdk": "2.867.0", | ||||
| 		"aws-sdk": "2.887.0", | ||||
| 		"bcryptjs": "2.4.3", | ||||
| 		"blurhash": "1.1.3", | ||||
| 		"broadcast-channel": "3.5.3", | ||||
| 		"bull": "3.21.1", | ||||
| 		"bull": "3.22.0", | ||||
| 		"cafy": "15.2.1", | ||||
| 		"cbor": "7.0.4", | ||||
| 		"cbor": "7.0.5", | ||||
| 		"chalk": "4.1.0", | ||||
| 		"chart.js": "2.9.4", | ||||
| 		"cli-highlight": "2.1.10", | ||||
| 		"commander": "4.1.1", | ||||
| 		"concurrently": "6.0.0", | ||||
| 		"cli-highlight": "2.1.11", | ||||
| 		"commander": "7.2.0", | ||||
| 		"concurrently": "6.0.2", | ||||
| 		"content-disposition": "0.5.3", | ||||
| 		"core-js": "3.9.1", | ||||
| 		"core-js": "3.10.1", | ||||
| 		"crc-32": "1.2.0", | ||||
| 		"css-loader": "5.1.3", | ||||
| 		"cssnano": "4.1.10", | ||||
| 		"css-loader": "5.2.1", | ||||
| 		"cssnano": "5.0.1", | ||||
| 		"dateformat": "4.5.1", | ||||
| 		"diskusage": "1.1.3", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"eslint": "7.22.0", | ||||
| 		"eslint-plugin-vue": "7.7.0", | ||||
| 		"eslint": "7.24.0", | ||||
| 		"eslint-plugin-vue": "7.9.0", | ||||
| 		"eventemitter3": "4.0.7", | ||||
| 		"feed": "4.2.2", | ||||
| 		"fibers": "5.0.0", | ||||
|  | @ -156,17 +156,17 @@ | |||
| 		"http-proxy-agent": "4.0.1", | ||||
| 		"http-signature": "1.3.5", | ||||
| 		"https-proxy-agent": "5.0.0", | ||||
| 		"idb-keyval": "5.0.4", | ||||
| 		"idb-keyval": "5.0.5", | ||||
| 		"insert-text-at-cursor": "0.3.0", | ||||
| 		"is-root": "2.1.0", | ||||
| 		"is-svg": "4.3.1", | ||||
| 		"js-yaml": "4.0.0", | ||||
| 		"jsdom": "16.5.1", | ||||
| 		"js-yaml": "4.1.0", | ||||
| 		"jsdom": "16.5.3", | ||||
| 		"json5": "2.2.0", | ||||
| 		"json5-loader": "4.0.1", | ||||
| 		"jsonld": "4.0.1", | ||||
| 		"jsrsasign": "8.0.20", | ||||
| 		"katex": "0.13.0", | ||||
| 		"katex": "0.13.2", | ||||
| 		"koa": "2.13.1", | ||||
| 		"koa-bodyparser": "4.3.0", | ||||
| 		"koa-favicon": "2.1.0", | ||||
|  | @ -178,9 +178,9 @@ | |||
| 		"koa-views": "7.0.1", | ||||
| 		"langmap": "0.0.16", | ||||
| 		"lookup-dns-cache": "2.1.0", | ||||
| 		"markdown-it": "12.0.4", | ||||
| 		"markdown-it": "12.0.5", | ||||
| 		"markdown-it-anchor": "7.1.0", | ||||
| 		"matter-js": "0.16.1", | ||||
| 		"matter-js": "0.17.1", | ||||
| 		"mfm-js": "0.15.0", | ||||
| 		"mocha": "8.3.2", | ||||
| 		"moji": "0.5.1", | ||||
|  | @ -192,23 +192,23 @@ | |||
| 		"object-assign-deep": "0.4.0", | ||||
| 		"os-utils": "0.0.14", | ||||
| 		"parse5": "6.0.1", | ||||
| 		"pg": "8.5.1", | ||||
| 		"pg": "8.6.0", | ||||
| 		"portscanner": "2.2.0", | ||||
| 		"postcss": "8.2.8", | ||||
| 		"postcss": "8.2.10", | ||||
| 		"postcss-loader": "5.2.0", | ||||
| 		"prismjs": "1.23.0", | ||||
| 		"probe-image-size": "7.0.1", | ||||
| 		"probe-image-size": "7.1.0", | ||||
| 		"promise-limit": "2.7.0", | ||||
| 		"promise-sequential": "1.1.1", | ||||
| 		"pug": "3.0.2", | ||||
| 		"punycode": "2.1.1", | ||||
| 		"pureimage": "0.2.7", | ||||
| 		"pureimage": "0.3.2", | ||||
| 		"qrcode": "1.4.4", | ||||
| 		"random-seed": "0.3.0", | ||||
| 		"ratelimiter": "3.4.1", | ||||
| 		"re2": "1.15.9", | ||||
| 		"reconnecting-websocket": "4.4.0", | ||||
| 		"redis": "3.0.2", | ||||
| 		"redis": "3.1.1", | ||||
| 		"redis-lock": "0.1.4", | ||||
| 		"reflect-metadata": "0.1.13", | ||||
| 		"regenerator-runtime": "0.13.7", | ||||
|  | @ -221,32 +221,32 @@ | |||
| 		"sass": "1.32.8", | ||||
| 		"sass-loader": "11.0.1", | ||||
| 		"seedrandom": "3.0.5", | ||||
| 		"sharp": "0.27.2", | ||||
| 		"sharp": "0.28.1", | ||||
| 		"speakeasy": "2.0.0", | ||||
| 		"stringz": "2.1.0", | ||||
| 		"style-loader": "2.0.0", | ||||
| 		"summaly": "2.4.0", | ||||
| 		"syslog-pro": "1.0.0", | ||||
| 		"systeminformation": "5.6.7", | ||||
| 		"systeminformation": "5.6.12", | ||||
| 		"syuilo-password-strength": "0.0.1", | ||||
| 		"textarea-caret": "3.1.0", | ||||
| 		"three": "0.117.1", | ||||
| 		"throttle-debounce": "3.0.1", | ||||
| 		"tinycolor2": "1.4.2", | ||||
| 		"tmp": "0.2.1", | ||||
| 		"ts-loader": "8.0.18", | ||||
| 		"ts-loader": "8.1.0", | ||||
| 		"ts-node": "9.1.1", | ||||
| 		"tsc-alias": "1.2.8", | ||||
| 		"tsc-alias": "1.2.9", | ||||
| 		"tsconfig-paths": "3.9.0", | ||||
| 		"tslint": "6.1.3", | ||||
| 		"tslint-sonarts": "1.9.0", | ||||
| 		"typeorm": "0.2.31", | ||||
| 		"typescript": "4.2.3", | ||||
| 		"typeorm": "0.2.32", | ||||
| 		"typescript": "4.2.4", | ||||
| 		"ulid": "2.3.0", | ||||
| 		"uuid": "8.3.2", | ||||
| 		"v-debounce": "0.1.2", | ||||
| 		"vanilla-tilt": "1.7.0", | ||||
| 		"vue": "3.0.8", | ||||
| 		"vue": "3.0.11", | ||||
| 		"vue-color": "2.8.1", | ||||
| 		"vue-json-pretty": "1.7.1", | ||||
| 		"vue-loader": "16.1.2", | ||||
|  | @ -256,9 +256,9 @@ | |||
| 		"vue-svg-loader": "0.17.0-beta.2", | ||||
| 		"vuedraggable": "4.0.1", | ||||
| 		"web-push": "3.4.4", | ||||
| 		"webpack": "5.27.2", | ||||
| 		"webpack-cli": "4.5.0", | ||||
| 		"websocket": "1.0.33", | ||||
| 		"webpack": "5.33.2", | ||||
| 		"webpack-cli": "4.6.0", | ||||
| 		"websocket": "1.0.34", | ||||
| 		"ws": "7.4.4", | ||||
| 		"xev": "2.0.1" | ||||
| 	}, | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| import * as program from 'commander'; | ||||
| import { Command } from 'commander'; | ||||
| import config from '@/config'; | ||||
| 
 | ||||
| const program = new Command(); | ||||
| 
 | ||||
| program | ||||
| 	.version(config.version) | ||||
| 	.option('--no-daemons', 'Disable daemon processes (for debbuging)') | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;"> | ||||
| 		<MkAvatar :user="user" style="width:32px;height:32px;"/> | ||||
| 		<MkAvatar :user="user" style="width:32px;height:32px;" :show-indicator="true"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  |  | |||
|  | @ -18,7 +18,12 @@ export default defineComponent({ | |||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 		}, | ||||
| 		noGap: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  | @ -37,18 +42,16 @@ export default defineComponent({ | |||
| 	}, | ||||
| 
 | ||||
| 	render() { | ||||
| 		const noGap = [...document.querySelectorAll('._noGap_')].some(el => el.contains(this.$parent.$el)); | ||||
| 
 | ||||
| 		if (this.items.length === 0) return; | ||||
| 
 | ||||
| 		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? { | ||||
| 			class: 'sqadhkmv' + (noGap ? ' _block' : ''), | ||||
| 			class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), | ||||
| 			name: 'list', | ||||
| 			tag: 'div', | ||||
| 			'data-direction': this.direction, | ||||
| 			'data-reversed': this.reversed ? 'true' : 'false', | ||||
| 		} : { | ||||
| 			class: 'sqadhkmv', | ||||
| 			class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), | ||||
| 		}, this.items.map((item, i) => { | ||||
| 			const el = this.$slots.default({ | ||||
| 				item: item | ||||
|  | @ -154,9 +157,8 @@ export default defineComponent({ | |||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._noGap_ .sqadhkmv { | ||||
| 	&.noGap { | ||||
| 		> * { | ||||
| 			margin: 0 !important; | ||||
| 			border: none; | ||||
|  | @ -168,6 +170,7 @@ export default defineComponent({ | |||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._inContainer_ .sqadhkmv > * { | ||||
| 	margin: 0 !important; | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ import { | |||
| 	faFileArchive, | ||||
| 	faFilm | ||||
| 	} from '@fortawesome/free-solid-svg-icons'; | ||||
| import ImgWithBlurhash from './img-with-blurhash.vue'; | ||||
| import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; | ||||
| import { ColdDeviceStorage } from '@client/store'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|  |  | |||
|  | @ -24,9 +24,12 @@ export default defineComponent({ | |||
| 	--formXPadding: 32px; | ||||
| 	--formYPadding: 32px; | ||||
| 
 | ||||
| 	font-size: 95%; | ||||
| 	line-height: 1.3em; | ||||
| 	background: var(--bg); | ||||
| 	padding: var(--formYPadding) var(--formXPadding); | ||||
| 	max-width: 750px; | ||||
| 	margin: 0 auto; | ||||
| 
 | ||||
| 	&:not(.wide).max-width_400px { | ||||
| 		--formXPadding: 0px; | ||||
|  | @ -40,16 +43,16 @@ export default defineComponent({ | |||
| 			} | ||||
| 
 | ||||
| 			._form_group { | ||||
| 				> * { | ||||
| 					&:not(:first-child) { | ||||
| 				> *:not(._formNoConcat) { | ||||
| 					&:not(:last-child):not(._formNoConcatPrev) { | ||||
| 						&._formPanel, ._formPanel { | ||||
| 							border-top: none; | ||||
| 							border-bottom: solid 0.5px var(--divider); | ||||
| 						} | ||||
| 					} | ||||
| 
 | ||||
| 					&:not(:last-child) { | ||||
| 					&:not(:first-child):not(._formNoConcatNext) { | ||||
| 						&._formPanel, ._formPanel { | ||||
| 							border-bottom: solid 0.5px var(--divider); | ||||
| 							border-top: none; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div class="vrtktovg _formItem" v-size="{ max: [500] }"> | ||||
| <div class="vrtktovg _formItem _formNoConcat" v-size="{ max: [500] }" v-sticky-container> | ||||
| 	<div class="_formLabel"><slot name="label"></slot></div> | ||||
| 	<div class="main _form_group"> | ||||
| 	<div class="main _form_group" ref="child"> | ||||
| 		<slot></slot> | ||||
| 	</div> | ||||
| 	<div class="_formCaption"><slot name="caption"></slot></div> | ||||
|  | @ -9,33 +9,69 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { defineComponent, onMounted, ref } from 'vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	setup(props, context) { | ||||
| 		const child = ref<HTMLElement | null>(null); | ||||
| 
 | ||||
| 		const scanChild = () => { | ||||
| 			if (child.value == null) return; | ||||
| 			const els = Array.from(child.value.children); | ||||
| 			for (let i = 0; i < els.length; i++) { | ||||
| 				const el = els[i]; | ||||
| 				if (el.classList.contains('_formNoConcat')) { | ||||
| 					if (els[i - 1]) els[i - 1].classList.add('_formNoConcatPrev'); | ||||
| 					if (els[i + 1]) els[i + 1].classList.add('_formNoConcatNext'); | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			scanChild(); | ||||
| 
 | ||||
| 			const observer = new MutationObserver(records => { | ||||
| 				scanChild(); | ||||
| 			}); | ||||
| 
 | ||||
| 			observer.observe(child.value, { | ||||
| 				childList: true, | ||||
| 				subtree: false, | ||||
| 				attributes: false, | ||||
| 				characterData: false, | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			child | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .vrtktovg { | ||||
| 	> .main { | ||||
| 		> ::v-deep(*) { | ||||
| 		> ::v-deep(*):not(._formNoConcat) { | ||||
| 			&:not(._formNoConcatNext) { | ||||
| 				margin: 0; | ||||
| 
 | ||||
| 			&:not(:first-child) { | ||||
| 				&._formPanel, ._formPanel { | ||||
| 					border-top: none; | ||||
| 					border-top-left-radius: 0; | ||||
| 					border-top-right-radius: 0; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			&:not(:last-child) { | ||||
| 			&:not(:last-child):not(._formNoConcatPrev) { | ||||
| 				&._formPanel, ._formPanel { | ||||
| 					border-bottom: solid 0.5px var(--divider); | ||||
| 					border-bottom-left-radius: 0; | ||||
| 					border-bottom-right-radius: 0; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			&:not(:first-child):not(._formNoConcatNext) { | ||||
| 				&._formPanel, ._formPanel { | ||||
| 					border-top: none; | ||||
| 					border-top-left-radius: 0; | ||||
| 					border-top-right-radius: 0; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -22,9 +22,17 @@ export default defineComponent({ | |||
| 	align-items: center; | ||||
| 	padding: 14px 16px; | ||||
| 
 | ||||
| 	> .key { | ||||
| 		margin-right: 12px; | ||||
| 		white-space: nowrap; | ||||
| 	} | ||||
| 
 | ||||
| 	> .value { | ||||
| 		margin-left: auto; | ||||
| 		opacity: 0.7; | ||||
| 		text-overflow: ellipsis; | ||||
| 		white-space: nowrap; | ||||
| 		overflow: hidden; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
							
								
								
									
										102
									
								
								src/client/components/form/object-view.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/client/components/form/object-view.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | |||
| <template> | ||||
| <FormGroup class="_formItem"> | ||||
| 	<template #label><slot></slot></template> | ||||
| 	<div class="drooglns _formItem" :class="{ tall }"> | ||||
| 		<div class="input _formPanel"> | ||||
| 			<textarea class="_monospace" | ||||
| 				v-model="v" | ||||
| 				readonly | ||||
| 				:spellcheck="false" | ||||
| 			></textarea> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<template #caption><slot name="desc"></slot></template> | ||||
| </FormGroup> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, ref, toRefs, watch } from 'vue'; | ||||
| import * as JSON5 from 'json5'; | ||||
| import './form.scss'; | ||||
| import FormGroup from './group.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormGroup, | ||||
| 	}, | ||||
| 	props: { | ||||
| 		value: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 		tall: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		pre: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		manualSave: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 	setup(props, context) { | ||||
| 		const { value } = toRefs(props); | ||||
| 		const v = ref(''); | ||||
| 
 | ||||
| 		watch(() => value, newValue => { | ||||
| 			v.value = JSON5.stringify(newValue.value, null, '\t'); | ||||
| 		}, { | ||||
| 			immediate: true | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			v, | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .drooglns { | ||||
| 	position: relative; | ||||
| 
 | ||||
| 	> .input { | ||||
| 		position: relative; | ||||
| 	 | ||||
| 		> textarea { | ||||
| 			display: block; | ||||
| 			width: 100%; | ||||
| 			min-width: 100%; | ||||
| 			max-width: 100%; | ||||
| 			min-height: 130px; | ||||
| 			margin: 0; | ||||
| 			padding: 16px; | ||||
| 			box-sizing: border-box; | ||||
| 			font: inherit; | ||||
| 			font-weight: normal; | ||||
| 			font-size: 1em; | ||||
| 			background: transparent; | ||||
| 			border: none; | ||||
| 			border-radius: 0; | ||||
| 			outline: none; | ||||
| 			box-shadow: none; | ||||
| 			color: var(--fg); | ||||
| 			tab-size: 2; | ||||
| 			white-space: pre; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.tall { | ||||
| 		> .input { | ||||
| 			> textarea { | ||||
| 				min-height: 200px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										92
									
								
								src/client/components/form/suspense.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/client/components/form/suspense.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | |||
| <template> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<div class="_formItem" v-if="pending"> | ||||
| 		<div class="_formPanel"> | ||||
| 			<MkLoading/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<FormGroup v-else-if="resolved" class="_formItem"> | ||||
| 		<slot :result="result"></slot> | ||||
| 	</FormGroup> | ||||
| 	<div class="_formItem" v-else> | ||||
| 		<div class="_formPanel"> | ||||
| 			error! | ||||
| 			<button @click="retry">retry</button> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, PropType, ref, watch } from 'vue'; | ||||
| import './form.scss'; | ||||
| import FormGroup from './group.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormGroup, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		p: { | ||||
| 			type: Function as PropType<() => Promise<any>>, | ||||
| 			required: true, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props, context) { | ||||
| 		const pending = ref(true); | ||||
| 		const resolved = ref(false); | ||||
| 		const rejected = ref(false); | ||||
| 		const result = ref(null); | ||||
| 
 | ||||
| 		const process = () => { | ||||
| 			if (props.p == null) { | ||||
| 				return; | ||||
| 			} | ||||
| 			const promise = props.p(); | ||||
| 			pending.value = true; | ||||
| 			resolved.value = false; | ||||
| 			rejected.value = false; | ||||
| 			promise.then((_result) => { | ||||
| 				pending.value = false; | ||||
| 				resolved.value = true; | ||||
| 				result.value = _result; | ||||
| 			}); | ||||
| 			promise.catch(() => { | ||||
| 				pending.value = false; | ||||
| 				rejected.value = true; | ||||
| 			}); | ||||
| 		}; | ||||
| 
 | ||||
| 		watch(() => props.p, () => { | ||||
| 			process(); | ||||
| 		}, { | ||||
| 			immediate: true | ||||
| 		}); | ||||
| 
 | ||||
| 		const retry = () => { | ||||
| 			process(); | ||||
| 		}; | ||||
| 
 | ||||
| 		return { | ||||
| 			pending, | ||||
| 			resolved, | ||||
| 			rejected, | ||||
| 			result, | ||||
| 			retry, | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| </style> | ||||
|  | @ -1,9 +1,11 @@ | |||
| <template> | ||||
| <span class="eiwwqkts _noSelect" :class="{ cat }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick"> | ||||
| 	<img class="inner" :src="url" decoding="async"/> | ||||
| 	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/> | ||||
| </span> | ||||
| <MkA class="eiwwqkts _noSelect" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id"> | ||||
| 	<img class="inner" :src="url" decoding="async"/> | ||||
| 	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/> | ||||
| </MkA> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -12,8 +14,12 @@ import { defineComponent } from 'vue'; | |||
| import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | ||||
| import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; | ||||
| import { acct, userPage } from '@client/filters/user'; | ||||
| import MkUserOnlineIndicator from '@client/components/user-online-indicator.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkUserOnlineIndicator | ||||
| 	}, | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
|  | @ -30,6 +36,10 @@ export default defineComponent({ | |||
| 		disablePreview: { | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		showIndicator: { | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 	emits: ['click'], | ||||
|  | @ -93,7 +103,7 @@ export default defineComponent({ | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.inner { | ||||
| 	> .inner { | ||||
| 		position: absolute; | ||||
| 		bottom: 0; | ||||
| 		left: 0; | ||||
|  | @ -106,5 +116,14 @@ export default defineComponent({ | |||
| 		width: 100%; | ||||
| 		height: 100%; | ||||
| 	} | ||||
| 
 | ||||
| 	> .indicator { | ||||
| 		position: absolute; | ||||
| 		z-index: 1; | ||||
| 		bottom: 0; | ||||
| 		left: 0; | ||||
| 		width: 20%; | ||||
| 		height: 20%; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,12 +1,11 @@ | |||
| <template> | ||||
| <div class="yxspomdl" :class="{ inline }"> | ||||
| <div class="yxspomdl" :class="{ inline, colored }"> | ||||
| 	<div class="ring"></div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import * as os from '@client/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
|  | @ -14,6 +13,11 @@ export default defineComponent({ | |||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		colored: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: true | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | @ -32,6 +36,11 @@ export default defineComponent({ | |||
| .yxspomdl { | ||||
| 	padding: 32px; | ||||
| 	text-align: center; | ||||
| 	cursor: wait; | ||||
| 
 | ||||
| 	&.colored { | ||||
| 		color: var(--accent); | ||||
| 	} | ||||
| 
 | ||||
| 	&.inline { | ||||
| 		display: inline; | ||||
|  | @ -41,15 +50,23 @@ export default defineComponent({ | |||
| 			width: 32px; | ||||
| 			height: 32px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .ring { | ||||
| 			&:before, | ||||
| 			&:after { | ||||
| 				width: 32px; | ||||
| 				height: 32px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .ring { | ||||
| 		position: relative; | ||||
| 		display: inline-block; | ||||
| 		opacity: 0.7; | ||||
| 		vertical-align: middle; | ||||
| 	} | ||||
| 
 | ||||
| 	> .ring:after { | ||||
| 		&:before, | ||||
| 		&:after { | ||||
| 			content: " "; | ||||
| 			display: block; | ||||
| 			box-sizing: border-box; | ||||
|  | @ -57,8 +74,19 @@ export default defineComponent({ | |||
| 			height: 48px; | ||||
| 			border-radius: 50%; | ||||
| 			border: solid 4px; | ||||
| 		} | ||||
| 
 | ||||
| 		&:before { | ||||
| 			border-color: currentColor; | ||||
| 			opacity: 0.3; | ||||
| 		} | ||||
| 
 | ||||
| 		&:after { | ||||
| 			position: absolute; | ||||
| 			top: 0; | ||||
| 			border-color: currentColor transparent transparent transparent; | ||||
| 			animation: ring 0.5s linear infinite; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -71,6 +71,7 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .xubzgfgb { | ||||
| 	position: relative; | ||||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
| 
 | ||||
|  | @ -82,6 +83,7 @@ export default defineComponent({ | |||
| 	} | ||||
| 
 | ||||
| 	> canvas { | ||||
| 		position: absolute; | ||||
| 		object-fit: cover; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-i | |||
| import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | ||||
| import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; | ||||
| import ImageViewer from './image-viewer.vue'; | ||||
| import ImgWithBlurhash from './img-with-blurhash.vue'; | ||||
| import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; | ||||
| import * as os from '@client/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ | |||
| 	</div> | ||||
| 	<article class="article" @contextmenu.stop="onContextmenu"> | ||||
| 		<header class="header"> | ||||
| 			<MkAvatar class="avatar" :user="appearNote.user"/> | ||||
| 			<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/> | ||||
| 			<div class="body"> | ||||
| 				<div class="top"> | ||||
| 					<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.user.id"> | ||||
|  |  | |||
|  | @ -1,12 +1,15 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<div class="_fullinfo" v-if="empty"> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 
 | ||||
| 	<MkError v-else-if="error" @retry="init()"/> | ||||
| 
 | ||||
| 	<div class="_fullinfo" v-else-if="empty"> | ||||
| 		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 		<div>{{ $ts.noNotes }}</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<MkError v-if="error" @retry="init()"/> | ||||
| 
 | ||||
| 	<div v-else> | ||||
| 		<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | ||||
| 			<MkButton style="margin: 0 auto;" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| 				<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
|  | @ -14,7 +17,7 @@ | |||
| 			</MkButton> | ||||
| 		</div> | ||||
| 
 | ||||
| 	<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> | ||||
| 		<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap"> | ||||
| 			<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> | ||||
| 		</XList> | ||||
| 
 | ||||
|  | @ -25,6 +28,7 @@ | |||
| 			</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -55,11 +59,15 @@ export default defineComponent({ | |||
| 		pagination: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 
 | ||||
| 		prop: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 		}, | ||||
| 		noGap: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['before', 'after'], | ||||
|  | @ -90,3 +98,14 @@ export default defineComponent({ | |||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,6 +1,13 @@ | |||
| <template> | ||||
| <div class="mfcuwfyp _noGap_ _magnetParent"> | ||||
| 	<XList class="notifications _magnetChild" :items="items" v-slot="{ item: notification }"> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 
 | ||||
| 	<MkError v-else-if="error" @retry="init()"/> | ||||
| 
 | ||||
| 	<p class="mfcuwfyp" v-else-if="empty">{{ $ts.noNotifications }}</p> | ||||
| 
 | ||||
| 	<div v-else class="_magnetParent"> | ||||
| 		<XList class="notifications _magnetChild" :items="items" v-slot="{ item: notification }" :no-gap="true"> | ||||
| 			<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/> | ||||
| 			<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/> | ||||
| 		</XList> | ||||
|  | @ -9,11 +16,8 @@ | |||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 		</button> | ||||
| 
 | ||||
| 	<p class="empty" v-if="empty">{{ $ts.noNotifications }}</p> | ||||
| 
 | ||||
| 	<MkError v-if="error" @retry="init()"/> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -120,17 +124,19 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .mfcuwfyp { | ||||
| 	> .empty { | ||||
| 	margin: 0; | ||||
| 	padding: 16px; | ||||
| 	text-align: center; | ||||
| 	color: var(--fg); | ||||
| } | ||||
| 
 | ||||
| 	> .placeholder { | ||||
| 		padding: 32px; | ||||
| 		opacity: 0.3; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <XNotes :class="{ _noGap_: !$store.state.showGapBetweenNotesInTimeline }" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/> | ||||
| <XNotes :no-gap="!$store.state.showGapBetweenNotesInTimeline" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
| 		<div class="title"><slot name="header"></slot></div> | ||||
| 		<div class="sub"> | ||||
| 			<slot name="func"></slot> | ||||
| 			<button class="_button" v-if="bodyTogglable" @click="() => showBody = !showBody"> | ||||
| 			<button class="_button" v-if="foldable" @click="() => showBody = !showBody"> | ||||
| 				<template v-if="showBody"><Fa :icon="faAngleUp"/></template> | ||||
| 				<template v-else><Fa :icon="faAngleDown"/></template> | ||||
| 			</button> | ||||
|  | @ -16,8 +16,11 @@ | |||
| 		@leave="leave" | ||||
| 		@after-leave="afterLeave" | ||||
| 	> | ||||
| 		<div v-show="showBody"> | ||||
| 		<div v-show="showBody" class="content" :class="{ omitted }" ref="content"> | ||||
| 			<slot></slot> | ||||
| 			<button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }"> | ||||
| 				<span>{{ $ts.showMore }}</span> | ||||
| 			</button> | ||||
| 		</div> | ||||
| 	</transition> | ||||
| </div> | ||||
|  | @ -39,7 +42,7 @@ export default defineComponent({ | |||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		bodyTogglable: { | ||||
| 		foldable: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
|  | @ -54,10 +57,17 @@ export default defineComponent({ | |||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		maxHeight: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: null | ||||
| 		}, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showBody: this.expanded, | ||||
| 			omitted: null, | ||||
| 			ignoreOmit: false, | ||||
| 			faAngleUp, faAngleDown | ||||
| 		}; | ||||
| 	}, | ||||
|  | @ -73,10 +83,23 @@ export default defineComponent({ | |||
| 		}, { | ||||
| 			immediate: true | ||||
| 		}); | ||||
| 
 | ||||
| 		this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px'); | ||||
| 
 | ||||
| 		const calcOmit = () => { | ||||
| 			if (this.omitted || this.ignoreOmit || this.maxHeight == null) return; | ||||
| 			const height = this.$refs.content.offsetHeight; | ||||
| 			this.omitted = height > this.maxHeight; | ||||
| 		}; | ||||
| 
 | ||||
| 		calcOmit(); | ||||
| 		new ResizeObserver((entries, observer) => { | ||||
| 			calcOmit(); | ||||
| 		}).observe(this.$refs.content); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		toggleContent(show: boolean) { | ||||
| 			if (!this.bodyTogglable) return; | ||||
| 			if (!this.foldable) return; | ||||
| 			this.showBody = show; | ||||
| 		}, | ||||
| 
 | ||||
|  | @ -127,7 +150,7 @@ export default defineComponent({ | |||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 
 | ||||
| 		> div { | ||||
| 		> .content { | ||||
| 			overflow: auto; | ||||
| 		} | ||||
| 	} | ||||
|  | @ -169,12 +192,35 @@ export default defineComponent({ | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> div { | ||||
| 		> ::v-deep(._content) { | ||||
| 			padding: 24px; | ||||
| 	> .content { | ||||
| 		&.omitted { | ||||
| 			position: relative; | ||||
| 			max-height: var(--maxHeight); | ||||
| 			overflow: hidden; | ||||
| 
 | ||||
| 			& + ._content { | ||||
| 				border-top: solid 0.5px var(--divider); | ||||
| 			> .fade { | ||||
| 				display: block; | ||||
| 				position: absolute; | ||||
| 				bottom: 0; | ||||
| 				left: 0; | ||||
| 				width: 100%; | ||||
| 				height: 64px; | ||||
| 				background: linear-gradient(0deg, var(--panel), var(--X15)); | ||||
| 
 | ||||
| 				> span { | ||||
| 					display: inline-block; | ||||
| 					background: var(--panel); | ||||
| 					padding: 6px 10px; | ||||
| 					font-size: 0.8em; | ||||
| 					border-radius: 999px; | ||||
| 					box-shadow: 0 2px 6px rgb(0 0 0 / 20%); | ||||
| 				} | ||||
| 
 | ||||
| 				&:hover { | ||||
| 					> span { | ||||
| 						background: var(--panelHighlight); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | @ -187,10 +233,7 @@ export default defineComponent({ | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> div { | ||||
| 			> ::v-deep(._content) { | ||||
| 				padding: 16px; | ||||
| 			} | ||||
| 		> .content { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ export default defineComponent({ | |||
| 			const contentHeight = this.$refs.content.offsetHeight; | ||||
| 
 | ||||
| 			let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); | ||||
| 			let top = rect.top + window.pageYOffset + this.source.offsetHeight; | ||||
| 			let top = rect.top + window.pageYOffset - contentHeight; | ||||
| 
 | ||||
| 			left -= (this.$el.offsetWidth / 2); | ||||
| 
 | ||||
|  | @ -47,9 +47,9 @@ export default defineComponent({ | |||
| 				left = window.innerWidth - contentWidth + window.pageXOffset - 1; | ||||
| 			} | ||||
| 
 | ||||
| 			if (top + contentHeight - window.pageYOffset > window.innerHeight) { | ||||
| 				top = rect.top + window.pageYOffset - contentHeight; | ||||
| 				this.$refs.content.style.transformOrigin = 'center bottom'; | ||||
| 			if (top - window.pageYOffset < 0) { | ||||
| 				top = rect.top + window.pageYOffset + this.source.offsetHeight; | ||||
| 				this.$refs.content.style.transformOrigin = 'center top'; | ||||
| 			} | ||||
| 
 | ||||
| 			this.$el.style.left = left + 'px'; | ||||
|  | @ -81,6 +81,6 @@ export default defineComponent({ | |||
| 	text-align: center; | ||||
| 	border-radius: 4px; | ||||
| 	pointer-events: none; | ||||
| 	transform-origin: center top; | ||||
| 	transform-origin: center bottom; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div class="_panel vjnjpkug"> | ||||
| 	<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> | ||||
| 	<MkAvatar class="avatar" :user="user" :disable-preview="true"/> | ||||
| 	<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> | ||||
| 	<div class="title"> | ||||
| 		<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> | ||||
| 		<p class="username"><MkAcct :user="user"/></p> | ||||
|  |  | |||
							
								
								
									
										50
									
								
								src/client/components/user-online-indicator.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/client/components/user-online-indicator.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | |||
| <template> | ||||
| <div class="fzgwjkgc" :class="user.onlineStatus" v-tooltip="text"></div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		text(): string { | ||||
| 			switch (this.user.onlineStatus) { | ||||
| 				case 'online': return this.$ts.online; | ||||
| 				case 'active': return this.$ts.active; | ||||
| 				case 'offline': return this.$ts.offline; | ||||
| 				case 'unknown': return this.$ts.unknown; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fzgwjkgc { | ||||
| 	box-shadow: 0 0 0 3px var(--panel); | ||||
| 	border-radius: 100%; | ||||
| 
 | ||||
| 	&.online { | ||||
| 		background: #58d4c9; | ||||
| 	} | ||||
| 
 | ||||
| 	&.active { | ||||
| 		background: #e4bc48; | ||||
| 	} | ||||
| 
 | ||||
| 	&.offline { | ||||
| 		background: #ea5353; | ||||
| 	} | ||||
| 
 | ||||
| 	&.unknown { | ||||
| 		background: #888; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -3,7 +3,7 @@ | |||
| 	<div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }"> | ||||
| 		<div v-if="fetched" class="info"> | ||||
| 			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> | ||||
| 			<MkAvatar class="avatar" :user="user" :disable-preview="true"/> | ||||
| 			<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> | ||||
| 			<div class="title"> | ||||
| 				<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> | ||||
| 				<p class="username"><MkAcct :user="user"/></p> | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ | |||
| 	<div class="tbhwbxda _section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }"> | ||||
| 		<div class="users" v-if="users.length > 0"> | ||||
| 			<div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()"> | ||||
| 				<MkAvatar :user="user" class="avatar"/> | ||||
| 				<MkAvatar :user="user" class="avatar" :show-indicator="true"/> | ||||
| 				<div class="body"> | ||||
| 					<MkUserName :user="user" class="name"/> | ||||
| 					<MkAcct :user="user" class="acct"/> | ||||
|  | @ -31,7 +31,7 @@ | |||
| 	<div class="tbhwbxda _section recent" v-if="username == '' && host == ''"> | ||||
| 		<div class="users"> | ||||
| 			<div class="user" v-for="user in recentUsers" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()"> | ||||
| 				<MkAvatar :user="user" class="avatar"/> | ||||
| 				<MkAvatar :user="user" class="avatar" :show-indicator="true"/> | ||||
| 				<div class="body"> | ||||
| 					<MkUserName :user="user" class="name"/> | ||||
| 					<MkAcct :user="user" class="acct"/> | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
| 
 | ||||
| 	<div class="users"> | ||||
| 		<MkA v-for="item in items" class="user" :key="item.id" :to="userPage(extract ? extract(item) : item)"> | ||||
| 			<MkAvatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true"/> | ||||
| 			<MkAvatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true" :show-indicator="true"/> | ||||
| 			<div class="body"> | ||||
| 				<MkUserName :user="extract ? extract(item) : item" class="name"/> | ||||
| 				<MkAcct :user="extract ? extract(item) : item" class="acct"/> | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
| 
 | ||||
| 	<XPostForm :channel="channel" class="post-form _content _panel _gap" fixed v-if="$i"/> | ||||
| 
 | ||||
| 	<XTimeline class="_content _gap _noGap_" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/> | ||||
| 	<XTimeline class="_content _gap" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 			<Mfm :text="clip.description" :is-note="false" :i="$i"/> | ||||
| 		</div> | ||||
| 		<div class="user"> | ||||
| 			<MkAvatar :user="clip.user" class="avatar"/> <MkUserName :user="clip.user" :nowrap="false"/> | ||||
| 			<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ | |||
| 			<header><span>{{ $ts.exploreFediverse }}</span></header> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<MkFolder :body-togglable="true" :expanded="false" ref="tags" class="_gap"> | ||||
| 		<MkFolder :foldable="true" :expanded="false" ref="tags" class="_gap"> | ||||
| 			<template #header><Fa :icon="faHashtag" fixed-width style="margin-right: 0.5em;"/>{{ $ts.popularTags }}</template> | ||||
| 
 | ||||
| 			<div class="vxjfqztj"> | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ | |||
| 		</template> | ||||
| 		<template #default="{items}"> | ||||
| 			<div class="user _panel" v-for="req in items" :key="req.id"> | ||||
| 				<MkAvatar class="avatar" :user="req.follower"/> | ||||
| 				<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> | ||||
| 				<div class="body"> | ||||
| 					<div class="name"> | ||||
| 						<MkA class="name" :to="userPage(req.follower)" v-user-preview="req.follower.id"><MkUserName :user="req.follower"/></MkA> | ||||
|  |  | |||
							
								
								
									
										464
									
								
								src/client/pages/instance-info.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										464
									
								
								src/client/pages/instance-info.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,464 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormGroup v-if="instance"> | ||||
| 		<template #label>{{ instance.host }}</template> | ||||
| 		<FormGroup> | ||||
| 			<div class="_formItem"> | ||||
| 				<div class="_formPanel fnfelxur"> | ||||
| 					<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>Name</template> | ||||
| 				<template #value><span class="_monospace">{{ instance.name || `(${$ts.unknown})` }}</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 
 | ||||
| 		<FormTextarea readonly :value="instance.description"> | ||||
| 			<span>{{ $ts.description }}</span> | ||||
| 		</FormTextarea> | ||||
| 
 | ||||
| 		<FormGroup> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.software }}</template> | ||||
| 				<template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }}</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.version }}</template> | ||||
| 				<template #value><span class="_monospace">{{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 		<FormGroup> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.administrator }}</template> | ||||
| 				<template #value><span class="_monospace">{{ instance.maintainerName || `(${$ts.unknown})` }}</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.contact }}</template> | ||||
| 				<template #value><span class="_monospace">{{ instance.maintainerEmail || `(${$ts.unknown})` }}</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 		<FormGroup> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.latestRequestSentAt }}</template> | ||||
| 				<template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.latestStatus }}</template> | ||||
| 				<template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.latestRequestReceivedAt }}</template> | ||||
| 				<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 		<FormGroup> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>Open Registrations</template> | ||||
| 				<template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 		<div class="_formItem"> | ||||
| 			<div class="_formLabel">{{ $ts.statistics }}</div> | ||||
| 			<div class="_formPanel cmhjzshl"> | ||||
| 				<div class="selects"> | ||||
| 					<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;"> | ||||
| 						<option value="requests">{{ $ts._instanceCharts.requests }}</option> | ||||
| 						<option value="users">{{ $ts._instanceCharts.users }}</option> | ||||
| 						<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option> | ||||
| 						<option value="notes">{{ $ts._instanceCharts.notes }}</option> | ||||
| 						<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option> | ||||
| 						<option value="ff">{{ $ts._instanceCharts.ff }}</option> | ||||
| 						<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option> | ||||
| 						<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> | ||||
| 						<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> | ||||
| 						<option value="drive-files">{{ $ts._instanceCharts.files }}</option> | ||||
| 						<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> | ||||
| 					</MkSelect> | ||||
| 					<MkSelect v-model:value="chartSpan" style="margin: 0;"> | ||||
| 						<option value="hour">{{ $ts.perHour }}</option> | ||||
| 						<option value="day">{{ $ts.perDay }}</option> | ||||
| 					</MkSelect> | ||||
| 				</div> | ||||
| 				<div class="chart"> | ||||
| 					<canvas :ref="setChart"></canvas> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<FormGroup> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.registeredAt }}</template> | ||||
| 				<template #value><MkTime mode="detail" :time="instance.caughtAt"/></template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.updatedAt }}</template> | ||||
| 				<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 		<FormObjectView tall :value="instance"> | ||||
| 			<span>Raw</span> | ||||
| 		</FormObjectView> | ||||
| 		<FormGroup> | ||||
| 			<FormLink :to="`https://${host}/.well-known/host-meta`" external>host-meta</FormLink> | ||||
| 			<FormLink :to="`https://${host}/.well-known/host-meta.json`" external>host-meta.json</FormLink> | ||||
| 			<FormLink :to="`https://${host}/.well-known/nodeinfo`" external>nodeinfo</FormLink> | ||||
| 		</FormGroup> | ||||
| 		<FormSuspense :p="dnsPromiseFactory" v-slot="{ result: dns }"> | ||||
| 			<FormGroup> | ||||
| 				<template #label>DNS</template> | ||||
| 				<FormKeyValueView v-for="record in dns.a" :key="record"> | ||||
| 					<template #key>A</template> | ||||
| 					<template #value><span class="_monospace">{{ record }}</span></template> | ||||
| 				</FormKeyValueView> | ||||
| 				<FormKeyValueView v-for="record in dns.aaaa" :key="record"> | ||||
| 					<template #key>AAAA</template> | ||||
| 					<template #value><span class="_monospace">{{ record }}</span></template> | ||||
| 				</FormKeyValueView> | ||||
| 				<FormKeyValueView v-for="record in dns.cname" :key="record"> | ||||
| 					<template #key>CNAME</template> | ||||
| 					<template #value><span class="_monospace">{{ record }}</span></template> | ||||
| 				</FormKeyValueView> | ||||
| 				<FormKeyValueView v-for="record in dns.txt"> | ||||
| 					<template #key>TXT</template> | ||||
| 					<template #value><span class="_monospace">{{ record[0] }}</span></template> | ||||
| 				</FormKeyValueView> | ||||
| 			</FormGroup> | ||||
| 		</FormSuspense> | ||||
| 	</FormGroup> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineAsyncComponent, defineComponent } from 'vue'; | ||||
| import { faExternalLinkAlt, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import Chart from 'chart.js'; | ||||
| import FormObjectView from '@client/components/form/object-view.vue'; | ||||
| import FormTextarea from '@client/components/form/textarea.vue'; | ||||
| import FormLink from '@client/components/form/link.vue'; | ||||
| import FormBase from '@client/components/form/base.vue'; | ||||
| import FormGroup from '@client/components/form/group.vue'; | ||||
| import FormButton from '@client/components/form/button.vue'; | ||||
| import FormKeyValueView from '@client/components/form/key-value-view.vue'; | ||||
| import FormSuspense from '@client/components/form/suspense.vue'; | ||||
| import MkSelect from '@client/components/ui/select.vue'; | ||||
| import * as os from '@client/os'; | ||||
| import number from '@client/filters/number'; | ||||
| import bytes from '@client/filters/bytes'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| import { url } from '@client/config'; | ||||
| 
 | ||||
| const chartLimit = 90; | ||||
| const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); | ||||
| const negate = arr => arr.map(x => -x); | ||||
| const alpha = hex => { | ||||
| 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||
| 	const r = parseInt(result[1], 16); | ||||
| 	const g = parseInt(result[2], 16); | ||||
| 	const b = parseInt(result[3], 16); | ||||
| 	return `rgba(${r}, ${g}, ${b}, 0.1)`; | ||||
| }; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormBase, | ||||
| 		FormTextarea, | ||||
| 		FormObjectView, | ||||
| 		FormButton, | ||||
| 		FormLink, | ||||
| 		FormGroup, | ||||
| 		FormKeyValueView, | ||||
| 		FormSuspense, | ||||
| 		MkSelect, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		host: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.instanceInfo, | ||||
| 				icon: faInfoCircle, | ||||
| 				actions: [{ | ||||
| 					text: `https://${this.host}`, | ||||
| 					icon: faExternalLinkAlt, | ||||
| 					handler: () => { | ||||
| 						window.open(`https://${this.host}`, '_blank'); | ||||
| 					} | ||||
| 				}], | ||||
| 			}, | ||||
| 			instance: null, | ||||
| 			dnsPromiseFactory: () => os.api('federation/dns', { | ||||
| 				host: this.host | ||||
| 			}), | ||||
| 			now: null, | ||||
| 			canvas: null, | ||||
| 			chart: null, | ||||
| 			chartInstance: null, | ||||
| 			chartSrc: 'requests', | ||||
| 			chartSpan: 'hour', | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		data(): any { | ||||
| 			if (this.chart == null) return null; | ||||
| 			switch (this.chartSrc) { | ||||
| 				case 'requests': return this.requestsChart(); | ||||
| 				case 'users': return this.usersChart(false); | ||||
| 				case 'users-total': return this.usersChart(true); | ||||
| 				case 'notes': return this.notesChart(false); | ||||
| 				case 'notes-total': return this.notesChart(true); | ||||
| 				case 'ff': return this.ffChart(false); | ||||
| 				case 'ff-total': return this.ffChart(true); | ||||
| 				case 'drive-usage': return this.driveUsageChart(false); | ||||
| 				case 'drive-usage-total': return this.driveUsageChart(true); | ||||
| 				case 'drive-files': return this.driveFilesChart(false); | ||||
| 				case 'drive-files-total': return this.driveFilesChart(true); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		stats(): any[] { | ||||
| 			const stats = | ||||
| 				this.chartSpan == 'day' ? this.chart.perDay : | ||||
| 				this.chartSpan == 'hour' ? this.chart.perHour : | ||||
| 				null; | ||||
| 
 | ||||
| 			return stats; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		chartSrc() { | ||||
| 			this.renderChart(); | ||||
| 		}, | ||||
| 
 | ||||
| 		chartSpan() { | ||||
| 			this.renderChart(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		number, | ||||
| 		bytes, | ||||
| 
 | ||||
| 		async fetch() { | ||||
| 			this.instance = await os.api('federation/show-instance', { | ||||
| 				host: this.host | ||||
| 			}); | ||||
| 
 | ||||
| 			this.now = new Date(); | ||||
| 
 | ||||
| 			const [perHour, perDay] = await Promise.all([ | ||||
| 				os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }), | ||||
| 				os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }), | ||||
| 			]); | ||||
| 
 | ||||
| 			const chart = { | ||||
| 				perHour: perHour, | ||||
| 				perDay: perDay | ||||
| 			}; | ||||
| 
 | ||||
| 			this.chart = chart; | ||||
| 
 | ||||
| 			this.renderChart(); | ||||
| 		}, | ||||
| 
 | ||||
| 		setChart(el) { | ||||
| 			this.canvas = el; | ||||
| 		}, | ||||
| 
 | ||||
| 		renderChart() { | ||||
| 			if (this.chartInstance) { | ||||
| 				this.chartInstance.destroy(); | ||||
| 			} | ||||
| 
 | ||||
| 			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 			this.chartInstance = new Chart(this.canvas, { | ||||
| 				type: 'line', | ||||
| 				data: { | ||||
| 					labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), | ||||
| 					datasets: this.data.series.map(x => ({ | ||||
| 						label: x.name, | ||||
| 						data: x.data.slice().reverse(), | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: x.color, | ||||
| 						backgroundColor: alpha(x.color), | ||||
| 					})) | ||||
| 				}, | ||||
| 				options: { | ||||
| 					aspectRatio: 2.5, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 16 | ||||
| 						} | ||||
| 					}, | ||||
| 					legend: { | ||||
| 						position: 'bottom', | ||||
| 						labels: { | ||||
| 							boxWidth: 16, | ||||
| 						} | ||||
| 					}, | ||||
| 					scales: { | ||||
| 						xAxes: [{ | ||||
| 							gridLines: { | ||||
| 								display: false | ||||
| 							}, | ||||
| 							ticks: { | ||||
| 								display: false | ||||
| 							} | ||||
| 						}], | ||||
| 						yAxes: [{ | ||||
| 							position: 'right', | ||||
| 							ticks: { | ||||
| 								display: false | ||||
| 							} | ||||
| 						}] | ||||
| 					}, | ||||
| 					tooltips: { | ||||
| 						intersect: false, | ||||
| 						mode: 'index', | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		getDate(ago: number) { | ||||
| 			const y = this.now.getFullYear(); | ||||
| 			const m = this.now.getMonth(); | ||||
| 			const d = this.now.getDate(); | ||||
| 			const h = this.now.getHours(); | ||||
| 
 | ||||
| 			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); | ||||
| 		}, | ||||
| 
 | ||||
| 		format(arr) { | ||||
| 			return arr; | ||||
| 		}, | ||||
| 
 | ||||
| 		requestsChart(): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'In', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(this.stats.requests.received) | ||||
| 				}, { | ||||
| 					name: 'Out (succ)', | ||||
| 					color: '#00E396', | ||||
| 					data: this.format(this.stats.requests.succeeded) | ||||
| 				}, { | ||||
| 					name: 'Out (fail)', | ||||
| 					color: '#FEB019', | ||||
| 					data: this.format(this.stats.requests.failed) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		usersChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Users', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.users.total | ||||
| 						: sum(this.stats.users.inc, negate(this.stats.users.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		notesChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Notes', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.notes.total | ||||
| 						: sum(this.stats.notes.inc, negate(this.stats.notes.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		ffChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Following', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.following.total | ||||
| 						: sum(this.stats.following.inc, negate(this.stats.following.dec)) | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Followers', | ||||
| 					color: '#00E396', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.followers.total | ||||
| 						: sum(this.stats.followers.inc, negate(this.stats.followers.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveUsageChart(total: boolean): any { | ||||
| 			return { | ||||
| 				bytes: true, | ||||
| 				series: [{ | ||||
| 					name: 'Drive usage', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.drive.totalUsage | ||||
| 						: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveFilesChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Drive files', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.drive.totalFiles | ||||
| 						: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fnfelxur { | ||||
| 	padding: 16px; | ||||
| 
 | ||||
| 	> img { | ||||
| 		display: block; | ||||
| 		margin: auto; | ||||
| 		height: 64px; | ||||
| 		border-radius: 8px; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .cmhjzshl { | ||||
| 	> .selects { | ||||
| 		display: flex; | ||||
| 		padding: 16px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -36,7 +36,7 @@ | |||
| 			<MkPagination :pagination="pagination" #default="{items}" ref="reports" style="margin-top: var(--margin);"> | ||||
| 				<div class="bcekxzvu _card _gap" v-for="report in items" :key="report.id"> | ||||
| 					<div class="_content target"> | ||||
| 						<MkAvatar class="avatar" :user="report.targetUser"/> | ||||
| 						<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/> | ||||
| 						<div class="info"> | ||||
| 							<MkUserName class="name" :user="report.targetUser"/> | ||||
| 							<div class="acct">@{{ acct(report.targetUser) }}</div> | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
| 		<template #header><Fa :icon="faHeartbeat"/> {{ $ts.metrics }}</template> | ||||
| 		<div class="_section" style="padding: 0 var(--margin);"> | ||||
| 			<div class="_content"> | ||||
| 				<MkContainer :body-togglable="false" class="_gap"> | ||||
| 				<MkContainer :foldable="false" class="_gap"> | ||||
| 					<template #header><Fa :icon="faMicrochip"/>{{ $ts.cpuAndMemory }}</template> | ||||
| 					<!-- | ||||
| 					<template #func> | ||||
|  | @ -27,7 +27,7 @@ | |||
| 					</div> | ||||
| 				</MkContainer> | ||||
| 
 | ||||
| 				<MkContainer :body-togglable="false" class="_gap"> | ||||
| 				<MkContainer :foldable="false" class="_gap"> | ||||
| 					<template #header><Fa :icon="faHdd"/> {{ $ts.disk }}</template> | ||||
| 					<!-- | ||||
| 					<template #func> | ||||
|  | @ -50,7 +50,7 @@ | |||
| 					</div> | ||||
| 				</MkContainer> | ||||
| 
 | ||||
| 				<MkContainer :body-togglable="false" class="_gap"> | ||||
| 				<MkContainer :foldable="false" class="_gap"> | ||||
| 					<template #header><Fa :icon="faExchangeAlt"/> {{ $ts.network }}</template> | ||||
| 					<!-- | ||||
| 					<template #func> | ||||
|  | @ -78,7 +78,7 @@ | |||
| 		<template #header><Fa :icon="faClipboardList"/> {{ $ts.jobQueue }}</template> | ||||
| 
 | ||||
| 		<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }"> | ||||
| 			<MkContainer :body-togglable="false" :scrollable="true" :resize-base-el="() => $el"> | ||||
| 			<MkContainer :foldable="false" :scrollable="true" :resize-base-el="() => $el"> | ||||
| 				<template #header><Fa :icon="faExclamationTriangle"/> {{ $ts.delayed }}</template> | ||||
| 
 | ||||
| 				<div class="_content"> | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
| 		<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }"> | ||||
| 			<MkInstanceStats :chart-limit="300" :detailed="true" class="_gap" ref="stats"/> | ||||
| 
 | ||||
| 			<MkContainer :body-togglable="true" class="_gap"> | ||||
| 			<MkContainer :foldable="true" class="_gap"> | ||||
| 				<template #header><Fa :icon="faInfoCircle"/>{{ $ts.instanceInfo }}</template> | ||||
| 
 | ||||
| 				<div class="_content"> | ||||
|  | @ -19,7 +19,7 @@ | |||
| 				</div> | ||||
| 			</MkContainer> | ||||
| 			 | ||||
| 			<MkContainer :body-togglable="true" :scrollable="true" class="_gap" style="height: 300px;"> | ||||
| 			<MkContainer :foldable="true" :scrollable="true" class="_gap" style="height: 300px;"> | ||||
| 				<template #header><Fa :icon="faDatabase"/>{{ $ts.database }}</template> | ||||
| 
 | ||||
| 				<div class="_content" v-if="dbInfo"> | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
| 	<div class="vrcsvlkm" v-if="user && info"> | ||||
| 		<div class="_section"> | ||||
| 			<div class="banner" :style="bannerStyle"> | ||||
| 				<MkAvatar class="avatar" :user="user"/> | ||||
| 				<MkAvatar class="avatar" :user="user" :show-indicator="true"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="_section"> | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ | |||
| 
 | ||||
| 			<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users"> | ||||
| 				<button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)"> | ||||
| 					<MkAvatar class="avatar" :user="user" :disable-link="true"/> | ||||
| 					<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/> | ||||
| 					<div class="body"> | ||||
| 						<header> | ||||
| 							<MkUserName class="name" :user="user"/> | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ | |||
| 			v-anim="i" | ||||
| 		> | ||||
| 			<div> | ||||
| 				<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/> | ||||
| 				<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/> | ||||
| 				<header v-if="message.groupId"> | ||||
| 					<span class="name">{{ message.group.name }}</span> | ||||
| 					<MkTime :time="message.createdAt" class="time"/> | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <div class="thvuemwp" :class="{ isMe }" v-size="{ max: [400, 500] }"> | ||||
| 	<MkAvatar class="avatar" :user="message.user"/> | ||||
| 	<MkAvatar class="avatar" :user="message.user" :show-indicator="true"/> | ||||
| 	<div class="content"> | ||||
| 		<div class="balloon" :class="{ noText: message.text == null }" > | ||||
| 			<button class="delete-button" v-if="isMe" :title="$ts.delete" @click="del"> | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ | |||
| 			<div class="_content"> | ||||
| 				<div class="users"> | ||||
| 					<div class="user _panel" v-for="user in users" :key="user.id"> | ||||
| 						<MkAvatar :user="user" class="avatar"/> | ||||
| 						<MkAvatar :user="user" class="avatar" :show-indicator="true"/> | ||||
| 						<div class="body"> | ||||
| 							<MkUserName :user="user" class="name"/> | ||||
| 							<MkAcct :user="user" class="acct"/> | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ | |||
| 			<div class="_content"> | ||||
| 				<div class="users"> | ||||
| 					<div class="user _panel" v-for="user in users" :key="user.id"> | ||||
| 						<MkAvatar :user="user" class="avatar"/> | ||||
| 						<MkAvatar :user="user" class="avatar" :show-indicator="true"/> | ||||
| 						<div class="body"> | ||||
| 							<MkUserName :user="user" class="name"/> | ||||
| 							<MkAcct :user="user" class="acct"/> | ||||
|  |  | |||
|  | @ -1,8 +1,9 @@ | |||
| <template> | ||||
| <div class="fcuexfpr _root"> | ||||
| 	<div v-if="note" class="note" v-anim> | ||||
| 	<transition name="fade" mode="out-in"> | ||||
| 		<div v-if="note" class="note"> | ||||
| 			<div class="_gap" v-if="showNext"> | ||||
| 			<XNotes class="_content _noGap_" :pagination="next"/> | ||||
| 				<XNotes class="_content" :pagination="next" :no-gap="true"/> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="main _gap"> | ||||
|  | @ -17,7 +18,7 @@ | |||
| 						<b>{{ item.name }}</b> | ||||
| 						<div v-if="item.description" class="description">{{ item.description }}</div> | ||||
| 						<div class="user"> | ||||
| 						<MkAvatar :user="item.user" class="avatar"/> <MkUserName :user="item.user" :nowrap="false"/> | ||||
| 							<MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/> | ||||
| 						</div> | ||||
| 					</MkA> | ||||
| 				</div> | ||||
|  | @ -25,13 +26,12 @@ | |||
| 			</div> | ||||
| 
 | ||||
| 			<div class="_gap" v-if="showPrev"> | ||||
| 			<XNotes class="_content _noGap_" :pagination="prev"/> | ||||
| 				<XNotes class="_content" :pagination="prev" :no-gap="true"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 	<div v-if="error"> | ||||
| 		<MkError @retry="fetch()"/> | ||||
| 	</div> | ||||
| 		<MkError v-else-if="error" @retry="fetch()"/> | ||||
| 		<MkLoading v-else/> | ||||
| 	</transition> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -106,6 +106,7 @@ export default defineComponent({ | |||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.note = null; | ||||
| 			os.api('notes/show', { | ||||
| 				noteId: this.noteId | ||||
| 			}).then(note => { | ||||
|  | @ -138,6 +139,15 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .fcuexfpr { | ||||
| 	> .note { | ||||
| 		> .main { | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
| 		<MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<MkContainer :body-togglable="true" :expanded="true" class="_gap"> | ||||
| 	<MkContainer :foldable="true" :expanded="true" class="_gap"> | ||||
| 		<template #header><Fa :icon="faCog"/> {{ $ts._pages.pageSetting }}</template> | ||||
| 		<div style="padding: 16px;"> | ||||
| 			<MkInput v-model:value="title"> | ||||
|  | @ -44,7 +44,7 @@ | |||
| 		</div> | ||||
| 	</MkContainer> | ||||
| 
 | ||||
| 	<MkContainer :body-togglable="true" :expanded="true" class="_gap"> | ||||
| 	<MkContainer :foldable="true" :expanded="true" class="_gap"> | ||||
| 		<template #header><Fa :icon="faStickyNote"/> {{ $ts._pages.contents }}</template> | ||||
| 		<div style="padding: 16px;"> | ||||
| 			<XBlocks class="content" v-model:value="content" :hpml="hpml"/> | ||||
|  | @ -53,7 +53,7 @@ | |||
| 		</div> | ||||
| 	</MkContainer> | ||||
| 
 | ||||
| 	<MkContainer :body-togglable="true" class="_gap"> | ||||
| 	<MkContainer :foldable="true" class="_gap"> | ||||
| 		<template #header><Fa :icon="faMagic"/> {{ $ts._pages.variables }}</template> | ||||
| 		<div class="qmuvgica"> | ||||
| 			<XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> | ||||
|  | @ -74,7 +74,7 @@ | |||
| 		</div> | ||||
| 	</MkContainer> | ||||
| 
 | ||||
| 	<MkContainer :body-togglable="true" :expanded="true" class="_gap"> | ||||
| 	<MkContainer :foldable="true" :expanded="true" class="_gap"> | ||||
| 		<template #header><Fa :icon="faCode"/> {{ $ts.script }}</template> | ||||
| 		<div> | ||||
| 			<MkTextarea class="_code" v-model:value="script"/> | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ | |||
| 			<template #header>{{ $ts.invitations }}</template> | ||||
| 			<div class="nfcacttm"> | ||||
| 				<button class="invitation _panel _button" v-for="invitation in invitations" tabindex="-1" @click="accept(invitation)"> | ||||
| 					<MkAvatar class="avatar" :user="invitation.parent"/> | ||||
| 					<MkAvatar class="avatar" :user="invitation.parent" :show-indicator="true"/> | ||||
| 					<span class="name"><b><MkUserName :user="invitation.parent"/></b></span> | ||||
| 					<span class="username">@{{ invitation.parent.username }}</span> | ||||
| 					<MkTime :time="invitation.createdAt" class="time"/> | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 		<MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><Fa :icon="faPlay"/></MkButton> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<MkContainer :body-togglable="true" class="_gap"> | ||||
| 	<MkContainer :foldable="true" class="_gap"> | ||||
| 		<template #header><Fa fixed-width/>{{ $ts.output }}</template> | ||||
| 		<div class="bepmlvbi"> | ||||
| 			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> | ||||
|  |  | |||
|  | @ -5,6 +5,10 @@ | |||
| 		<FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch> | ||||
| 		<template #caption>{{ $ts.lockedAccountInfo }}</template> | ||||
| 	</FormGroup> | ||||
| 	<FormSwitch v-model:value="hideOnlineStatus" @update:value="save()"> | ||||
| 		{{ $ts.hideOnlineStatus }} | ||||
| 		<template #desc>{{ $ts.hideOnlineStatusDescription }}</template> | ||||
| 	</FormSwitch> | ||||
| 	<FormSwitch v-model:value="noCrawle" @update:value="save()"> | ||||
| 		{{ $ts.noCrawle }} | ||||
| 		<template #desc>{{ $ts.noCrawleDescription }}</template> | ||||
|  | @ -58,6 +62,7 @@ export default defineComponent({ | |||
| 			autoAcceptFollowed: false, | ||||
| 			noCrawle: false, | ||||
| 			isExplorable: false, | ||||
| 			hideOnlineStatus: false, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -72,6 +77,7 @@ export default defineComponent({ | |||
| 		this.autoAcceptFollowed = this.$i.autoAcceptFollowed; | ||||
| 		this.noCrawle = this.$i.noCrawle; | ||||
| 		this.isExplorable = this.$i.isExplorable; | ||||
| 		this.hideOnlineStatus = this.$i.hideOnlineStatus; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
|  | @ -85,6 +91,7 @@ export default defineComponent({ | |||
| 				autoAcceptFollowed: !!this.autoAcceptFollowed, | ||||
| 				noCrawle: !!this.noCrawle, | ||||
| 				isExplorable: !!this.isExplorable, | ||||
| 				hideOnlineStatus: !!this.hideOnlineStatus, | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										125
									
								
								src/client/pages/user-ap-info.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/client/pages/user-ap-info.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,125 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormSuspense :p="apPromiseFactory" v-slot="{ result: ap }"> | ||||
| 		<FormGroup> | ||||
| 			<template #label>ActivityPub</template> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>Type</template> | ||||
| 				<template #value><span class="_monospace">{{ ap.type }}</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>URI</template> | ||||
| 				<template #value><span class="_monospace">{{ ap.id }}</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>URL</template> | ||||
| 				<template #value><span class="_monospace">{{ ap.url }}</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormGroup> | ||||
| 				<FormKeyValueView> | ||||
| 					<template #key>Inbox</template> | ||||
| 					<template #value><span class="_monospace">{{ ap.inbox }}</span></template> | ||||
| 				</FormKeyValueView> | ||||
| 				<FormKeyValueView> | ||||
| 					<template #key>Shared Inbox</template> | ||||
| 					<template #value><span class="_monospace">{{ ap.sharedInbox || ap.endpoints.sharedInbox }}</span></template> | ||||
| 				</FormKeyValueView> | ||||
| 				<FormKeyValueView> | ||||
| 					<template #key>Outbox</template> | ||||
| 					<template #value><span class="_monospace">{{ ap.outbox }}</span></template> | ||||
| 				</FormKeyValueView> | ||||
| 			</FormGroup> | ||||
| 			<FormTextarea readonly tall code pre :value="ap.publicKey.publicKeyPem"> | ||||
| 				<span>Public Key</span> | ||||
| 			</FormTextarea> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>Discoverable</template> | ||||
| 				<template #value>{{ ap.discoverable ? $ts.yes : $ts.no }}</template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>ManuallyApprovesFollowers</template> | ||||
| 				<template #value>{{ ap.manuallyApprovesFollowers ? $ts.yes : $ts.no }}</template> | ||||
| 			</FormKeyValueView> | ||||
| 			<FormObjectView tall :value="ap"> | ||||
| 				<span>Raw</span> | ||||
| 			</FormObjectView> | ||||
| 			<FormGroup> | ||||
| 				<FormLink :to="`https://${user.host}/.well-known/webfinger?resource=acct:${user.username}`" external>WebFinger</FormLink> | ||||
| 			</FormGroup> | ||||
| 			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> | ||||
| 			<FormKeyValueView v-else> | ||||
| 				<template #key>{{ $ts.instanceInfo }}</template> | ||||
| 				<template #value>(Local user)</template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 	</FormSuspense> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineAsyncComponent, defineComponent } from 'vue'; | ||||
| import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import FormObjectView from '@client/components/form/object-view.vue'; | ||||
| import FormTextarea from '@client/components/form/textarea.vue'; | ||||
| import FormLink from '@client/components/form/link.vue'; | ||||
| import FormBase from '@client/components/form/base.vue'; | ||||
| import FormGroup from '@client/components/form/group.vue'; | ||||
| import FormButton from '@client/components/form/button.vue'; | ||||
| import FormKeyValueView from '@client/components/form/key-value-view.vue'; | ||||
| import FormSuspense from '@client/components/form/suspense.vue'; | ||||
| import * as os from '@client/os'; | ||||
| import number from '@client/filters/number'; | ||||
| import bytes from '@client/filters/bytes'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| import { url } from '@client/config'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormBase, | ||||
| 		FormTextarea, | ||||
| 		FormObjectView, | ||||
| 		FormButton, | ||||
| 		FormLink, | ||||
| 		FormGroup, | ||||
| 		FormKeyValueView, | ||||
| 		FormSuspense, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		userId: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.userInfo, | ||||
| 				icon: faInfoCircle | ||||
| 			}, | ||||
| 			user: null, | ||||
| 			apPromiseFactory: null, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		number, | ||||
| 		bytes, | ||||
| 
 | ||||
| 		async fetch() { | ||||
| 			this.user = await os.api('users/show', { | ||||
| 				userId: this.userId | ||||
| 			}); | ||||
| 
 | ||||
| 			this.apPromiseFactory = () => os.api('ap/get', { | ||||
| 				uri: this.user.uri || `${url}/users/${this.user.id}` | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										103
									
								
								src/client/pages/user-info.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/client/pages/user-info.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,103 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormGroup v-if="user"> | ||||
| 		<template #label><MkAcct :user="user"/></template> | ||||
| 
 | ||||
| 		<FormKeyValueView> | ||||
| 			<template #key>ID</template> | ||||
| 			<template #value><span class="_monospace">{{ user.id }}</span></template> | ||||
| 		</FormKeyValueView> | ||||
| 
 | ||||
| 		<FormGroup> | ||||
| 			<FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> | ||||
| 
 | ||||
| 			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> | ||||
| 			<FormKeyValueView v-else> | ||||
| 				<template #key>{{ $ts.instanceInfo }}</template> | ||||
| 				<template #value>(Local user)</template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 
 | ||||
| 		<FormGroup> | ||||
| 			<FormKeyValueView> | ||||
| 				<template #key>{{ $ts.updatedAt }}</template> | ||||
| 				<template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 
 | ||||
| 		<FormObjectView tall :value="user"> | ||||
| 			<span>Raw</span> | ||||
| 		</FormObjectView> | ||||
| 	</FormGroup> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineAsyncComponent, defineComponent } from 'vue'; | ||||
| import { faExternalLinkAlt, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import FormObjectView from '@client/components/form/object-view.vue'; | ||||
| import FormTextarea from '@client/components/form/textarea.vue'; | ||||
| import FormLink from '@client/components/form/link.vue'; | ||||
| import FormBase from '@client/components/form/base.vue'; | ||||
| import FormGroup from '@client/components/form/group.vue'; | ||||
| import FormButton from '@client/components/form/button.vue'; | ||||
| import FormKeyValueView from '@client/components/form/key-value-view.vue'; | ||||
| import FormSuspense from '@client/components/form/suspense.vue'; | ||||
| import * as os from '@client/os'; | ||||
| import number from '@client/filters/number'; | ||||
| import bytes from '@client/filters/bytes'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| import { url } from '@client/config'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormBase, | ||||
| 		FormTextarea, | ||||
| 		FormObjectView, | ||||
| 		FormButton, | ||||
| 		FormLink, | ||||
| 		FormGroup, | ||||
| 		FormKeyValueView, | ||||
| 		FormSuspense, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		userId: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 				title: this.$ts.userInfo, | ||||
| 				icon: faInfoCircle, | ||||
| 				actions: this.user ? [this.user.url ? { | ||||
| 					text: this.user.url, | ||||
| 					icon: faExternalLinkAlt, | ||||
| 					handler: () => { | ||||
| 						window.open(this.user.url, '_blank'); | ||||
| 					} | ||||
| 				} : undefined].filter(x => x !== undefined) : [], | ||||
| 			})), | ||||
| 			user: null, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		number, | ||||
| 		bytes, | ||||
| 
 | ||||
| 		async fetch() { | ||||
| 			this.user = await os.api('users/show', { | ||||
| 				userId: this.userId | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -1,14 +1,16 @@ | |||
| <template> | ||||
| <MkContainer> | ||||
| <MkContainer :max-height="300" :foldable="true"> | ||||
| 	<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $ts.images }}</template> | ||||
| 	<div class="ujigsodd"> | ||||
| 		<MkLoading v-if="fetching"/> | ||||
| 		<div class="stream" v-if="!fetching && images.length > 0"> | ||||
| 			<MkA v-for="image in images" | ||||
| 				class="img" | ||||
| 				:style="`background-image: url(${thumbnail(image.file)})`" | ||||
| 				:to="notePage(image.note)" | ||||
| 			></MkA> | ||||
| 				:key="image.id" | ||||
| 			> | ||||
| 				<ImgWithBlurhash :hash="image.blurhash" :src="thumbnail(image.file)" :alt="image.name" :title="image.name"/> | ||||
| 			</MkA> | ||||
| 		</div> | ||||
| 		<p class="empty" v-if="!fetching && images.length == 0">{{ $ts.nothing }}</p> | ||||
| 	</div> | ||||
|  | @ -22,10 +24,12 @@ import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | |||
| import notePage from '../../filters/note'; | ||||
| import * as os from '@client/os'; | ||||
| import MkContainer from '@client/components/ui/container.vue'; | ||||
| import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		ImgWithBlurhash, | ||||
| 	}, | ||||
| 	props: { | ||||
| 		user: { | ||||
|  | @ -52,18 +56,16 @@ export default defineComponent({ | |||
| 			userId: this.user.id, | ||||
| 			fileType: image, | ||||
| 			excludeNsfw: this.$store.state.nsfw !== 'ignore', | ||||
| 			limit: 9, | ||||
| 			limit: 10, | ||||
| 		}).then(notes => { | ||||
| 			for (const note of notes) { | ||||
| 				for (const file of note.files) { | ||||
| 					if (this.images.length < 9) { | ||||
| 					this.images.push({ | ||||
| 						note, | ||||
| 						file | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 			} | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 	}, | ||||
|  | @ -83,20 +85,14 @@ export default defineComponent({ | |||
| 	padding: 8px; | ||||
| 
 | ||||
| 	> .stream { | ||||
| 		display: flex; | ||||
| 		justify-content: center; | ||||
| 		flex-wrap: wrap; | ||||
| 		display: grid; | ||||
| 		grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | ||||
| 		grid-gap: 6px; | ||||
| 
 | ||||
| 		> .img { | ||||
| 			flex: 1 1 33%; | ||||
| 			width: 33%; | ||||
| 			height: 90px; | ||||
| 			box-sizing: border-box; | ||||
| 			background-position: center center; | ||||
| 			background-size: cover; | ||||
| 			background-clip: content-box; | ||||
| 			border: solid 2px transparent; | ||||
| 			height: 128px; | ||||
| 			border-radius: 6px; | ||||
| 			overflow: clip; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 		<option value="replies">{{ $ts.notesAndReplies }}</option> | ||||
| 		<option value="files">{{ $ts.withFiles }}</option> | ||||
| 	</MkTab> | ||||
| 	<XNotes ref="timeline" class="_noGap_" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> | ||||
| 	<XNotes ref="timeline" :no-gap="true" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<div class="ftskorzw wide _section" v-if="user && narrow === false"> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<div class="ftskorzw wide" v-if="user && narrow === false"> | ||||
| 		<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_gap"/> | ||||
| 
 | ||||
| 		<div class="banner-container _gap" :style="style"> | ||||
|  | @ -8,7 +8,7 @@ | |||
| 		</div> | ||||
| 		<div class="contents"> | ||||
| 			<div class="side _forceContainerFull_"> | ||||
| 				<MkAvatar class="avatar" :user="user" :disable-preview="true"/> | ||||
| 				<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> | ||||
| 				<div class="name"> | ||||
| 					<MkUserName :user="user" :nowrap="false" class="name"/> | ||||
| 					<MkAcct :user="user" :detail="true" class="acct"/> | ||||
|  | @ -121,7 +121,7 @@ | |||
| 						<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<MkAvatar class="avatar" :user="user" :disable-preview="true"/> | ||||
| 				<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> | ||||
| 				<div class="title"> | ||||
| 					<MkUserName :user="user" :nowrap="false" class="name"/> | ||||
| 					<div class="bottom"> | ||||
|  | @ -212,10 +212,9 @@ | |||
| 			<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div v-else-if="error"> | ||||
| 		<MkError @retry="fetch()"/> | ||||
| 	</div> | ||||
| </div> | ||||
| 	<MkError v-else-if="error" @retry="fetch()"/> | ||||
| 	<MkLoading v-else/> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -279,6 +278,7 @@ export default defineComponent({ | |||
| 				share: { | ||||
| 					title: this.user.name, | ||||
| 				}, | ||||
| 				menu: () => getUserMenu(this.user), | ||||
| 			} : null), | ||||
| 			user: null, | ||||
| 			error: null, | ||||
|  | @ -323,6 +323,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 		fetch() { | ||||
| 			if (this.acct == null) return; | ||||
| 			this.user = null; | ||||
| 			Progress.start(); | ||||
| 			os.api('users/show', parseAcct(this.acct)).then(user => { | ||||
| 				this.user = user; | ||||
|  | @ -368,6 +369,15 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .ftskorzw.wide { | ||||
| 	max-width: 1150px; | ||||
| 	margin: 0 auto; | ||||
|  |  | |||
|  | @ -72,6 +72,9 @@ export const router = createRouter({ | |||
| 		{ path: '/instance/abuses', component: page('instance/abuses') }, | ||||
| 		{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, | ||||
| 		{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) }, | ||||
| 		{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) }, | ||||
| 		{ path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) }, | ||||
| 		{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) }, | ||||
| 		{ path: '/games/reversi', component: page('reversi/index') }, | ||||
| 		{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) }, | ||||
| 		{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') }, | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug, faExclamationCircle, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { i18n } from '@client/i18n'; | ||||
| import copyToClipboard from '@client/scripts/copy-to-clipboard'; | ||||
|  | @ -126,6 +126,12 @@ export function getUserMenu(user) { | |||
| 		action: () => { | ||||
| 			copyToClipboard(`@${user.username}@${user.host || host}`); | ||||
| 		} | ||||
| 	}, { | ||||
| 		icon: faInfoCircle, | ||||
| 		text: i18n.locale.info, | ||||
| 		action: () => { | ||||
| 			os.pageWindow(`/user-info/${user.id}`); | ||||
| 		} | ||||
| 	}, { | ||||
| 		icon: faEnvelope, | ||||
| 		text: i18n.locale.sendMessage, | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ html { | |||
| 	background-position: center; | ||||
| 	color: var(--fg); | ||||
| 	overflow: auto; | ||||
| 	overflow-wrap: break-word; | ||||
| 	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; | ||||
| 	line-height: 1.35; | ||||
| 	text-size-adjust: 100%; | ||||
|  | @ -88,10 +89,6 @@ html._themeChanging_ { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| body { | ||||
| 	overflow-wrap: break-word; | ||||
| } | ||||
| 
 | ||||
| html, body { | ||||
| 	margin: 0; | ||||
| 	padding: 0; | ||||
|  | @ -458,7 +455,7 @@ hr { | |||
| } | ||||
| 
 | ||||
| ._monospace { | ||||
| 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; | ||||
| 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; | ||||
| } | ||||
| 
 | ||||
| ._code { | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
| 		<div class="titleContainer"> | ||||
| 			<div class="title"> | ||||
| 				<Fa v-if="info.icon" :icon="info.icon" :key="info.icon" class="icon"/> | ||||
| 				<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true"/> | ||||
| 				<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/> | ||||
| 				<MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> | ||||
| 				<span v-else-if="info.title" class="text">{{ info.title }}</span> | ||||
| 			</div> | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ | |||
| 		<div>{{ $ts.noNotes }}</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 
 | ||||
| 	<MkError v-if="error" @retry="init()"/> | ||||
| 
 | ||||
| 	<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | ||||
|  |  | |||
|  | @ -310,6 +310,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 		> .widgets { | ||||
| 			//--panelShadow: none; | ||||
| 			width: 300px; | ||||
| 
 | ||||
| 			@media (max-width: $widgets-hide-threshold) { | ||||
| 				display: none; | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ | |||
| <div class="efzpzdvf"> | ||||
| 	<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> | ||||
| 
 | ||||
| 	<button v-if="editMode" @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button> | ||||
| 	<button v-else @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> | ||||
| 	<button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button> | ||||
| 	<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -62,18 +62,11 @@ export default defineComponent({ | |||
| 	position: sticky; | ||||
| 	height: min-content; | ||||
| 	box-sizing: border-box; | ||||
| 	padding-bottom: 8px; | ||||
| 
 | ||||
| 	> * { | ||||
| 		margin: var(--margin) 0; | ||||
| 		width: 300px; | ||||
| 
 | ||||
| 		&:first-child { | ||||
| 			margin-top: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .add { | ||||
| 		margin: 0 auto; | ||||
| 	> .edit { | ||||
| 		display: block; | ||||
| 		margin: 16px auto; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ | |||
| 			<div class="page active link" v-if="info"> | ||||
| 				<div class="title"> | ||||
| 					<Fa v-if="info.icon" :icon="info.icon" :key="info.icon" class="icon"/> | ||||
| 					<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true"/> | ||||
| 					<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/> | ||||
| 					<span v-if="info.title" class="text">{{ info.title }}</span> | ||||
| 					<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> | ||||
| 				</div> | ||||
|  | @ -28,7 +28,7 @@ | |||
| 		</button> | ||||
| 		<div class="title" v-if="info"> | ||||
| 			<Fa v-if="info.icon" :icon="info.icon" :key="info.icon" class="icon"/> | ||||
| 			<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true"/> | ||||
| 			<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/> | ||||
| 			<span v-if="info.title" class="text">{{ info.title }}</span> | ||||
| 			<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> | ||||
| 		</div> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkContainer :show-header="props.showHeader" :body-togglable="bodyTogglable" :scrollable="scrollable"> | ||||
| <MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable"> | ||||
| 	<template #header><Fa :icon="faGlobe"/>{{ $ts._widgets.federation }}</template> | ||||
| 
 | ||||
| 	<div class="wbrkwalb"> | ||||
|  | @ -42,7 +42,7 @@ export default defineComponent({ | |||
| 		MkContainer, MkMiniChart | ||||
| 	}, | ||||
| 	props: { | ||||
| 		bodyTogglable: { | ||||
| 		foldable: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
|  |  | |||
							
								
								
									
										2
									
								
								src/const.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/const.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
 | ||||
| export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
 | ||||
|  | @ -26,6 +26,17 @@ export class User { | |||
| 	}) | ||||
| 	public lastFetchedAt: Date | null; | ||||
| 
 | ||||
| 	@Index() | ||||
| 	@Column('timestamp with time zone', { | ||||
| 		nullable: true | ||||
| 	}) | ||||
| 	public lastActiveDate: Date | null; | ||||
| 
 | ||||
| 	@Column('boolean', { | ||||
| 		default: false, | ||||
| 	}) | ||||
| 	public hideOnlineStatus: boolean; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 128, | ||||
| 		comment: 'The username of the User.' | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import { SchemaType } from '@/misc/schema'; | |||
| import { awaitAll } from '../../prelude/await-all'; | ||||
| import { populateEmojis } from '@/misc/populate-emojis'; | ||||
| import { getAntennas } from '@/misc/antenna-cache'; | ||||
| import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const'; | ||||
| 
 | ||||
| export type PackedUser = SchemaType<typeof packedUserSchema>; | ||||
| 
 | ||||
|  | @ -145,6 +146,17 @@ export class UserRepository extends Repository<User> { | |||
| 		return count > 0; | ||||
| 	} | ||||
| 
 | ||||
| 	public getOnlineStatus(user: User): string { | ||||
| 		if (user.hideOnlineStatus == null) return 'unknown'; | ||||
| 		if (user.lastActiveDate == null) return 'unknown'; | ||||
| 		const elapsed = Date.now() - user.lastActiveDate.getTime(); | ||||
| 		return ( | ||||
| 			elapsed < USER_ONLINE_THRESHOLD ? 'online' : | ||||
| 			elapsed < USER_ACTIVE_THRESHOLD ? 'active' : | ||||
| 			'offline' | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	public async pack( | ||||
| 		src: User['id'] | User, | ||||
| 		me?: { id: User['id'] } | null | undefined, | ||||
|  | @ -192,11 +204,14 @@ export class UserRepository extends Repository<User> { | |||
| 				themeColor: instance.themeColor, | ||||
| 			} : undefined) : undefined, | ||||
| 			emojis: populateEmojis(user.emojis, user.host), | ||||
| 			onlineStatus: this.getOnlineStatus(user), | ||||
| 
 | ||||
| 			...(opts.detail ? { | ||||
| 				url: profile!.url, | ||||
| 				uri: user.uri, | ||||
| 				createdAt: user.createdAt.toISOString(), | ||||
| 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, | ||||
| 				lastFetchedAt: user.lastFetchedAt?.toISOString(), | ||||
| 				bannerUrl: user.bannerUrl, | ||||
| 				bannerBlurhash: user.bannerBlurhash, | ||||
| 				bannerColor: null, // 後方互換性のため
 | ||||
|  | @ -237,6 +252,7 @@ export class UserRepository extends Repository<User> { | |||
| 				autoAcceptFollowed: profile!.autoAcceptFollowed, | ||||
| 				noCrawle: profile!.noCrawle, | ||||
| 				isExplorable: user.isExplorable, | ||||
| 				hideOnlineStatus: user.hideOnlineStatus, | ||||
| 				hasUnreadSpecifiedNotes: NoteUnreads.count({ | ||||
| 					where: { userId: user.id, isSpecified: true }, | ||||
| 					take: 1 | ||||
|  |  | |||
							
								
								
									
										38
									
								
								src/server/api/endpoints/ap/get.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/server/api/endpoints/ap/get.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | |||
| import $ from 'cafy'; | ||||
| import define from '../../define'; | ||||
| import Resolver from '../../../../remote/activitypub/resolver'; | ||||
| import { ApiError } from '../../error'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['federation'], | ||||
| 
 | ||||
| 	desc: { | ||||
| 		'ja-JP': 'URIを指定してActivityPubオブジェクトを参照します。', | ||||
| 		'en-US': 'Browse to the ActivityPub object by specifying the URI.' | ||||
| 	}, | ||||
| 
 | ||||
| 	requireCredential: false as const, | ||||
| 
 | ||||
| 	params: { | ||||
| 		uri: { | ||||
| 			validator: $.str, | ||||
| 			desc: { | ||||
| 				'ja-JP': 'ActivityPubオブジェクトのURI' | ||||
| 			} | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	errors: { | ||||
| 	}, | ||||
| 
 | ||||
| 	res: { | ||||
| 		type: 'object' as const, | ||||
| 		optional: false as const, nullable: false as const, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| 	const resolver = new Resolver(); | ||||
| 	const object = await resolver.resolve(ps.uri); | ||||
| 	return object; | ||||
| }); | ||||
							
								
								
									
										43
									
								
								src/server/api/endpoints/federation/dns.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/server/api/endpoints/federation/dns.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import { promises as dns } from 'dns'; | ||||
| import $ from 'cafy'; | ||||
| import define from '../../define'; | ||||
| import { Instances } from '../../../../models'; | ||||
| import { toPuny } from '@/misc/convert-host'; | ||||
| 
 | ||||
| const resolver = new dns.Resolver(); | ||||
| resolver.setServers(['1.1.1.1']); | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['federation'], | ||||
| 
 | ||||
| 	requireCredential: false as const, | ||||
| 
 | ||||
| 	params: { | ||||
| 		host: { | ||||
| 			validator: $.str | ||||
| 		} | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const instance = await Instances.findOneOrFail({ host: toPuny(ps.host) }); | ||||
| 
 | ||||
| 	const [ | ||||
| 		resolved4, | ||||
| 		resolved6, | ||||
| 		resolvedCname, | ||||
| 		resolvedTxt, | ||||
| 	] = await Promise.all([ | ||||
| 		resolver.resolve4(instance.host).catch(() => []), | ||||
| 		resolver.resolve6(instance.host).catch(() => []), | ||||
| 		resolver.resolveCname(instance.host).catch(() => []), | ||||
| 		resolver.resolveTxt(instance.host).catch(() => []), | ||||
| 	]); | ||||
| 
 | ||||
| 	return { | ||||
| 		a: resolved4, | ||||
| 		aaaa: resolved6, | ||||
| 		cname: resolvedCname, | ||||
| 		txt: resolvedTxt, | ||||
| 	}; | ||||
| }); | ||||
|  | @ -1,6 +1,7 @@ | |||
| import { USER_ONLINE_THRESHOLD } from '@/const'; | ||||
| import { Users } from '@/models'; | ||||
| import { MoreThan } from 'typeorm'; | ||||
| import define from '../define'; | ||||
| import { redisClient } from '../../../db/redis'; | ||||
| import config from '@/config'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['meta'], | ||||
|  | @ -11,12 +12,12 @@ export const meta = { | |||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, (ps, user) => { | ||||
| 	return new Promise((res, rej) => { | ||||
| 		redisClient.pubsub('numsub', config.host, (_, x) => { | ||||
| 			res({ | ||||
| 				count: x[1] | ||||
| 			}); | ||||
| 		}); | ||||
| export default define(meta, async () => { | ||||
| 	const count = await Users.count({ | ||||
| 		lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD)) | ||||
| 	}); | ||||
| 
 | ||||
| 	return { | ||||
| 		count | ||||
| 	}; | ||||
| }); | ||||
|  |  | |||
|  | @ -96,6 +96,10 @@ export const meta = { | |||
| 			validator: $.optional.bool, | ||||
| 		}, | ||||
| 
 | ||||
| 		hideOnlineStatus: { | ||||
| 			validator: $.optional.bool, | ||||
| 		}, | ||||
| 
 | ||||
| 		carefulBot: { | ||||
| 			validator: $.optional.bool, | ||||
| 			desc: { | ||||
|  | @ -228,6 +232,7 @@ export default define(meta, async (ps, _user, token) => { | |||
| 	if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; | ||||
| 	if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; | ||||
| 	if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; | ||||
| 	if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; | ||||
| 	if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; | ||||
| 	if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; | ||||
| 	if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import { ParsedUrlQuery } from 'querystring'; | |||
| import authenticate from './authenticate'; | ||||
| import { EventEmitter } from 'events'; | ||||
| import { subsdcriber as redisClient } from '../../db/redis'; | ||||
| import { Users } from '@/models'; | ||||
| 
 | ||||
| module.exports = (server: http.Server) => { | ||||
| 	// Init websocket server
 | ||||
|  | @ -45,5 +46,11 @@ module.exports = (server: http.Server) => { | |||
| 				connection.send('pong'); | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		if (user) { | ||||
| 			Users.update(user.id, { | ||||
| 				lastActiveDate: new Date(), | ||||
| 			}); | ||||
| 		} | ||||
| 	}); | ||||
| }; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue