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-dev \ | ||||||
|     vips |     vips | ||||||
| 
 | 
 | ||||||
| COPY package.json yarn.lock ./ | COPY package.json yarn.lock .yarnrc ./ | ||||||
| RUN yarn install | RUN yarn install | ||||||
| COPY . ./ | COPY . ./ | ||||||
| RUN yarn build | RUN yarn build | ||||||
|  |  | ||||||
|  | @ -719,6 +719,15 @@ quitFullView: "フルビュー解除" | ||||||
| addDescription: "説明を追加" | addDescription: "説明を追加" | ||||||
| userPagePinTip: "個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。" | userPagePinTip: "個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。" | ||||||
| notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります" | notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります" | ||||||
|  | info: "情報" | ||||||
|  | userInfo: "ユーザー情報" | ||||||
|  | unknown: "不明" | ||||||
|  | onlineStatus: "オンライン状態" | ||||||
|  | hideOnlineStatus: "オンライン状態を隠す" | ||||||
|  | hideOnlineStatusDescription: "オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。" | ||||||
|  | online: "オンライン" | ||||||
|  | active: "アクティブ" | ||||||
|  | offline: "オフライン" | ||||||
| 
 | 
 | ||||||
| _email: | _email: | ||||||
|   _follow: |   _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", | 	"name": "misskey", | ||||||
| 	"author": "syuilo <syuilotan@yahoo.co.jp>", | 	"author": "syuilo <syuilotan@yahoo.co.jp>", | ||||||
| 	"version": "12.76.1", | 	"version": "12.77.0", | ||||||
| 	"codename": "indigo", | 	"codename": "indigo", | ||||||
| 	"repository": { | 	"repository": { | ||||||
| 		"type": "git", | 		"type": "git", | ||||||
|  | @ -37,7 +37,7 @@ | ||||||
| 		"lodash": "^4.17.20" | 		"lodash": "^4.17.20" | ||||||
| 	}, | 	}, | ||||||
| 	"dependencies": { | 	"dependencies": { | ||||||
| 		"@babel/plugin-transform-runtime": "7.13.10", | 		"@babel/plugin-transform-runtime": "7.13.15", | ||||||
| 		"@elastic/elasticsearch": "7.11.0", | 		"@elastic/elasticsearch": "7.11.0", | ||||||
| 		"@fortawesome/fontawesome-svg-core": "1.2.35", | 		"@fortawesome/fontawesome-svg-core": "1.2.35", | ||||||
| 		"@fortawesome/free-brands-svg-icons": "5.15.3", | 		"@fortawesome/free-brands-svg-icons": "5.15.3", | ||||||
|  | @ -49,7 +49,7 @@ | ||||||
| 		"@koa/router": "9.0.1", | 		"@koa/router": "9.0.1", | ||||||
| 		"@sentry/browser": "5.29.2", | 		"@sentry/browser": "5.29.2", | ||||||
| 		"@sentry/tracing": "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", | 		"@syuilo/aiscript": "0.11.1", | ||||||
| 		"@types/bcryptjs": "2.4.2", | 		"@types/bcryptjs": "2.4.2", | ||||||
| 		"@types/bull": "3.15.0", | 		"@types/bull": "3.15.0", | ||||||
|  | @ -62,7 +62,7 @@ | ||||||
| 		"@types/gulp-replace": "0.0.31", | 		"@types/gulp-replace": "0.0.31", | ||||||
| 		"@types/is-url": "1.2.28", | 		"@types/is-url": "1.2.28", | ||||||
| 		"@types/js-yaml": "4.0.0", | 		"@types/js-yaml": "4.0.0", | ||||||
| 		"@types/jsdom": "16.2.7", | 		"@types/jsdom": "16.2.10", | ||||||
| 		"@types/jsonld": "1.5.5", | 		"@types/jsonld": "1.5.5", | ||||||
| 		"@types/katex": "0.11.0", | 		"@types/katex": "0.11.0", | ||||||
| 		"@types/koa": "2.13.1", | 		"@types/koa": "2.13.1", | ||||||
|  | @ -77,10 +77,10 @@ | ||||||
| 		"@types/koa__multer": "2.0.2", | 		"@types/koa__multer": "2.0.2", | ||||||
| 		"@types/koa__router": "8.0.4", | 		"@types/koa__router": "8.0.4", | ||||||
| 		"@types/markdown-it": "12.0.1", | 		"@types/markdown-it": "12.0.1", | ||||||
| 		"@types/matter-js": "0.14.10", | 		"@types/matter-js": "0.14.11", | ||||||
| 		"@types/mocha": "8.2.1", | 		"@types/mocha": "8.2.2", | ||||||
| 		"@types/node": "14.14.35", | 		"@types/node": "14.14.41", | ||||||
| 		"@types/node-fetch": "2.5.8", | 		"@types/node-fetch": "2.5.10", | ||||||
| 		"@types/nodemailer": "6.4.1", | 		"@types/nodemailer": "6.4.1", | ||||||
| 		"@types/nprogress": "0.2.0", | 		"@types/nprogress": "0.2.0", | ||||||
| 		"@types/oauth": "0.9.1", | 		"@types/oauth": "0.9.1", | ||||||
|  | @ -97,7 +97,7 @@ | ||||||
| 		"@types/request-stats": "3.0.0", | 		"@types/request-stats": "3.0.0", | ||||||
| 		"@types/rimraf": "3.0.0", | 		"@types/rimraf": "3.0.0", | ||||||
| 		"@types/seedrandom": "2.4.28", | 		"@types/seedrandom": "2.4.28", | ||||||
| 		"@types/sharp": "0.27.1", | 		"@types/sharp": "0.28.0", | ||||||
| 		"@types/sinonjs__fake-timers": "6.0.2", | 		"@types/sinonjs__fake-timers": "6.0.2", | ||||||
| 		"@types/speakeasy": "2.0.5", | 		"@types/speakeasy": "2.0.5", | ||||||
| 		"@types/throttle-debounce": "2.1.0", | 		"@types/throttle-debounce": "2.1.0", | ||||||
|  | @ -105,39 +105,39 @@ | ||||||
| 		"@types/tmp": "0.2.0", | 		"@types/tmp": "0.2.0", | ||||||
| 		"@types/uuid": "8.3.0", | 		"@types/uuid": "8.3.0", | ||||||
| 		"@types/web-push": "3.3.0", | 		"@types/web-push": "3.3.0", | ||||||
| 		"@types/webpack": "4.41.26", | 		"@types/webpack": "5.28.0", | ||||||
| 		"@types/webpack-stream": "3.2.11", | 		"@types/webpack-stream": "3.2.12", | ||||||
| 		"@types/websocket": "1.0.2", | 		"@types/websocket": "1.0.2", | ||||||
| 		"@types/ws": "7.4.0", | 		"@types/ws": "7.4.1", | ||||||
| 		"@typescript-eslint/parser": "4.18.0", | 		"@typescript-eslint/parser": "4.22.0", | ||||||
| 		"@vue/compiler-sfc": "3.0.8", | 		"@vue/compiler-sfc": "3.0.11", | ||||||
| 		"abort-controller": "3.0.0", | 		"abort-controller": "3.0.0", | ||||||
| 		"apexcharts": "3.26.0", | 		"apexcharts": "3.26.0", | ||||||
| 		"autobind-decorator": "2.4.0", | 		"autobind-decorator": "2.4.0", | ||||||
| 		"autosize": "4.0.2", | 		"autosize": "4.0.2", | ||||||
| 		"autwh": "0.1.0", | 		"autwh": "0.1.0", | ||||||
| 		"aws-sdk": "2.867.0", | 		"aws-sdk": "2.887.0", | ||||||
| 		"bcryptjs": "2.4.3", | 		"bcryptjs": "2.4.3", | ||||||
| 		"blurhash": "1.1.3", | 		"blurhash": "1.1.3", | ||||||
| 		"broadcast-channel": "3.5.3", | 		"broadcast-channel": "3.5.3", | ||||||
| 		"bull": "3.21.1", | 		"bull": "3.22.0", | ||||||
| 		"cafy": "15.2.1", | 		"cafy": "15.2.1", | ||||||
| 		"cbor": "7.0.4", | 		"cbor": "7.0.5", | ||||||
| 		"chalk": "4.1.0", | 		"chalk": "4.1.0", | ||||||
| 		"chart.js": "2.9.4", | 		"chart.js": "2.9.4", | ||||||
| 		"cli-highlight": "2.1.10", | 		"cli-highlight": "2.1.11", | ||||||
| 		"commander": "4.1.1", | 		"commander": "7.2.0", | ||||||
| 		"concurrently": "6.0.0", | 		"concurrently": "6.0.2", | ||||||
| 		"content-disposition": "0.5.3", | 		"content-disposition": "0.5.3", | ||||||
| 		"core-js": "3.9.1", | 		"core-js": "3.10.1", | ||||||
| 		"crc-32": "1.2.0", | 		"crc-32": "1.2.0", | ||||||
| 		"css-loader": "5.1.3", | 		"css-loader": "5.2.1", | ||||||
| 		"cssnano": "4.1.10", | 		"cssnano": "5.0.1", | ||||||
| 		"dateformat": "4.5.1", | 		"dateformat": "4.5.1", | ||||||
| 		"diskusage": "1.1.3", | 		"diskusage": "1.1.3", | ||||||
| 		"escape-regexp": "0.0.1", | 		"escape-regexp": "0.0.1", | ||||||
| 		"eslint": "7.22.0", | 		"eslint": "7.24.0", | ||||||
| 		"eslint-plugin-vue": "7.7.0", | 		"eslint-plugin-vue": "7.9.0", | ||||||
| 		"eventemitter3": "4.0.7", | 		"eventemitter3": "4.0.7", | ||||||
| 		"feed": "4.2.2", | 		"feed": "4.2.2", | ||||||
| 		"fibers": "5.0.0", | 		"fibers": "5.0.0", | ||||||
|  | @ -156,17 +156,17 @@ | ||||||
| 		"http-proxy-agent": "4.0.1", | 		"http-proxy-agent": "4.0.1", | ||||||
| 		"http-signature": "1.3.5", | 		"http-signature": "1.3.5", | ||||||
| 		"https-proxy-agent": "5.0.0", | 		"https-proxy-agent": "5.0.0", | ||||||
| 		"idb-keyval": "5.0.4", | 		"idb-keyval": "5.0.5", | ||||||
| 		"insert-text-at-cursor": "0.3.0", | 		"insert-text-at-cursor": "0.3.0", | ||||||
| 		"is-root": "2.1.0", | 		"is-root": "2.1.0", | ||||||
| 		"is-svg": "4.3.1", | 		"is-svg": "4.3.1", | ||||||
| 		"js-yaml": "4.0.0", | 		"js-yaml": "4.1.0", | ||||||
| 		"jsdom": "16.5.1", | 		"jsdom": "16.5.3", | ||||||
| 		"json5": "2.2.0", | 		"json5": "2.2.0", | ||||||
| 		"json5-loader": "4.0.1", | 		"json5-loader": "4.0.1", | ||||||
| 		"jsonld": "4.0.1", | 		"jsonld": "4.0.1", | ||||||
| 		"jsrsasign": "8.0.20", | 		"jsrsasign": "8.0.20", | ||||||
| 		"katex": "0.13.0", | 		"katex": "0.13.2", | ||||||
| 		"koa": "2.13.1", | 		"koa": "2.13.1", | ||||||
| 		"koa-bodyparser": "4.3.0", | 		"koa-bodyparser": "4.3.0", | ||||||
| 		"koa-favicon": "2.1.0", | 		"koa-favicon": "2.1.0", | ||||||
|  | @ -178,9 +178,9 @@ | ||||||
| 		"koa-views": "7.0.1", | 		"koa-views": "7.0.1", | ||||||
| 		"langmap": "0.0.16", | 		"langmap": "0.0.16", | ||||||
| 		"lookup-dns-cache": "2.1.0", | 		"lookup-dns-cache": "2.1.0", | ||||||
| 		"markdown-it": "12.0.4", | 		"markdown-it": "12.0.5", | ||||||
| 		"markdown-it-anchor": "7.1.0", | 		"markdown-it-anchor": "7.1.0", | ||||||
| 		"matter-js": "0.16.1", | 		"matter-js": "0.17.1", | ||||||
| 		"mfm-js": "0.15.0", | 		"mfm-js": "0.15.0", | ||||||
| 		"mocha": "8.3.2", | 		"mocha": "8.3.2", | ||||||
| 		"moji": "0.5.1", | 		"moji": "0.5.1", | ||||||
|  | @ -192,23 +192,23 @@ | ||||||
| 		"object-assign-deep": "0.4.0", | 		"object-assign-deep": "0.4.0", | ||||||
| 		"os-utils": "0.0.14", | 		"os-utils": "0.0.14", | ||||||
| 		"parse5": "6.0.1", | 		"parse5": "6.0.1", | ||||||
| 		"pg": "8.5.1", | 		"pg": "8.6.0", | ||||||
| 		"portscanner": "2.2.0", | 		"portscanner": "2.2.0", | ||||||
| 		"postcss": "8.2.8", | 		"postcss": "8.2.10", | ||||||
| 		"postcss-loader": "5.2.0", | 		"postcss-loader": "5.2.0", | ||||||
| 		"prismjs": "1.23.0", | 		"prismjs": "1.23.0", | ||||||
| 		"probe-image-size": "7.0.1", | 		"probe-image-size": "7.1.0", | ||||||
| 		"promise-limit": "2.7.0", | 		"promise-limit": "2.7.0", | ||||||
| 		"promise-sequential": "1.1.1", | 		"promise-sequential": "1.1.1", | ||||||
| 		"pug": "3.0.2", | 		"pug": "3.0.2", | ||||||
| 		"punycode": "2.1.1", | 		"punycode": "2.1.1", | ||||||
| 		"pureimage": "0.2.7", | 		"pureimage": "0.3.2", | ||||||
| 		"qrcode": "1.4.4", | 		"qrcode": "1.4.4", | ||||||
| 		"random-seed": "0.3.0", | 		"random-seed": "0.3.0", | ||||||
| 		"ratelimiter": "3.4.1", | 		"ratelimiter": "3.4.1", | ||||||
| 		"re2": "1.15.9", | 		"re2": "1.15.9", | ||||||
| 		"reconnecting-websocket": "4.4.0", | 		"reconnecting-websocket": "4.4.0", | ||||||
| 		"redis": "3.0.2", | 		"redis": "3.1.1", | ||||||
| 		"redis-lock": "0.1.4", | 		"redis-lock": "0.1.4", | ||||||
| 		"reflect-metadata": "0.1.13", | 		"reflect-metadata": "0.1.13", | ||||||
| 		"regenerator-runtime": "0.13.7", | 		"regenerator-runtime": "0.13.7", | ||||||
|  | @ -221,32 +221,32 @@ | ||||||
| 		"sass": "1.32.8", | 		"sass": "1.32.8", | ||||||
| 		"sass-loader": "11.0.1", | 		"sass-loader": "11.0.1", | ||||||
| 		"seedrandom": "3.0.5", | 		"seedrandom": "3.0.5", | ||||||
| 		"sharp": "0.27.2", | 		"sharp": "0.28.1", | ||||||
| 		"speakeasy": "2.0.0", | 		"speakeasy": "2.0.0", | ||||||
| 		"stringz": "2.1.0", | 		"stringz": "2.1.0", | ||||||
| 		"style-loader": "2.0.0", | 		"style-loader": "2.0.0", | ||||||
| 		"summaly": "2.4.0", | 		"summaly": "2.4.0", | ||||||
| 		"syslog-pro": "1.0.0", | 		"syslog-pro": "1.0.0", | ||||||
| 		"systeminformation": "5.6.7", | 		"systeminformation": "5.6.12", | ||||||
| 		"syuilo-password-strength": "0.0.1", | 		"syuilo-password-strength": "0.0.1", | ||||||
| 		"textarea-caret": "3.1.0", | 		"textarea-caret": "3.1.0", | ||||||
| 		"three": "0.117.1", | 		"three": "0.117.1", | ||||||
| 		"throttle-debounce": "3.0.1", | 		"throttle-debounce": "3.0.1", | ||||||
| 		"tinycolor2": "1.4.2", | 		"tinycolor2": "1.4.2", | ||||||
| 		"tmp": "0.2.1", | 		"tmp": "0.2.1", | ||||||
| 		"ts-loader": "8.0.18", | 		"ts-loader": "8.1.0", | ||||||
| 		"ts-node": "9.1.1", | 		"ts-node": "9.1.1", | ||||||
| 		"tsc-alias": "1.2.8", | 		"tsc-alias": "1.2.9", | ||||||
| 		"tsconfig-paths": "3.9.0", | 		"tsconfig-paths": "3.9.0", | ||||||
| 		"tslint": "6.1.3", | 		"tslint": "6.1.3", | ||||||
| 		"tslint-sonarts": "1.9.0", | 		"tslint-sonarts": "1.9.0", | ||||||
| 		"typeorm": "0.2.31", | 		"typeorm": "0.2.32", | ||||||
| 		"typescript": "4.2.3", | 		"typescript": "4.2.4", | ||||||
| 		"ulid": "2.3.0", | 		"ulid": "2.3.0", | ||||||
| 		"uuid": "8.3.2", | 		"uuid": "8.3.2", | ||||||
| 		"v-debounce": "0.1.2", | 		"v-debounce": "0.1.2", | ||||||
| 		"vanilla-tilt": "1.7.0", | 		"vanilla-tilt": "1.7.0", | ||||||
| 		"vue": "3.0.8", | 		"vue": "3.0.11", | ||||||
| 		"vue-color": "2.8.1", | 		"vue-color": "2.8.1", | ||||||
| 		"vue-json-pretty": "1.7.1", | 		"vue-json-pretty": "1.7.1", | ||||||
| 		"vue-loader": "16.1.2", | 		"vue-loader": "16.1.2", | ||||||
|  | @ -256,9 +256,9 @@ | ||||||
| 		"vue-svg-loader": "0.17.0-beta.2", | 		"vue-svg-loader": "0.17.0-beta.2", | ||||||
| 		"vuedraggable": "4.0.1", | 		"vuedraggable": "4.0.1", | ||||||
| 		"web-push": "3.4.4", | 		"web-push": "3.4.4", | ||||||
| 		"webpack": "5.27.2", | 		"webpack": "5.33.2", | ||||||
| 		"webpack-cli": "4.5.0", | 		"webpack-cli": "4.6.0", | ||||||
| 		"websocket": "1.0.33", | 		"websocket": "1.0.34", | ||||||
| 		"ws": "7.4.4", | 		"ws": "7.4.4", | ||||||
| 		"xev": "2.0.1" | 		"xev": "2.0.1" | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| import * as program from 'commander'; | import { Command } from 'commander'; | ||||||
| import config from '@/config'; | import config from '@/config'; | ||||||
| 
 | 
 | ||||||
|  | const program = new Command(); | ||||||
|  | 
 | ||||||
| program | program | ||||||
| 	.version(config.version) | 	.version(config.version) | ||||||
| 	.option('--no-daemons', 'Disable daemon processes (for debbuging)') | 	.option('--no-daemons', 'Disable daemon processes (for debbuging)') | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;"> | 	<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> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -18,7 +18,12 @@ export default defineComponent({ | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
| 		} | 		}, | ||||||
|  | 		noGap: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
|  | @ -37,18 +42,16 @@ export default defineComponent({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	render() { | 	render() { | ||||||
| 		const noGap = [...document.querySelectorAll('._noGap_')].some(el => el.contains(this.$parent.$el)); |  | ||||||
| 
 |  | ||||||
| 		if (this.items.length === 0) return; | 		if (this.items.length === 0) return; | ||||||
| 
 | 
 | ||||||
| 		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? { | 		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? { | ||||||
| 			class: 'sqadhkmv' + (noGap ? ' _block' : ''), | 			class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), | ||||||
| 			name: 'list', | 			name: 'list', | ||||||
| 			tag: 'div', | 			tag: 'div', | ||||||
| 			'data-direction': this.direction, | 			'data-direction': this.direction, | ||||||
| 			'data-reversed': this.reversed ? 'true' : 'false', | 			'data-reversed': this.reversed ? 'true' : 'false', | ||||||
| 		} : { | 		} : { | ||||||
| 			class: 'sqadhkmv', | 			class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), | ||||||
| 		}, this.items.map((item, i) => { | 		}, this.items.map((item, i) => { | ||||||
| 			const el = this.$slots.default({ | 			const el = this.$slots.default({ | ||||||
| 				item: item | 				item: item | ||||||
|  | @ -154,17 +157,17 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| ._noGap_ .sqadhkmv { | 	&.noGap { | ||||||
| 	> * { | 		> * { | ||||||
| 		margin: 0 !important; | 			margin: 0 !important; | ||||||
| 		border: none; | 			border: none; | ||||||
| 		border-radius: 0; | 			border-radius: 0; | ||||||
| 		box-shadow: none; | 			box-shadow: none; | ||||||
| 
 | 
 | ||||||
| 		&:not(:last-child) { | 			&:not(:last-child) { | ||||||
| 			border-bottom: solid 0.5px var(--divider); | 				border-bottom: solid 0.5px var(--divider); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ import { | ||||||
| 	faFileArchive, | 	faFileArchive, | ||||||
| 	faFilm | 	faFilm | ||||||
| 	} from '@fortawesome/free-solid-svg-icons'; | 	} 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'; | import { ColdDeviceStorage } from '@client/store'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  |  | ||||||
|  | @ -24,9 +24,12 @@ export default defineComponent({ | ||||||
| 	--formXPadding: 32px; | 	--formXPadding: 32px; | ||||||
| 	--formYPadding: 32px; | 	--formYPadding: 32px; | ||||||
| 
 | 
 | ||||||
|  | 	font-size: 95%; | ||||||
| 	line-height: 1.3em; | 	line-height: 1.3em; | ||||||
| 	background: var(--bg); | 	background: var(--bg); | ||||||
| 	padding: var(--formYPadding) var(--formXPadding); | 	padding: var(--formYPadding) var(--formXPadding); | ||||||
|  | 	max-width: 750px; | ||||||
|  | 	margin: 0 auto; | ||||||
| 
 | 
 | ||||||
| 	&:not(.wide).max-width_400px { | 	&:not(.wide).max-width_400px { | ||||||
| 		--formXPadding: 0px; | 		--formXPadding: 0px; | ||||||
|  | @ -40,16 +43,16 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			._form_group { | 			._form_group { | ||||||
| 				> * { | 				> *:not(._formNoConcat) { | ||||||
| 					&:not(:first-child) { | 					&:not(:last-child):not(._formNoConcatPrev) { | ||||||
| 						&._formPanel, ._formPanel { | 						&._formPanel, ._formPanel { | ||||||
| 							border-top: none; | 							border-bottom: solid 0.5px var(--divider); | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					&:not(:last-child) { | 					&:not(:first-child):not(._formNoConcatNext) { | ||||||
| 						&._formPanel, ._formPanel { | 						&._formPanel, ._formPanel { | ||||||
| 							border-bottom: solid 0.5px var(--divider); | 							border-top: none; | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <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="_formLabel"><slot name="label"></slot></div> | ||||||
| 	<div class="main _form_group"> | 	<div class="main _form_group" ref="child"> | ||||||
| 		<slot></slot> | 		<slot></slot> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_formCaption"><slot name="caption"></slot></div> | 	<div class="_formCaption"><slot name="caption"></slot></div> | ||||||
|  | @ -9,33 +9,69 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent, onMounted, ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | 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> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .vrtktovg { | .vrtktovg { | ||||||
| 	> .main { | 	> .main { | ||||||
| 		> ::v-deep(*) { | 		> ::v-deep(*):not(._formNoConcat) { | ||||||
| 			margin: 0; | 			&: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 { | 				&._formPanel, ._formPanel { | ||||||
| 					border-bottom: solid 0.5px var(--divider); | 					border-bottom: solid 0.5px var(--divider); | ||||||
| 					border-bottom-left-radius: 0; | 					border-bottom-left-radius: 0; | ||||||
| 					border-bottom-right-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; | 	align-items: center; | ||||||
| 	padding: 14px 16px; | 	padding: 14px 16px; | ||||||
| 
 | 
 | ||||||
|  | 	> .key { | ||||||
|  | 		margin-right: 12px; | ||||||
|  | 		white-space: nowrap; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	> .value { | 	> .value { | ||||||
| 		margin-left: auto; | 		margin-left: auto; | ||||||
| 		opacity: 0.7; | 		opacity: 0.7; | ||||||
|  | 		text-overflow: ellipsis; | ||||||
|  | 		white-space: nowrap; | ||||||
|  | 		overflow: hidden; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </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> | <template> | ||||||
| <span class="eiwwqkts _noSelect" :class="{ cat }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick"> | <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"/> | 	<img class="inner" :src="url" decoding="async"/> | ||||||
|  | 	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/> | ||||||
| </span> | </span> | ||||||
| <MkA class="eiwwqkts _noSelect" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id"> | <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"/> | 	<img class="inner" :src="url" decoding="async"/> | ||||||
|  | 	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/> | ||||||
| </MkA> | </MkA> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -12,8 +14,12 @@ import { defineComponent } from 'vue'; | ||||||
| import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | ||||||
| import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; | import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; | ||||||
| import { acct, userPage } from '@client/filters/user'; | import { acct, userPage } from '@client/filters/user'; | ||||||
|  | import MkUserOnlineIndicator from '@client/components/user-online-indicator.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		MkUserOnlineIndicator | ||||||
|  | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		user: { | 		user: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -30,6 +36,10 @@ export default defineComponent({ | ||||||
| 		disablePreview: { | 		disablePreview: { | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
|  | 		}, | ||||||
|  | 		showIndicator: { | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 	emits: ['click'], | 	emits: ['click'], | ||||||
|  | @ -93,7 +103,7 @@ export default defineComponent({ | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	.inner { | 	> .inner { | ||||||
| 		position: absolute; | 		position: absolute; | ||||||
| 		bottom: 0; | 		bottom: 0; | ||||||
| 		left: 0; | 		left: 0; | ||||||
|  | @ -106,5 +116,14 @@ export default defineComponent({ | ||||||
| 		width: 100%; | 		width: 100%; | ||||||
| 		height: 100%; | 		height: 100%; | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	> .indicator { | ||||||
|  | 		position: absolute; | ||||||
|  | 		z-index: 1; | ||||||
|  | 		bottom: 0; | ||||||
|  | 		left: 0; | ||||||
|  | 		width: 20%; | ||||||
|  | 		height: 20%; | ||||||
|  | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,12 +1,11 @@ | ||||||
| <template> | <template> | ||||||
| <div class="yxspomdl" :class="{ inline }"> | <div class="yxspomdl" :class="{ inline, colored }"> | ||||||
| 	<div class="ring"></div> | 	<div class="ring"></div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import * as os from '@client/os'; |  | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -14,6 +13,11 @@ export default defineComponent({ | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
|  | 		}, | ||||||
|  | 		colored: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | @ -32,6 +36,11 @@ export default defineComponent({ | ||||||
| .yxspomdl { | .yxspomdl { | ||||||
| 	padding: 32px; | 	padding: 32px; | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
|  | 	cursor: wait; | ||||||
|  | 
 | ||||||
|  | 	&.colored { | ||||||
|  | 		color: var(--accent); | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	&.inline { | 	&.inline { | ||||||
| 		display: inline; | 		display: inline; | ||||||
|  | @ -41,24 +50,43 @@ export default defineComponent({ | ||||||
| 			width: 32px; | 			width: 32px; | ||||||
| 			height: 32px; | 			height: 32px; | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		> .ring { | ||||||
|  | 			&:before, | ||||||
|  | 			&:after { | ||||||
|  | 				width: 32px; | ||||||
|  | 				height: 32px; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> .ring { | 	> .ring { | ||||||
|  | 		position: relative; | ||||||
| 		display: inline-block; | 		display: inline-block; | ||||||
| 		opacity: 0.7; |  | ||||||
| 		vertical-align: middle; | 		vertical-align: middle; | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	> .ring:after { | 		&:before, | ||||||
| 		content: " "; | 		&:after { | ||||||
| 		display: block; | 			content: " "; | ||||||
| 		box-sizing: border-box; | 			display: block; | ||||||
| 		width: 48px; | 			box-sizing: border-box; | ||||||
| 		height: 48px; | 			width: 48px; | ||||||
| 		border-radius: 50%; | 			height: 48px; | ||||||
| 		border: solid 4px; | 			border-radius: 50%; | ||||||
| 		border-color: currentColor transparent transparent transparent; | 			border: solid 4px; | ||||||
| 		animation: ring 0.5s linear infinite; | 		} | ||||||
|  | 
 | ||||||
|  | 		&: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> | </style> | ||||||
|  |  | ||||||
|  | @ -71,6 +71,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .xubzgfgb { | .xubzgfgb { | ||||||
|  | 	position: relative; | ||||||
| 	width: 100%; | 	width: 100%; | ||||||
| 	height: 100%; | 	height: 100%; | ||||||
| 
 | 
 | ||||||
|  | @ -82,6 +83,7 @@ export default defineComponent({ | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> canvas { | 	> canvas { | ||||||
|  | 		position: absolute; | ||||||
| 		object-fit: cover; | 		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 { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | ||||||
| import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; | import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; | ||||||
| import ImageViewer from './image-viewer.vue'; | 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'; | import * as os from '@client/os'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  |  | ||||||
|  | @ -35,7 +35,7 @@ | ||||||
| 	</div> | 	</div> | ||||||
| 	<article class="article" @contextmenu.stop="onContextmenu"> | 	<article class="article" @contextmenu.stop="onContextmenu"> | ||||||
| 		<header class="header"> | 		<header class="header"> | ||||||
| 			<MkAvatar class="avatar" :user="appearNote.user"/> | 			<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/> | ||||||
| 			<div class="body"> | 			<div class="body"> | ||||||
| 				<div class="top"> | 				<div class="top"> | ||||||
| 					<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.user.id"> | 					<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.user.id"> | ||||||
|  |  | ||||||
|  | @ -1,30 +1,34 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <transition name="fade" mode="out-in"> | ||||||
| 	<div class="_fullinfo" v-if="empty"> | 	<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"/> | 		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||||
| 		<div>{{ $ts.noNotes }}</div> | 		<div>{{ $ts.noNotes }}</div> | ||||||
| 	</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> | ||||||
|  | 				<template v-if="moreFetching"><MkLoading inline/></template> | ||||||
|  | 			</MkButton> | ||||||
|  | 		</div> | ||||||
| 
 | 
 | ||||||
| 	<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | 		<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap"> | ||||||
| 		<MkButton style="margin: 0 auto;" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | 			<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> | ||||||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | 		</XList> | ||||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> | 
 | ||||||
| 		</MkButton> | 		<div v-show="more && !reversed" style="margin-top: var(--margin);"> | ||||||
|  | 			<MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||||
|  | 				<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||||
|  | 				<template v-if="moreFetching"><MkLoading inline/></template> | ||||||
|  | 			</MkButton> | ||||||
|  | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | </transition> | ||||||
| 	<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> |  | ||||||
| 		<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> |  | ||||||
| 	</XList> |  | ||||||
| 
 |  | ||||||
| 	<div v-show="more && !reversed" style="margin-top: var(--margin);"> |  | ||||||
| 		<MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> |  | ||||||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> |  | ||||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> |  | ||||||
| 		</MkButton> |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -55,11 +59,15 @@ export default defineComponent({ | ||||||
| 		pagination: { | 		pagination: { | ||||||
| 			required: true | 			required: true | ||||||
| 		}, | 		}, | ||||||
| 
 |  | ||||||
| 		prop: { | 		prop: { | ||||||
| 			type: String, | 			type: String, | ||||||
| 			required: false | 			required: false | ||||||
| 		} | 		}, | ||||||
|  | 		noGap: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['before', 'after'], | 	emits: ['before', 'after'], | ||||||
|  | @ -90,3 +98,14 @@ export default defineComponent({ | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </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,19 +1,23 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mfcuwfyp _noGap_ _magnetParent"> | <transition name="fade" mode="out-in"> | ||||||
| 	<XList class="notifications _magnetChild" :items="items" v-slot="{ item: notification }"> | 	<MkLoading v-if="fetching"/> | ||||||
| 		<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> |  | ||||||
| 
 | 
 | ||||||
| 	<button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | 	<MkError v-else-if="error" @retry="init()"/> | ||||||
| 		<template v-if="!moreFetching">{{ $ts.loadMore }}</template> |  | ||||||
| 		<template v-if="moreFetching"><MkLoading inline/></template> |  | ||||||
| 	</button> |  | ||||||
| 
 | 
 | ||||||
| 	<p class="empty" v-if="empty">{{ $ts.noNotifications }}</p> | 	<p class="mfcuwfyp" v-else-if="empty">{{ $ts.noNotifications }}</p> | ||||||
| 
 | 
 | ||||||
| 	<MkError v-if="error" @retry="init()"/> | 	<div v-else class="_magnetParent"> | ||||||
| </div> | 		<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> | ||||||
|  | 
 | ||||||
|  | 		<button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||||
|  | 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||||
|  | 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||||
|  | 		</button> | ||||||
|  | 	</div> | ||||||
|  | </transition> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -120,17 +124,19 @@ export default defineComponent({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .mfcuwfyp { | .fade-enter-active, | ||||||
| 	> .empty { | .fade-leave-active { | ||||||
| 		margin: 0; | 	transition: opacity 0.125s ease; | ||||||
| 		padding: 16px; | } | ||||||
| 		text-align: center; | .fade-enter-from, | ||||||
| 		color: var(--fg); | .fade-leave-to { | ||||||
| 	} | 	opacity: 0; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 	> .placeholder { | .mfcuwfyp { | ||||||
| 		padding: 32px; | 	margin: 0; | ||||||
| 		opacity: 0.3; | 	padding: 16px; | ||||||
| 	} | 	text-align: center; | ||||||
|  | 	color: var(--fg); | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 		<div class="title"><slot name="header"></slot></div> | 		<div class="title"><slot name="header"></slot></div> | ||||||
| 		<div class="sub"> | 		<div class="sub"> | ||||||
| 			<slot name="func"></slot> | 			<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-if="showBody"><Fa :icon="faAngleUp"/></template> | ||||||
| 				<template v-else><Fa :icon="faAngleDown"/></template> | 				<template v-else><Fa :icon="faAngleDown"/></template> | ||||||
| 			</button> | 			</button> | ||||||
|  | @ -16,8 +16,11 @@ | ||||||
| 		@leave="leave" | 		@leave="leave" | ||||||
| 		@after-leave="afterLeave" | 		@after-leave="afterLeave" | ||||||
| 	> | 	> | ||||||
| 		<div v-show="showBody"> | 		<div v-show="showBody" class="content" :class="{ omitted }" ref="content"> | ||||||
| 			<slot></slot> | 			<slot></slot> | ||||||
|  | 			<button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }"> | ||||||
|  | 				<span>{{ $ts.showMore }}</span> | ||||||
|  | 			</button> | ||||||
| 		</div> | 		</div> | ||||||
| 	</transition> | 	</transition> | ||||||
| </div> | </div> | ||||||
|  | @ -39,7 +42,7 @@ export default defineComponent({ | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
| 		}, | 		}, | ||||||
| 		bodyTogglable: { | 		foldable: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
|  | @ -54,10 +57,17 @@ export default defineComponent({ | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
| 		}, | 		}, | ||||||
|  | 		maxHeight: { | ||||||
|  | 			type: Number, | ||||||
|  | 			required: false, | ||||||
|  | 			default: null | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			showBody: this.expanded, | 			showBody: this.expanded, | ||||||
|  | 			omitted: null, | ||||||
|  | 			ignoreOmit: false, | ||||||
| 			faAngleUp, faAngleDown | 			faAngleUp, faAngleDown | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  | @ -73,10 +83,23 @@ export default defineComponent({ | ||||||
| 		}, { | 		}, { | ||||||
| 			immediate: true | 			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: { | 	methods: { | ||||||
| 		toggleContent(show: boolean) { | 		toggleContent(show: boolean) { | ||||||
| 			if (!this.bodyTogglable) return; | 			if (!this.foldable) return; | ||||||
| 			this.showBody = show; | 			this.showBody = show; | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | @ -127,7 +150,7 @@ export default defineComponent({ | ||||||
| 		display: flex; | 		display: flex; | ||||||
| 		flex-direction: column; | 		flex-direction: column; | ||||||
| 
 | 
 | ||||||
| 		> div { | 		> .content { | ||||||
| 			overflow: auto; | 			overflow: auto; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -169,12 +192,35 @@ export default defineComponent({ | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> div { | 	> .content { | ||||||
| 		> ::v-deep(._content) { | 		&.omitted { | ||||||
| 			padding: 24px; | 			position: relative; | ||||||
|  | 			max-height: var(--maxHeight); | ||||||
|  | 			overflow: hidden; | ||||||
| 
 | 
 | ||||||
| 			& + ._content { | 			> .fade { | ||||||
| 				border-top: solid 0.5px var(--divider); | 				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 { | 		> .content { | ||||||
| 			> ::v-deep(._content) { |  | ||||||
| 				padding: 16px; |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -39,7 +39,7 @@ export default defineComponent({ | ||||||
| 			const contentHeight = this.$refs.content.offsetHeight; | 			const contentHeight = this.$refs.content.offsetHeight; | ||||||
| 
 | 
 | ||||||
| 			let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); | 			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); | 			left -= (this.$el.offsetWidth / 2); | ||||||
| 
 | 
 | ||||||
|  | @ -47,9 +47,9 @@ export default defineComponent({ | ||||||
| 				left = window.innerWidth - contentWidth + window.pageXOffset - 1; | 				left = window.innerWidth - contentWidth + window.pageXOffset - 1; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			if (top + contentHeight - window.pageYOffset > window.innerHeight) { | 			if (top - window.pageYOffset < 0) { | ||||||
| 				top = rect.top + window.pageYOffset - contentHeight; | 				top = rect.top + window.pageYOffset + this.source.offsetHeight; | ||||||
| 				this.$refs.content.style.transformOrigin = 'center bottom'; | 				this.$refs.content.style.transformOrigin = 'center top'; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			this.$el.style.left = left + 'px'; | 			this.$el.style.left = left + 'px'; | ||||||
|  | @ -81,6 +81,6 @@ export default defineComponent({ | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
| 	border-radius: 4px; | 	border-radius: 4px; | ||||||
| 	pointer-events: none; | 	pointer-events: none; | ||||||
| 	transform-origin: center top; | 	transform-origin: center bottom; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <div class="_panel vjnjpkug"> | <div class="_panel vjnjpkug"> | ||||||
| 	<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> | 	<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"> | 	<div class="title"> | ||||||
| 		<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> | 		<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> | ||||||
| 		<p class="username"><MkAcct :user="user"/></p> | 		<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="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 v-if="fetched" class="info"> | ||||||
| 			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> | 			<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"> | 			<div class="title"> | ||||||
| 				<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> | 				<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> | ||||||
| 				<p class="username"><MkAcct :user="user"/></p> | 				<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="tbhwbxda _section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }"> | ||||||
| 		<div class="users" v-if="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()"> | 			<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"> | 				<div class="body"> | ||||||
| 					<MkUserName :user="user" class="name"/> | 					<MkUserName :user="user" class="name"/> | ||||||
| 					<MkAcct :user="user" class="acct"/> | 					<MkAcct :user="user" class="acct"/> | ||||||
|  | @ -31,7 +31,7 @@ | ||||||
| 	<div class="tbhwbxda _section recent" v-if="username == '' && host == ''"> | 	<div class="tbhwbxda _section recent" v-if="username == '' && host == ''"> | ||||||
| 		<div class="users"> | 		<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()"> | 			<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"> | 				<div class="body"> | ||||||
| 					<MkUserName :user="user" class="name"/> | 					<MkUserName :user="user" class="name"/> | ||||||
| 					<MkAcct :user="user" class="acct"/> | 					<MkAcct :user="user" class="acct"/> | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
| 
 | 
 | ||||||
| 	<div class="users"> | 	<div class="users"> | ||||||
| 		<MkA v-for="item in items" class="user" :key="item.id" :to="userPage(extract ? extract(item) : item)"> | 		<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"> | 			<div class="body"> | ||||||
| 				<MkUserName :user="extract ? extract(item) : item" class="name"/> | 				<MkUserName :user="extract ? extract(item) : item" class="name"/> | ||||||
| 				<MkAcct :user="extract ? extract(item) : item" class="acct"/> | 				<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"/> | 	<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> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 			<Mfm :text="clip.description" :is-note="false" :i="$i"/> | 			<Mfm :text="clip.description" :is-note="false" :i="$i"/> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="user"> | 		<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> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ | ||||||
| 			<header><span>{{ $ts.exploreFediverse }}</span></header> | 			<header><span>{{ $ts.exploreFediverse }}</span></header> | ||||||
| 		</div> | 		</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> | 			<template #header><Fa :icon="faHashtag" fixed-width style="margin-right: 0.5em;"/>{{ $ts.popularTags }}</template> | ||||||
| 
 | 
 | ||||||
| 			<div class="vxjfqztj"> | 			<div class="vxjfqztj"> | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
| 		</template> | 		</template> | ||||||
| 		<template #default="{items}"> | 		<template #default="{items}"> | ||||||
| 			<div class="user _panel" v-for="req in items" :key="req.id"> | 			<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="body"> | ||||||
| 					<div class="name"> | 					<div class="name"> | ||||||
| 						<MkA class="name" :to="userPage(req.follower)" v-user-preview="req.follower.id"><MkUserName :user="req.follower"/></MkA> | 						<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);"> | 			<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="bcekxzvu _card _gap" v-for="report in items" :key="report.id"> | ||||||
| 					<div class="_content target"> | 					<div class="_content target"> | ||||||
| 						<MkAvatar class="avatar" :user="report.targetUser"/> | 						<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/> | ||||||
| 						<div class="info"> | 						<div class="info"> | ||||||
| 							<MkUserName class="name" :user="report.targetUser"/> | 							<MkUserName class="name" :user="report.targetUser"/> | ||||||
| 							<div class="acct">@{{ acct(report.targetUser) }}</div> | 							<div class="acct">@{{ acct(report.targetUser) }}</div> | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 		<template #header><Fa :icon="faHeartbeat"/> {{ $ts.metrics }}</template> | 		<template #header><Fa :icon="faHeartbeat"/> {{ $ts.metrics }}</template> | ||||||
| 		<div class="_section" style="padding: 0 var(--margin);"> | 		<div class="_section" style="padding: 0 var(--margin);"> | ||||||
| 			<div class="_content"> | 			<div class="_content"> | ||||||
| 				<MkContainer :body-togglable="false" class="_gap"> | 				<MkContainer :foldable="false" class="_gap"> | ||||||
| 					<template #header><Fa :icon="faMicrochip"/>{{ $ts.cpuAndMemory }}</template> | 					<template #header><Fa :icon="faMicrochip"/>{{ $ts.cpuAndMemory }}</template> | ||||||
| 					<!-- | 					<!-- | ||||||
| 					<template #func> | 					<template #func> | ||||||
|  | @ -27,7 +27,7 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</MkContainer> | 				</MkContainer> | ||||||
| 
 | 
 | ||||||
| 				<MkContainer :body-togglable="false" class="_gap"> | 				<MkContainer :foldable="false" class="_gap"> | ||||||
| 					<template #header><Fa :icon="faHdd"/> {{ $ts.disk }}</template> | 					<template #header><Fa :icon="faHdd"/> {{ $ts.disk }}</template> | ||||||
| 					<!-- | 					<!-- | ||||||
| 					<template #func> | 					<template #func> | ||||||
|  | @ -50,7 +50,7 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</MkContainer> | 				</MkContainer> | ||||||
| 
 | 
 | ||||||
| 				<MkContainer :body-togglable="false" class="_gap"> | 				<MkContainer :foldable="false" class="_gap"> | ||||||
| 					<template #header><Fa :icon="faExchangeAlt"/> {{ $ts.network }}</template> | 					<template #header><Fa :icon="faExchangeAlt"/> {{ $ts.network }}</template> | ||||||
| 					<!-- | 					<!-- | ||||||
| 					<template #func> | 					<template #func> | ||||||
|  | @ -78,7 +78,7 @@ | ||||||
| 		<template #header><Fa :icon="faClipboardList"/> {{ $ts.jobQueue }}</template> | 		<template #header><Fa :icon="faClipboardList"/> {{ $ts.jobQueue }}</template> | ||||||
| 
 | 
 | ||||||
| 		<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }"> | 		<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> | 				<template #header><Fa :icon="faExclamationTriangle"/> {{ $ts.delayed }}</template> | ||||||
| 
 | 
 | ||||||
| 				<div class="_content"> | 				<div class="_content"> | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| 		<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }"> | 		<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }"> | ||||||
| 			<MkInstanceStats :chart-limit="300" :detailed="true" class="_gap" ref="stats"/> | 			<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> | 				<template #header><Fa :icon="faInfoCircle"/>{{ $ts.instanceInfo }}</template> | ||||||
| 
 | 
 | ||||||
| 				<div class="_content"> | 				<div class="_content"> | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</MkContainer> | 			</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> | 				<template #header><Fa :icon="faDatabase"/>{{ $ts.database }}</template> | ||||||
| 
 | 
 | ||||||
| 				<div class="_content" v-if="dbInfo"> | 				<div class="_content" v-if="dbInfo"> | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
| 	<div class="vrcsvlkm" v-if="user && info"> | 	<div class="vrcsvlkm" v-if="user && info"> | ||||||
| 		<div class="_section"> | 		<div class="_section"> | ||||||
| 			<div class="banner" :style="bannerStyle"> | 			<div class="banner" :style="bannerStyle"> | ||||||
| 				<MkAvatar class="avatar" :user="user"/> | 				<MkAvatar class="avatar" :user="user" :show-indicator="true"/> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="_section"> | 		<div class="_section"> | ||||||
|  |  | ||||||
|  | @ -54,7 +54,7 @@ | ||||||
| 
 | 
 | ||||||
| 			<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users"> | 			<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)"> | 				<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"> | 					<div class="body"> | ||||||
| 						<header> | 						<header> | ||||||
| 							<MkUserName class="name" :user="user"/> | 							<MkUserName class="name" :user="user"/> | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ | ||||||
| 			v-anim="i" | 			v-anim="i" | ||||||
| 		> | 		> | ||||||
| 			<div> | 			<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"> | 				<header v-if="message.groupId"> | ||||||
| 					<span class="name">{{ message.group.name }}</span> | 					<span class="name">{{ message.group.name }}</span> | ||||||
| 					<MkTime :time="message.createdAt" class="time"/> | 					<MkTime :time="message.createdAt" class="time"/> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <div class="thvuemwp" :class="{ isMe }" v-size="{ max: [400, 500] }"> | <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="content"> | ||||||
| 		<div class="balloon" :class="{ noText: message.text == null }" > | 		<div class="balloon" :class="{ noText: message.text == null }" > | ||||||
| 			<button class="delete-button" v-if="isMe" :title="$ts.delete" @click="del"> | 			<button class="delete-button" v-if="isMe" :title="$ts.delete" @click="del"> | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ | ||||||
| 			<div class="_content"> | 			<div class="_content"> | ||||||
| 				<div class="users"> | 				<div class="users"> | ||||||
| 					<div class="user _panel" v-for="user in users" :key="user.id"> | 					<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"> | 						<div class="body"> | ||||||
| 							<MkUserName :user="user" class="name"/> | 							<MkUserName :user="user" class="name"/> | ||||||
| 							<MkAcct :user="user" class="acct"/> | 							<MkAcct :user="user" class="acct"/> | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ | ||||||
| 			<div class="_content"> | 			<div class="_content"> | ||||||
| 				<div class="users"> | 				<div class="users"> | ||||||
| 					<div class="user _panel" v-for="user in users" :key="user.id"> | 					<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"> | 						<div class="body"> | ||||||
| 							<MkUserName :user="user" class="name"/> | 							<MkUserName :user="user" class="name"/> | ||||||
| 							<MkAcct :user="user" class="acct"/> | 							<MkAcct :user="user" class="acct"/> | ||||||
|  |  | ||||||
|  | @ -1,37 +1,37 @@ | ||||||
| <template> | <template> | ||||||
| <div class="fcuexfpr _root"> | <div class="fcuexfpr _root"> | ||||||
| 	<div v-if="note" class="note" v-anim> | 	<transition name="fade" mode="out-in"> | ||||||
| 		<div class="_gap" v-if="showNext"> | 		<div v-if="note" class="note"> | ||||||
| 			<XNotes class="_content _noGap_" :pagination="next"/> | 			<div class="_gap" v-if="showNext"> | ||||||
| 		</div> | 				<XNotes class="_content" :pagination="next" :no-gap="true"/> | ||||||
| 
 |  | ||||||
| 		<div class="main _gap"> |  | ||||||
| 			<MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton> |  | ||||||
| 			<div class="_content _gap"> |  | ||||||
| 				<MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_gap"/> |  | ||||||
| 				<XNoteDetailed v-model:note="note" :key="note.id" class="_gap"/> |  | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="_content clips _gap" v-if="clips && clips.length > 0"> | 
 | ||||||
| 				<div class="title">{{ $ts.clip }}</div> | 			<div class="main _gap"> | ||||||
| 				<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> | 				<MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton> | ||||||
| 					<b>{{ item.name }}</b> | 				<div class="_content _gap"> | ||||||
| 					<div v-if="item.description" class="description">{{ item.description }}</div> | 					<MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_gap"/> | ||||||
| 					<div class="user"> | 					<XNoteDetailed v-model:note="note" :key="note.id" class="_gap"/> | ||||||
| 						<MkAvatar :user="item.user" class="avatar"/> <MkUserName :user="item.user" :nowrap="false"/> | 				</div> | ||||||
| 					</div> | 				<div class="_content clips _gap" v-if="clips && clips.length > 0"> | ||||||
| 				</MkA> | 					<div class="title">{{ $ts.clip }}</div> | ||||||
|  | 					<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> | ||||||
|  | 						<b>{{ item.name }}</b> | ||||||
|  | 						<div v-if="item.description" class="description">{{ item.description }}</div> | ||||||
|  | 						<div class="user"> | ||||||
|  | 							<MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/> | ||||||
|  | 						</div> | ||||||
|  | 					</MkA> | ||||||
|  | 				</div> | ||||||
|  | 				<MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton> | ||||||
| 			</div> | 			</div> | ||||||
| 			<MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton> |  | ||||||
| 		</div> |  | ||||||
| 
 | 
 | ||||||
| 		<div class="_gap" v-if="showPrev"> | 			<div class="_gap" v-if="showPrev"> | ||||||
| 			<XNotes class="_content _noGap_" :pagination="prev"/> | 				<XNotes class="_content" :pagination="prev" :no-gap="true"/> | ||||||
|  | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 		<MkError v-else-if="error" @retry="fetch()"/> | ||||||
| 
 | 		<MkLoading v-else/> | ||||||
| 	<div v-if="error"> | 	</transition> | ||||||
| 		<MkError @retry="fetch()"/> |  | ||||||
| 	</div> |  | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -106,6 +106,7 @@ export default defineComponent({ | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		fetch() { | 		fetch() { | ||||||
|  | 			this.note = null; | ||||||
| 			os.api('notes/show', { | 			os.api('notes/show', { | ||||||
| 				noteId: this.noteId | 				noteId: this.noteId | ||||||
| 			}).then(note => { | 			}).then(note => { | ||||||
|  | @ -138,6 +139,15 @@ export default defineComponent({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  | .fade-enter-active, | ||||||
|  | .fade-leave-active { | ||||||
|  | 	transition: opacity 0.125s ease; | ||||||
|  | } | ||||||
|  | .fade-enter-from, | ||||||
|  | .fade-leave-to { | ||||||
|  | 	opacity: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .fcuexfpr { | .fcuexfpr { | ||||||
| 	> .note { | 	> .note { | ||||||
| 		> .main { | 		> .main { | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
| 		<MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton> | 		<MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton> | ||||||
| 	</div> | 	</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> | 		<template #header><Fa :icon="faCog"/> {{ $ts._pages.pageSetting }}</template> | ||||||
| 		<div style="padding: 16px;"> | 		<div style="padding: 16px;"> | ||||||
| 			<MkInput v-model:value="title"> | 			<MkInput v-model:value="title"> | ||||||
|  | @ -44,7 +44,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</MkContainer> | 	</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> | 		<template #header><Fa :icon="faStickyNote"/> {{ $ts._pages.contents }}</template> | ||||||
| 		<div style="padding: 16px;"> | 		<div style="padding: 16px;"> | ||||||
| 			<XBlocks class="content" v-model:value="content" :hpml="hpml"/> | 			<XBlocks class="content" v-model:value="content" :hpml="hpml"/> | ||||||
|  | @ -53,7 +53,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</MkContainer> | 	</MkContainer> | ||||||
| 
 | 
 | ||||||
| 	<MkContainer :body-togglable="true" class="_gap"> | 	<MkContainer :foldable="true" class="_gap"> | ||||||
| 		<template #header><Fa :icon="faMagic"/> {{ $ts._pages.variables }}</template> | 		<template #header><Fa :icon="faMagic"/> {{ $ts._pages.variables }}</template> | ||||||
| 		<div class="qmuvgica"> | 		<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"> | 			<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> | 		</div> | ||||||
| 	</MkContainer> | 	</MkContainer> | ||||||
| 
 | 
 | ||||||
| 	<MkContainer :body-togglable="true" :expanded="true" class="_gap"> | 	<MkContainer :foldable="true" :expanded="true" class="_gap"> | ||||||
| 		<template #header><Fa :icon="faCode"/> {{ $ts.script }}</template> | 		<template #header><Fa :icon="faCode"/> {{ $ts.script }}</template> | ||||||
| 		<div> | 		<div> | ||||||
| 			<MkTextarea class="_code" v-model:value="script"/> | 			<MkTextarea class="_code" v-model:value="script"/> | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ | ||||||
| 			<template #header>{{ $ts.invitations }}</template> | 			<template #header>{{ $ts.invitations }}</template> | ||||||
| 			<div class="nfcacttm"> | 			<div class="nfcacttm"> | ||||||
| 				<button class="invitation _panel _button" v-for="invitation in invitations" tabindex="-1" @click="accept(invitation)"> | 				<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="name"><b><MkUserName :user="invitation.parent"/></b></span> | ||||||
| 					<span class="username">@{{ invitation.parent.username }}</span> | 					<span class="username">@{{ invitation.parent.username }}</span> | ||||||
| 					<MkTime :time="invitation.createdAt" class="time"/> | 					<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> | 		<MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><Fa :icon="faPlay"/></MkButton> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<MkContainer :body-togglable="true" class="_gap"> | 	<MkContainer :foldable="true" class="_gap"> | ||||||
| 		<template #header><Fa fixed-width/>{{ $ts.output }}</template> | 		<template #header><Fa fixed-width/>{{ $ts.output }}</template> | ||||||
| 		<div class="bepmlvbi"> | 		<div class="bepmlvbi"> | ||||||
| 			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> | 			<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> | 		<FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch> | ||||||
| 		<template #caption>{{ $ts.lockedAccountInfo }}</template> | 		<template #caption>{{ $ts.lockedAccountInfo }}</template> | ||||||
| 	</FormGroup> | 	</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()"> | 	<FormSwitch v-model:value="noCrawle" @update:value="save()"> | ||||||
| 		{{ $ts.noCrawle }} | 		{{ $ts.noCrawle }} | ||||||
| 		<template #desc>{{ $ts.noCrawleDescription }}</template> | 		<template #desc>{{ $ts.noCrawleDescription }}</template> | ||||||
|  | @ -58,6 +62,7 @@ export default defineComponent({ | ||||||
| 			autoAcceptFollowed: false, | 			autoAcceptFollowed: false, | ||||||
| 			noCrawle: false, | 			noCrawle: false, | ||||||
| 			isExplorable: false, | 			isExplorable: false, | ||||||
|  | 			hideOnlineStatus: false, | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -72,6 +77,7 @@ export default defineComponent({ | ||||||
| 		this.autoAcceptFollowed = this.$i.autoAcceptFollowed; | 		this.autoAcceptFollowed = this.$i.autoAcceptFollowed; | ||||||
| 		this.noCrawle = this.$i.noCrawle; | 		this.noCrawle = this.$i.noCrawle; | ||||||
| 		this.isExplorable = this.$i.isExplorable; | 		this.isExplorable = this.$i.isExplorable; | ||||||
|  | 		this.hideOnlineStatus = this.$i.hideOnlineStatus; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
|  | @ -85,6 +91,7 @@ export default defineComponent({ | ||||||
| 				autoAcceptFollowed: !!this.autoAcceptFollowed, | 				autoAcceptFollowed: !!this.autoAcceptFollowed, | ||||||
| 				noCrawle: !!this.noCrawle, | 				noCrawle: !!this.noCrawle, | ||||||
| 				isExplorable: !!this.isExplorable, | 				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> | <template> | ||||||
| <MkContainer> | <MkContainer :max-height="300" :foldable="true"> | ||||||
| 	<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $ts.images }}</template> | 	<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $ts.images }}</template> | ||||||
| 	<div class="ujigsodd"> | 	<div class="ujigsodd"> | ||||||
| 		<MkLoading v-if="fetching"/> | 		<MkLoading v-if="fetching"/> | ||||||
| 		<div class="stream" v-if="!fetching && images.length > 0"> | 		<div class="stream" v-if="!fetching && images.length > 0"> | ||||||
| 			<MkA v-for="image in images" | 			<MkA v-for="image in images" | ||||||
| 				class="img" | 				class="img" | ||||||
| 				:style="`background-image: url(${thumbnail(image.file)})`" |  | ||||||
| 				:to="notePage(image.note)" | 				:to="notePage(image.note)" | ||||||
| 			></MkA> | 				:key="image.id" | ||||||
|  | 			> | ||||||
|  | 				<ImgWithBlurhash :hash="image.blurhash" :src="thumbnail(image.file)" :alt="image.name" :title="image.name"/> | ||||||
|  | 			</MkA> | ||||||
| 		</div> | 		</div> | ||||||
| 		<p class="empty" v-if="!fetching && images.length == 0">{{ $ts.nothing }}</p> | 		<p class="empty" v-if="!fetching && images.length == 0">{{ $ts.nothing }}</p> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -22,10 +24,12 @@ import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | ||||||
| import notePage from '../../filters/note'; | import notePage from '../../filters/note'; | ||||||
| import * as os from '@client/os'; | import * as os from '@client/os'; | ||||||
| import MkContainer from '@client/components/ui/container.vue'; | import MkContainer from '@client/components/ui/container.vue'; | ||||||
|  | import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkContainer, | 		MkContainer, | ||||||
|  | 		ImgWithBlurhash, | ||||||
| 	}, | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		user: { | 		user: { | ||||||
|  | @ -52,16 +56,14 @@ export default defineComponent({ | ||||||
| 			userId: this.user.id, | 			userId: this.user.id, | ||||||
| 			fileType: image, | 			fileType: image, | ||||||
| 			excludeNsfw: this.$store.state.nsfw !== 'ignore', | 			excludeNsfw: this.$store.state.nsfw !== 'ignore', | ||||||
| 			limit: 9, | 			limit: 10, | ||||||
| 		}).then(notes => { | 		}).then(notes => { | ||||||
| 			for (const note of notes) { | 			for (const note of notes) { | ||||||
| 				for (const file of note.files) { | 				for (const file of note.files) { | ||||||
| 					if (this.images.length < 9) { | 					this.images.push({ | ||||||
| 						this.images.push({ | 						note, | ||||||
| 							note, | 						file | ||||||
| 							file | 					}); | ||||||
| 						}); |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			this.fetching = false; | 			this.fetching = false; | ||||||
|  | @ -83,20 +85,14 @@ export default defineComponent({ | ||||||
| 	padding: 8px; | 	padding: 8px; | ||||||
| 
 | 
 | ||||||
| 	> .stream { | 	> .stream { | ||||||
| 		display: flex; | 		display: grid; | ||||||
| 		justify-content: center; | 		grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | ||||||
| 		flex-wrap: wrap; | 		grid-gap: 6px; | ||||||
| 
 | 
 | ||||||
| 		> .img { | 		> .img { | ||||||
| 			flex: 1 1 33%; | 			height: 128px; | ||||||
| 			width: 33%; |  | ||||||
| 			height: 90px; |  | ||||||
| 			box-sizing: border-box; |  | ||||||
| 			background-position: center center; |  | ||||||
| 			background-size: cover; |  | ||||||
| 			background-clip: content-box; |  | ||||||
| 			border: solid 2px transparent; |  | ||||||
| 			border-radius: 6px; | 			border-radius: 6px; | ||||||
|  | 			overflow: clip; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 		<option value="replies">{{ $ts.notesAndReplies }}</option> | 		<option value="replies">{{ $ts.notesAndReplies }}</option> | ||||||
| 		<option value="files">{{ $ts.withFiles }}</option> | 		<option value="files">{{ $ts.withFiles }}</option> | ||||||
| 	</MkTab> | 	</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> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <transition name="fade" mode="out-in"> | ||||||
| 	<div class="ftskorzw wide _section" v-if="user && narrow === false"> | 	<div class="ftskorzw wide" v-if="user && narrow === false"> | ||||||
| 		<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_gap"/> | 		<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_gap"/> | ||||||
| 
 | 
 | ||||||
| 		<div class="banner-container _gap" :style="style"> | 		<div class="banner-container _gap" :style="style"> | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="contents"> | 		<div class="contents"> | ||||||
| 			<div class="side _forceContainerFull_"> | 			<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"> | 				<div class="name"> | ||||||
| 					<MkUserName :user="user" :nowrap="false" class="name"/> | 					<MkUserName :user="user" :nowrap="false" class="name"/> | ||||||
| 					<MkAcct :user="user" :detail="true" class="acct"/> | 					<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"/> | 						<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<MkAvatar class="avatar" :user="user" :disable-preview="true"/> | 				<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> | ||||||
| 				<div class="title"> | 				<div class="title"> | ||||||
| 					<MkUserName :user="user" :nowrap="false" class="name"/> | 					<MkUserName :user="user" :nowrap="false" class="name"/> | ||||||
| 					<div class="bottom"> | 					<div class="bottom"> | ||||||
|  | @ -212,10 +212,9 @@ | ||||||
| 			<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> | 			<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div v-else-if="error"> | 	<MkError v-else-if="error" @retry="fetch()"/> | ||||||
| 		<MkError @retry="fetch()"/> | 	<MkLoading v-else/> | ||||||
| 	</div> | </transition> | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -279,6 +278,7 @@ export default defineComponent({ | ||||||
| 				share: { | 				share: { | ||||||
| 					title: this.user.name, | 					title: this.user.name, | ||||||
| 				}, | 				}, | ||||||
|  | 				menu: () => getUserMenu(this.user), | ||||||
| 			} : null), | 			} : null), | ||||||
| 			user: null, | 			user: null, | ||||||
| 			error: null, | 			error: null, | ||||||
|  | @ -323,6 +323,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 		fetch() { | 		fetch() { | ||||||
| 			if (this.acct == null) return; | 			if (this.acct == null) return; | ||||||
|  | 			this.user = null; | ||||||
| 			Progress.start(); | 			Progress.start(); | ||||||
| 			os.api('users/show', parseAcct(this.acct)).then(user => { | 			os.api('users/show', parseAcct(this.acct)).then(user => { | ||||||
| 				this.user = user; | 				this.user = user; | ||||||
|  | @ -368,6 +369,15 @@ export default defineComponent({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <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 { | .ftskorzw.wide { | ||||||
| 	max-width: 1150px; | 	max-width: 1150px; | ||||||
| 	margin: 0 auto; | 	margin: 0 auto; | ||||||
|  |  | ||||||
|  | @ -72,6 +72,9 @@ export const router = createRouter({ | ||||||
| 		{ path: '/instance/abuses', component: page('instance/abuses') }, | 		{ path: '/instance/abuses', component: page('instance/abuses') }, | ||||||
| 		{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, | 		{ 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: '/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', component: page('reversi/index') }, | ||||||
| 		{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) }, | 		{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) }, | ||||||
| 		{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') }, | 		{ 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 { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { i18n } from '@client/i18n'; | import { i18n } from '@client/i18n'; | ||||||
| import copyToClipboard from '@client/scripts/copy-to-clipboard'; | import copyToClipboard from '@client/scripts/copy-to-clipboard'; | ||||||
|  | @ -126,6 +126,12 @@ export function getUserMenu(user) { | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			copyToClipboard(`@${user.username}@${user.host || host}`); | 			copyToClipboard(`@${user.username}@${user.host || host}`); | ||||||
| 		} | 		} | ||||||
|  | 	}, { | ||||||
|  | 		icon: faInfoCircle, | ||||||
|  | 		text: i18n.locale.info, | ||||||
|  | 		action: () => { | ||||||
|  | 			os.pageWindow(`/user-info/${user.id}`); | ||||||
|  | 		} | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: faEnvelope, | 		icon: faEnvelope, | ||||||
| 		text: i18n.locale.sendMessage, | 		text: i18n.locale.sendMessage, | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ html { | ||||||
| 	background-position: center; | 	background-position: center; | ||||||
| 	color: var(--fg); | 	color: var(--fg); | ||||||
| 	overflow: auto; | 	overflow: auto; | ||||||
|  | 	overflow-wrap: break-word; | ||||||
| 	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; | 	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; | ||||||
| 	line-height: 1.35; | 	line-height: 1.35; | ||||||
| 	text-size-adjust: 100%; | 	text-size-adjust: 100%; | ||||||
|  | @ -88,10 +89,6 @@ html._themeChanging_ { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| body { |  | ||||||
| 	overflow-wrap: break-word; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| html, body { | html, body { | ||||||
| 	margin: 0; | 	margin: 0; | ||||||
| 	padding: 0; | 	padding: 0; | ||||||
|  | @ -458,7 +455,7 @@ hr { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ._monospace { | ._monospace { | ||||||
| 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; | 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ._code { | ._code { | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
| 		<div class="titleContainer"> | 		<div class="titleContainer"> | ||||||
| 			<div class="title"> | 			<div class="title"> | ||||||
| 				<Fa v-if="info.icon" :icon="info.icon" :key="info.icon" class="icon"/> | 				<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"/> | 				<MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> | ||||||
| 				<span v-else-if="info.title" class="text">{{ info.title }}</span> | 				<span v-else-if="info.title" class="text">{{ info.title }}</span> | ||||||
| 			</div> | 			</div> | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ | ||||||
| 		<div>{{ $ts.noNotes }}</div> | 		<div>{{ $ts.noNotes }}</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
|  | 	<MkLoading v-if="fetching"/> | ||||||
|  | 
 | ||||||
| 	<MkError v-if="error" @retry="init()"/> | 	<MkError v-if="error" @retry="init()"/> | ||||||
| 
 | 
 | ||||||
| 	<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | 	<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | ||||||
|  |  | ||||||
|  | @ -310,6 +310,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 		> .widgets { | 		> .widgets { | ||||||
| 			//--panelShadow: none; | 			//--panelShadow: none; | ||||||
|  | 			width: 300px; | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: $widgets-hide-threshold) { | 			@media (max-width: $widgets-hide-threshold) { | ||||||
| 				display: none; | 				display: none; | ||||||
|  |  | ||||||
|  | @ -2,8 +2,8 @@ | ||||||
| <div class="efzpzdvf"> | <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"/> | 	<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-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" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> | 	<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -62,18 +62,11 @@ export default defineComponent({ | ||||||
| 	position: sticky; | 	position: sticky; | ||||||
| 	height: min-content; | 	height: min-content; | ||||||
| 	box-sizing: border-box; | 	box-sizing: border-box; | ||||||
|  | 	padding-bottom: 8px; | ||||||
| 
 | 
 | ||||||
| 	> * { | 	> .edit { | ||||||
| 		margin: var(--margin) 0; | 		display: block; | ||||||
| 		width: 300px; | 		margin: 16px auto; | ||||||
| 
 |  | ||||||
| 		&:first-child { |  | ||||||
| 			margin-top: 0; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .add { |  | ||||||
| 		margin: 0 auto; |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
| 			<div class="page active link" v-if="info"> | 			<div class="page active link" v-if="info"> | ||||||
| 				<div class="title"> | 				<div class="title"> | ||||||
| 					<Fa v-if="info.icon" :icon="info.icon" :key="info.icon" class="icon"/> | 					<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> | 					<span v-if="info.title" class="text">{{ info.title }}</span> | ||||||
| 					<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> | 					<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> | ||||||
| 				</div> | 				</div> | ||||||
|  | @ -28,7 +28,7 @@ | ||||||
| 		</button> | 		</button> | ||||||
| 		<div class="title" v-if="info"> | 		<div class="title" v-if="info"> | ||||||
| 			<Fa v-if="info.icon" :icon="info.icon" :key="info.icon" class="icon"/> | 			<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> | 			<span v-if="info.title" class="text">{{ info.title }}</span> | ||||||
| 			<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> | 			<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> | ||||||
| 		</div> | 		</div> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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> | 	<template #header><Fa :icon="faGlobe"/>{{ $ts._widgets.federation }}</template> | ||||||
| 
 | 
 | ||||||
| 	<div class="wbrkwalb"> | 	<div class="wbrkwalb"> | ||||||
|  | @ -42,7 +42,7 @@ export default defineComponent({ | ||||||
| 		MkContainer, MkMiniChart | 		MkContainer, MkMiniChart | ||||||
| 	}, | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		bodyTogglable: { | 		foldable: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: 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; | 	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', { | 	@Column('varchar', { | ||||||
| 		length: 128, | 		length: 128, | ||||||
| 		comment: 'The username of the User.' | 		comment: 'The username of the User.' | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import { SchemaType } from '@/misc/schema'; | ||||||
| import { awaitAll } from '../../prelude/await-all'; | import { awaitAll } from '../../prelude/await-all'; | ||||||
| import { populateEmojis } from '@/misc/populate-emojis'; | import { populateEmojis } from '@/misc/populate-emojis'; | ||||||
| import { getAntennas } from '@/misc/antenna-cache'; | import { getAntennas } from '@/misc/antenna-cache'; | ||||||
|  | import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const'; | ||||||
| 
 | 
 | ||||||
| export type PackedUser = SchemaType<typeof packedUserSchema>; | export type PackedUser = SchemaType<typeof packedUserSchema>; | ||||||
| 
 | 
 | ||||||
|  | @ -145,6 +146,17 @@ export class UserRepository extends Repository<User> { | ||||||
| 		return count > 0; | 		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( | 	public async pack( | ||||||
| 		src: User['id'] | User, | 		src: User['id'] | User, | ||||||
| 		me?: { id: User['id'] } | null | undefined, | 		me?: { id: User['id'] } | null | undefined, | ||||||
|  | @ -192,11 +204,14 @@ export class UserRepository extends Repository<User> { | ||||||
| 				themeColor: instance.themeColor, | 				themeColor: instance.themeColor, | ||||||
| 			} : undefined) : undefined, | 			} : undefined) : undefined, | ||||||
| 			emojis: populateEmojis(user.emojis, user.host), | 			emojis: populateEmojis(user.emojis, user.host), | ||||||
|  | 			onlineStatus: this.getOnlineStatus(user), | ||||||
| 
 | 
 | ||||||
| 			...(opts.detail ? { | 			...(opts.detail ? { | ||||||
| 				url: profile!.url, | 				url: profile!.url, | ||||||
|  | 				uri: user.uri, | ||||||
| 				createdAt: user.createdAt.toISOString(), | 				createdAt: user.createdAt.toISOString(), | ||||||
| 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, | 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, | ||||||
|  | 				lastFetchedAt: user.lastFetchedAt?.toISOString(), | ||||||
| 				bannerUrl: user.bannerUrl, | 				bannerUrl: user.bannerUrl, | ||||||
| 				bannerBlurhash: user.bannerBlurhash, | 				bannerBlurhash: user.bannerBlurhash, | ||||||
| 				bannerColor: null, // 後方互換性のため
 | 				bannerColor: null, // 後方互換性のため
 | ||||||
|  | @ -237,6 +252,7 @@ export class UserRepository extends Repository<User> { | ||||||
| 				autoAcceptFollowed: profile!.autoAcceptFollowed, | 				autoAcceptFollowed: profile!.autoAcceptFollowed, | ||||||
| 				noCrawle: profile!.noCrawle, | 				noCrawle: profile!.noCrawle, | ||||||
| 				isExplorable: user.isExplorable, | 				isExplorable: user.isExplorable, | ||||||
|  | 				hideOnlineStatus: user.hideOnlineStatus, | ||||||
| 				hasUnreadSpecifiedNotes: NoteUnreads.count({ | 				hasUnreadSpecifiedNotes: NoteUnreads.count({ | ||||||
| 					where: { userId: user.id, isSpecified: true }, | 					where: { userId: user.id, isSpecified: true }, | ||||||
| 					take: 1 | 					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 define from '../define'; | ||||||
| import { redisClient } from '../../../db/redis'; |  | ||||||
| import config from '@/config'; |  | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	tags: ['meta'], | 	tags: ['meta'], | ||||||
|  | @ -11,12 +12,12 @@ export const meta = { | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, (ps, user) => { | export default define(meta, async () => { | ||||||
| 	return new Promise((res, rej) => { | 	const count = await Users.count({ | ||||||
| 		redisClient.pubsub('numsub', config.host, (_, x) => { | 		lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD)) | ||||||
| 			res({ |  | ||||||
| 				count: x[1] |  | ||||||
| 			}); |  | ||||||
| 		}); |  | ||||||
| 	}); | 	}); | ||||||
|  | 
 | ||||||
|  | 	return { | ||||||
|  | 		count | ||||||
|  | 	}; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -96,6 +96,10 @@ export const meta = { | ||||||
| 			validator: $.optional.bool, | 			validator: $.optional.bool, | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		hideOnlineStatus: { | ||||||
|  | 			validator: $.optional.bool, | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		carefulBot: { | 		carefulBot: { | ||||||
| 			validator: $.optional.bool, | 			validator: $.optional.bool, | ||||||
| 			desc: { | 			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 (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; | ||||||
| 	if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; | 	if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; | ||||||
| 	if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; | 	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.isBot === 'boolean') updates.isBot = ps.isBot; | ||||||
| 	if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; | 	if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; | ||||||
| 	if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; | 	if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import { ParsedUrlQuery } from 'querystring'; | ||||||
| import authenticate from './authenticate'; | import authenticate from './authenticate'; | ||||||
| import { EventEmitter } from 'events'; | import { EventEmitter } from 'events'; | ||||||
| import { subsdcriber as redisClient } from '../../db/redis'; | import { subsdcriber as redisClient } from '../../db/redis'; | ||||||
|  | import { Users } from '@/models'; | ||||||
| 
 | 
 | ||||||
| module.exports = (server: http.Server) => { | module.exports = (server: http.Server) => { | ||||||
| 	// Init websocket server
 | 	// Init websocket server
 | ||||||
|  | @ -45,5 +46,11 @@ module.exports = (server: http.Server) => { | ||||||
| 				connection.send('pong'); | 				connection.send('pong'); | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (user) { | ||||||
|  | 			Users.update(user.id, { | ||||||
|  | 				lastActiveDate: new Date(), | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
| 	}); | 	}); | ||||||
| }; | }; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue