Compare commits
	
		
			32 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ca668898f4 | ||
|   | fcd437c89f | ||
|   | 7f7d7edc7f | ||
|   | bd827f946a | ||
|   | ad8aa1c179 | ||
|   | 3ebaf83ce0 | ||
|   | 39b1978ff3 | ||
|   | bddff17e5e | ||
|   | 0ac9120064 | ||
|   | d90f75425f | ||
|   | dec7d537dc | ||
|   | 11e95ea092 | ||
|   | c5e9b69eb3 | ||
|   | 120c11b181 | ||
|   | a1ae832129 | ||
|   | 3a4833818f | ||
|   | 8814fc9c9c | ||
|   | e6e02ece89 | ||
|   | 9059c149dd | ||
|   | 7d8e70b2ac | ||
|   | 89105f5641 | ||
|   | 1813d17b4c | ||
|   | ce27b36fd0 | ||
|   | e635a87628 | ||
|   | 80c52433cc | ||
|   | 1472f0b141 | ||
|   | 4d914f5c0a | ||
|   | 0318f7344f | ||
|   | 413fbb3d0c | ||
|   | 8bc47baf4f | ||
|   | e3f6d42a47 | ||
|   | 8230935fd3 | 
| @@ -1,8 +1,8 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <i@syuilo.com>", | ||||
| 	"version": "2.37.7", | ||||
| 	"clientVersion": "1.0.6474", | ||||
| 	"version": "2.40.1", | ||||
| 	"clientVersion": "1.0.6504", | ||||
| 	"codename": "nighthike", | ||||
| 	"main": "./built/index.js", | ||||
| 	"private": true, | ||||
| @@ -211,7 +211,6 @@ | ||||
| 		"vue-js-modal": "1.3.13", | ||||
| 		"vue-json-tree-view": "2.1.4", | ||||
| 		"vue-loader": "15.2.1", | ||||
| 		"vue-material": "^1.0.0-beta-10.2", | ||||
| 		"vue-router": "3.0.1", | ||||
| 		"vue-template-compiler": "2.5.16", | ||||
| 		"vuedraggable": "2.16.0", | ||||
|   | ||||
| @@ -7,11 +7,6 @@ html | ||||
| 			cursor progress !important | ||||
|  | ||||
| body | ||||
| 	// for md | ||||
| 	font-size 16px !important | ||||
| 	line-height initial !important | ||||
| 	letter-spacing initial !important | ||||
|  | ||||
| 	overflow-wrap break-word | ||||
|  | ||||
| #error | ||||
|   | ||||
| @@ -29,6 +29,14 @@ import fileTypeIcon from './file-type-icon.vue'; | ||||
| import Switch from './switch.vue'; | ||||
| import Othello from './othello.vue'; | ||||
| import welcomeTimeline from './welcome-timeline.vue'; | ||||
| import uiInput from './ui/input.vue'; | ||||
| import uiButton from './ui/button.vue'; | ||||
| import uiCard from './ui/card.vue'; | ||||
| import uiForm from './ui/form.vue'; | ||||
| import uiTextarea from './ui/textarea.vue'; | ||||
| import uiSwitch from './ui/switch.vue'; | ||||
| import uiRadio from './ui/radio.vue'; | ||||
| import uiSelect from './ui/select.vue'; | ||||
|  | ||||
| Vue.component('mk-analog-clock', analogClock); | ||||
| Vue.component('mk-menu', menu); | ||||
| @@ -59,3 +67,11 @@ Vue.component('mk-file-type-icon', fileTypeIcon); | ||||
| Vue.component('mk-switch', Switch); | ||||
| Vue.component('mk-othello', Othello); | ||||
| Vue.component('mk-welcome-timeline', welcomeTimeline); | ||||
| Vue.component('ui-input', uiInput); | ||||
| Vue.component('ui-button', uiButton); | ||||
| Vue.component('ui-card', uiCard); | ||||
| Vue.component('ui-form', uiForm); | ||||
| Vue.component('ui-textarea', uiTextarea); | ||||
| Vue.component('ui-switch', uiSwitch); | ||||
| Vue.component('ui-radio', uiRadio); | ||||
| Vue.component('ui-select', uiSelect); | ||||
|   | ||||
| @@ -44,10 +44,10 @@ export default Vue.component('mk-note-html', { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		while ( | ||||
| 		while (ast[ast.length - 1] && ( | ||||
| 			ast[ast.length - 1].type == 'hashtag' || | ||||
| 			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') || | ||||
| 			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n')) { | ||||
| 			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) { | ||||
| 			ast.pop(); | ||||
| 		} | ||||
|  | ||||
| @@ -103,7 +103,7 @@ export default Vue.component('mk-note-html', { | ||||
| 				case 'hashtag': | ||||
| 					return createElement('a', { | ||||
| 						attrs: { | ||||
| 							href: `${url}/tags/${token.content}`, | ||||
| 							href: `${url}/tags/${token.hashtag}`, | ||||
| 							target: '_blank' | ||||
| 						} | ||||
| 					}, token.content); | ||||
|   | ||||
| @@ -1,60 +1,58 @@ | ||||
| <template> | ||||
| <form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off"> | ||||
| 	<label class="username"> | ||||
| 		<p class="caption">%fa:at%%i18n:@username%</p> | ||||
| 		<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/> | ||||
| 		<p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/@${username}` }}</p> | ||||
| 		<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:@checking%</p> | ||||
| 		<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:@available%</p> | ||||
| 		<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@unavailable%</p> | ||||
| 		<p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@error%</p> | ||||
| 		<p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@invalid-format%</p> | ||||
| 		<p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-short%</p> | ||||
| 		<p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-long%</p> | ||||
| 	</label> | ||||
| 	<label class="password"> | ||||
| 		<p class="caption">%fa:lock%%i18n:@password%</p> | ||||
| 		<input v-model="password" type="password" placeholder="%i18n:@password-placeholder%" autocomplete="off" required @input="onChangePassword"/> | ||||
| 		<div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> | ||||
| 			<div class="value" ref="passwordMetar"></div> | ||||
| <form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()"> | ||||
| 	<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" required @input="onChangeUsername"> | ||||
| 		<span>%i18n:@username%</span> | ||||
| 		<span slot="prefix">@</span> | ||||
| 		<span slot="suffix">@{{ host }}</span> | ||||
| 		<p slot="text" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw% %i18n:@checking%</p> | ||||
| 		<p slot="text" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw% %i18n:@available%</p> | ||||
| 		<p slot="text" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@unavailable%</p> | ||||
| 		<p slot="text" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@error%</p> | ||||
| 		<p slot="text" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@invalid-format%</p> | ||||
| 		<p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-short%</p> | ||||
| 		<p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-long%</p> | ||||
| 	</ui-input> | ||||
| 	<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true"> | ||||
| 		<span>%i18n:@password%</span> | ||||
| 		<span slot="prefix">%fa:lock%</span> | ||||
| 		<div slot="text"> | ||||
| 			<p slot="text" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@weak-password%</p> | ||||
| 			<p slot="text" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw% %i18n:@normal-password%</p> | ||||
| 			<p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p> | ||||
| 		</div> | ||||
| 		<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@weak-password%</p> | ||||
| 		<p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:@normal-password%</p> | ||||
| 		<p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:@strong-password%</p> | ||||
| 	</label> | ||||
| 	<label class="retype-password"> | ||||
| 		<p class="caption">%fa:lock%%i18n:@password%(%i18n:@retype%)</p> | ||||
| 		<input v-model="retypedPassword" type="password" placeholder="%i18n:@retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/> | ||||
| 		<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:@password-matched%</p> | ||||
| 		<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@password-not-matched%</p> | ||||
| 	</label> | ||||
| 	<label class="recaptcha"> | ||||
| 		<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:@recaptcha%</p> | ||||
| 		<div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div> | ||||
| 	</label> | ||||
| 	<label class="agree-tou"> | ||||
| 		<input name="agree-tou" type="checkbox" autocomplete="off" required/> | ||||
| 	</ui-input> | ||||
| 	<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype"> | ||||
| 		<span>%i18n:@password% (%i18n:@retype%)</span> | ||||
| 		<span slot="prefix">%fa:lock%</span> | ||||
| 		<div slot="text"> | ||||
| 			<p slot="text" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw% %i18n:@password-matched%</p> | ||||
| 			<p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p> | ||||
| 		</div> | ||||
| 	</ui-input> | ||||
| 	<div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div> | ||||
| 	<label class="agree-tou" style="display: block; margin: 16px 0;"> | ||||
| 		<input name="agree-tou" type="checkbox" required/> | ||||
| 		<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p> | ||||
| 	</label> | ||||
| 	<button type="submit">%i18n:@create%</button> | ||||
| 	<ui-button type="submit">%i18n:@create%</ui-button> | ||||
| </form> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| const getPasswordStrength = require('syuilo-password-strength'); | ||||
| import { url, docsUrl, lang, recaptchaSitekey } from '../../../config'; | ||||
| import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			host, | ||||
| 			username: '', | ||||
| 			password: '', | ||||
| 			retypedPassword: '', | ||||
| 			url, | ||||
| 			touUrl: `${docsUrl}/${lang}/tou`, | ||||
| 			recaptchaSitekey, | ||||
| 			recaptchaed: false, | ||||
| 			usernameState: null, | ||||
| 			passwordStrength: '', | ||||
| 			passwordRetypeState: null | ||||
| @@ -104,7 +102,6 @@ export default Vue.extend({ | ||||
|  | ||||
| 			const strength = getPasswordStrength(this.password); | ||||
| 			this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; | ||||
| 			(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`; | ||||
| 		}, | ||||
| 		onChangePasswordRetype() { | ||||
| 			if (this.retypedPassword == '') { | ||||
| @@ -130,19 +127,9 @@ export default Vue.extend({ | ||||
| 				alert('%i18n:@some-error%'); | ||||
|  | ||||
| 				(window as any).grecaptcha.reset(); | ||||
| 				this.recaptchaed = false; | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 	created() { | ||||
| 		(window as any).onRecaptchaed = () => { | ||||
| 			this.recaptchaed = true; | ||||
| 		}; | ||||
|  | ||||
| 		(window as any).onRecaptchaExpired = () => { | ||||
| 			this.recaptchaed = false; | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		const head = document.getElementsByTagName('head')[0]; | ||||
| 		const script = document.createElement('script'); | ||||
| @@ -158,100 +145,6 @@ export default Vue.extend({ | ||||
| .mk-signup | ||||
| 	min-width 302px | ||||
|  | ||||
| 	label | ||||
| 		display block | ||||
| 		margin 0 0 16px 0 | ||||
|  | ||||
| 		> .caption | ||||
| 			margin 0 0 4px 0 | ||||
| 			color #828888 | ||||
| 			font-size 0.95em | ||||
|  | ||||
| 			> [data-fa] | ||||
| 				margin-right 0.25em | ||||
| 				color #96adac | ||||
|  | ||||
| 		> .info | ||||
| 			display block | ||||
| 			margin 4px 0 | ||||
| 			font-size 0.8em | ||||
|  | ||||
| 			> [data-fa] | ||||
| 				margin-right 0.3em | ||||
|  | ||||
| 		&.username | ||||
| 			.profile-page-url-preview | ||||
| 				display block | ||||
| 				margin 4px 8px 0 4px | ||||
| 				font-size 0.8em | ||||
| 				color #888 | ||||
|  | ||||
| 				&:empty | ||||
| 					display none | ||||
|  | ||||
| 				&:not(:empty) + .info | ||||
| 					margin-top 0 | ||||
|  | ||||
| 		&.password | ||||
| 			.meter | ||||
| 				display block | ||||
| 				margin-top 8px | ||||
| 				width 100% | ||||
| 				height 8px | ||||
|  | ||||
| 				&[data-strength=''] | ||||
| 					display none | ||||
|  | ||||
| 				&[data-strength='low'] | ||||
| 					> .value | ||||
| 						background #d73612 | ||||
|  | ||||
| 				&[data-strength='medium'] | ||||
| 					> .value | ||||
| 						background #d7ca12 | ||||
|  | ||||
| 				&[data-strength='high'] | ||||
| 					> .value | ||||
| 						background #61bb22 | ||||
|  | ||||
| 				> .value | ||||
| 					display block | ||||
| 					width 0% | ||||
| 					height 100% | ||||
| 					background transparent | ||||
| 					border-radius 4px | ||||
| 					transition all 0.1s ease | ||||
|  | ||||
| 	[type=text], [type=password] | ||||
| 		user-select text | ||||
| 		display inline-block | ||||
| 		cursor auto | ||||
| 		padding 0 12px | ||||
| 		margin 0 | ||||
| 		width 100% | ||||
| 		line-height 44px | ||||
| 		font-size 1em | ||||
| 		color #333 !important | ||||
| 		background #fff !important | ||||
| 		outline none | ||||
| 		border solid 1px rgba(#000, 0.1) | ||||
| 		border-radius 4px | ||||
| 		box-shadow 0 0 0 114514px #fff inset | ||||
| 		transition all .3s ease | ||||
|  | ||||
| 		&:hover | ||||
| 			border-color rgba(#000, 0.2) | ||||
| 			transition all .1s ease | ||||
|  | ||||
| 		&:focus | ||||
| 			color $theme-color !important | ||||
| 			border-color $theme-color | ||||
| 			box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%) | ||||
| 			transition all 0s ease | ||||
|  | ||||
| 		&:disabled | ||||
| 			opacity 0.5 | ||||
|  | ||||
| 	.agree-tou | ||||
| 		padding 4px | ||||
| 		border-radius 4px | ||||
| @@ -269,19 +162,4 @@ export default Vue.extend({ | ||||
| 			display inline | ||||
| 			color #555 | ||||
|  | ||||
| 	button | ||||
| 		margin 0 | ||||
| 		padding 16px | ||||
| 		width 100% | ||||
| 		font-size 1em | ||||
| 		color #fff | ||||
| 		background $theme-color | ||||
| 		border-radius 3px | ||||
|  | ||||
| 		&:hover | ||||
| 			background lighten($theme-color, 5%) | ||||
|  | ||||
| 		&:active | ||||
| 			background darken($theme-color, 5%) | ||||
|  | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										82
									
								
								src/client/app/common/views/components/ui/button.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/client/app/common/views/components/ui/button.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| <template> | ||||
| <div class="ui-button" :class="[styl]"> | ||||
| 	<button :type="type" @click="$emit('click')"> | ||||
| 		<slot></slot> | ||||
| 	</button> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		type: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			styl: 'fill' | ||||
| 		}; | ||||
| 	}, | ||||
| 	inject: { | ||||
| 		isCardChild: { default: false } | ||||
| 	}, | ||||
| 	created() { | ||||
| 		if (this.isCardChild) { | ||||
| 			this.styl = 'line'; | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark, fill) | ||||
| 	> button | ||||
| 		display block | ||||
| 		width 100% | ||||
| 		margin 0 | ||||
| 		padding 0 | ||||
| 		font-weight bold | ||||
| 		font-size 16px | ||||
| 		line-height 44px | ||||
| 		border none | ||||
| 		border-radius 6px | ||||
| 		outline none | ||||
| 		box-shadow none | ||||
|  | ||||
| 		if fill | ||||
| 			color $theme-color-foreground | ||||
| 			background $theme-color | ||||
|  | ||||
| 			&:hover | ||||
| 				background lighten($theme-color, 5%) | ||||
|  | ||||
| 			&:active | ||||
| 				background darken($theme-color, 5%) | ||||
| 		else | ||||
| 			color $theme-color | ||||
| 			background none | ||||
|  | ||||
| 			&:hover | ||||
| 				color darken($theme-color, 5%) | ||||
|  | ||||
| 			&:active | ||||
| 				background rgba($theme-color, 0.3) | ||||
|  | ||||
| .ui-button[data-darkmode] | ||||
| 	&.fill | ||||
| 		root(true, true) | ||||
| 	&:not(.fill) | ||||
| 		root(true, false) | ||||
|  | ||||
| .ui-button:not([data-darkmode]) | ||||
| 	&.fill | ||||
| 		root(false, true) | ||||
| 	&:not(.fill) | ||||
| 		root(false, false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										46
									
								
								src/client/app/common/views/components/ui/card.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/client/app/common/views/components/ui/card.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <template> | ||||
| <div class="ui-card"> | ||||
| 	<header> | ||||
| 		<slot name="title"></slot> | ||||
| 	</header> | ||||
|  | ||||
| 	<slot></slot> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| 	provide() { | ||||
| 		return { | ||||
| 			isCardChild: true | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark) | ||||
| 	margin 16px | ||||
| 	padding 16px | ||||
| 	color isDark ? #fff : #000 | ||||
| 	background isDark ? #282C37 : #fff | ||||
| 	box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) | ||||
|  | ||||
| 	@media (min-width 500px) | ||||
| 		padding 32px | ||||
|  | ||||
| 	> header | ||||
| 		font-weight normal | ||||
| 		font-size 24px | ||||
| 		color isDark ? #fff : #444 | ||||
|  | ||||
| .ui-card[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .ui-card:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										30
									
								
								src/client/app/common/views/components/ui/form.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/client/app/common/views/components/ui/form.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <template> | ||||
| <div class="ui-form"> | ||||
| 	<fieldset :disabled="disabled"> | ||||
| 		<slot></slot> | ||||
| 	</fieldset> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		disabled: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| .ui-form | ||||
| 	> fieldset | ||||
| 		margin 0 | ||||
| 		padding 0 | ||||
| 		border none | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										322
									
								
								src/client/app/common/views/components/ui/input.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										322
									
								
								src/client/app/common/views/components/ui/input.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,322 @@ | ||||
| <template> | ||||
| <div class="ui-input" :class="[{ focused, filled }, styl]"> | ||||
| 	<div class="icon" ref="icon"><slot name="icon"></slot></div> | ||||
| 	<div class="input" @click="focus" @mousedown="focus"> | ||||
| 		<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> | ||||
| 			<div class="value" ref="passwordMetar"></div> | ||||
| 		</div> | ||||
| 		<span class="label" ref="label"><slot></slot></span> | ||||
| 		<div class="prefix" ref="prefix"><slot name="prefix"></slot></div> | ||||
| 		<template v-if="type != 'file'"> | ||||
| 			<input ref="input" | ||||
| 					:type="type" | ||||
| 					:value="v" | ||||
| 					:required="required" | ||||
| 					:readonly="readonly" | ||||
| 					:pattern="pattern" | ||||
| 					:autocomplete="autocomplete" | ||||
| 					@input="$emit('input', $event.target.value)" | ||||
| 					@focus="focused = true" | ||||
| 					@blur="focused = false"> | ||||
| 		</template> | ||||
| 		<template v-else> | ||||
| 			<input ref="input" | ||||
| 					type="text" | ||||
| 					:value="placeholder" | ||||
| 					readonly | ||||
| 					@click="chooseFile"> | ||||
| 			<input ref="file" | ||||
| 					type="file" | ||||
| 					:value="value" | ||||
| 					@change="onChangeFile"> | ||||
| 		</template> | ||||
| 		<div class="suffix"><slot name="suffix"></slot></div> | ||||
| 	</div> | ||||
| 	<div class="text"><slot name="text"></slot></div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| const getPasswordStrength = require('syuilo-password-strength'); | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		value: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 		type: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		required: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		readonly: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		pattern: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		autocomplete: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 		withPasswordMeter: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			v: this.value, | ||||
| 			focused: false, | ||||
| 			passwordStrength: '', | ||||
| 			styl: 'fill' | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		filled(): boolean { | ||||
| 			return this.v != '' && this.v != null; | ||||
| 		}, | ||||
| 		placeholder(): string { | ||||
| 			if (this.type != 'file') return null; | ||||
| 			if (this.v == null) return null; | ||||
|  | ||||
| 			if (typeof this.v == 'string') return this.v; | ||||
|  | ||||
| 			if (Array.isArray(this.v)) { | ||||
| 				return this.v.map(file => file.name).join(', '); | ||||
| 			} else { | ||||
| 				return this.v.name; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		value(v) { | ||||
| 			this.v = v; | ||||
| 		}, | ||||
| 		v(v) { | ||||
| 			if (this.withPasswordMeter) { | ||||
| 				if (v == '') { | ||||
| 					this.passwordStrength = ''; | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				const strength = getPasswordStrength(v); | ||||
| 				this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; | ||||
| 				(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 	inject: { | ||||
| 		isCardChild: { default: false } | ||||
| 	}, | ||||
| 	created() { | ||||
| 		if (this.isCardChild) { | ||||
| 			this.styl = 'line'; | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		if (this.$refs.prefix) { | ||||
| 			this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		focus() { | ||||
| 			this.$refs.input.focus(); | ||||
| 		}, | ||||
| 		chooseFile() { | ||||
| 			this.$refs.file.click(); | ||||
| 		}, | ||||
| 		onChangeFile() { | ||||
| 			this.v = Array.from((this.$refs.file as any).files); | ||||
| 			this.$emit('input', this.v); | ||||
| 			this.$emit('change', this.v); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark, fill) | ||||
| 	margin 32px 0 | ||||
|  | ||||
| 	> .icon | ||||
| 		position absolute | ||||
| 		top 0 | ||||
| 		left 0 | ||||
| 		width 24px | ||||
| 		text-align center | ||||
| 		line-height 32px | ||||
| 		color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) | ||||
|  | ||||
| 		&:not(:empty) + .input | ||||
| 			margin-left 28px | ||||
|  | ||||
| 	> .input | ||||
| 		display flex | ||||
| 		cursor text | ||||
|  | ||||
| 		if fill | ||||
| 			padding 6px 12px | ||||
| 			background rgba(#000, 0.035) | ||||
| 			border-radius 6px | ||||
| 		else | ||||
| 			&:before | ||||
| 				content '' | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				bottom 0 | ||||
| 				left 0 | ||||
| 				right 0 | ||||
| 				height 1px | ||||
| 				background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42) | ||||
|  | ||||
| 			&:after | ||||
| 				content '' | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				bottom 0 | ||||
| 				left 0 | ||||
| 				right 0 | ||||
| 				height 2px | ||||
| 				background $theme-color | ||||
| 				opacity 0 | ||||
| 				transform scaleX(0.12) | ||||
| 				transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) | ||||
| 				will-change border opacity transform | ||||
|  | ||||
| 		> .password-meter | ||||
| 			position absolute | ||||
| 			top 0 | ||||
| 			left 0 | ||||
| 			width 100% | ||||
| 			height 100% | ||||
| 			border-radius 6px | ||||
| 			overflow hidden | ||||
| 			opacity 0.3 | ||||
|  | ||||
| 			&[data-strength=''] | ||||
| 				display none | ||||
|  | ||||
| 			&[data-strength='low'] | ||||
| 				> .value | ||||
| 					background #d73612 | ||||
|  | ||||
| 			&[data-strength='medium'] | ||||
| 				> .value | ||||
| 					background #d7ca12 | ||||
|  | ||||
| 			&[data-strength='high'] | ||||
| 				> .value | ||||
| 					background #61bb22 | ||||
|  | ||||
| 			> .value | ||||
| 				display block | ||||
| 				width 0% | ||||
| 				height 100% | ||||
| 				background transparent | ||||
| 				border-radius 6px | ||||
| 				transition all 0.1s ease | ||||
|  | ||||
| 		> .label | ||||
| 			position absolute | ||||
| 			top fill ? 6px : 0 | ||||
| 			left 0 | ||||
| 			pointer-events none | ||||
| 			transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) | ||||
| 			transition-duration 0.3s | ||||
| 			font-size 16px | ||||
| 			line-height 32px | ||||
| 			color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) | ||||
| 			pointer-events none | ||||
| 			//will-change transform | ||||
| 			transform-origin top left | ||||
| 			transform scale(1) | ||||
|  | ||||
| 		> input | ||||
| 			display block | ||||
| 			flex 1 | ||||
| 			width 100% | ||||
| 			margin 0 | ||||
| 			padding 0 | ||||
| 			font inherit | ||||
| 			font-weight fill ? bold : normal | ||||
| 			font-size 16px | ||||
| 			line-height 32px | ||||
| 			color isDark ? #fff : #000 | ||||
| 			background transparent | ||||
| 			border none | ||||
| 			border-radius 0 | ||||
| 			outline none | ||||
| 			box-shadow none | ||||
|  | ||||
| 			&[type='file'] | ||||
| 				display none | ||||
|  | ||||
| 		> .prefix | ||||
| 		> .suffix | ||||
| 			display block | ||||
| 			align-self center | ||||
| 			justify-self center | ||||
| 			font-size 16px | ||||
| 			line-height 32px | ||||
| 			color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) | ||||
| 			pointer-events none | ||||
|  | ||||
| 			> * | ||||
| 				display block | ||||
| 				min-width 16px | ||||
|  | ||||
| 		> .prefix | ||||
| 			padding-right 4px | ||||
|  | ||||
| 		> .suffix | ||||
| 			padding-left 4px | ||||
|  | ||||
| 	> .text | ||||
| 		margin 6px 0 | ||||
| 		font-size 13px | ||||
|  | ||||
| 		* | ||||
| 			margin 0 | ||||
|  | ||||
| 	&.focused | ||||
| 		> .input | ||||
| 			if fill | ||||
| 				background rgba(#000, 0.05) | ||||
| 			else | ||||
| 				&:after | ||||
| 					opacity 1 | ||||
| 					transform scaleX(1) | ||||
|  | ||||
| 			> .label | ||||
| 				color $theme-color | ||||
|  | ||||
| 	&.focused | ||||
| 	&.filled | ||||
| 		> .input | ||||
| 			> .label | ||||
| 				top fill ? -24px : -17px | ||||
| 				left 0 !important | ||||
| 				transform scale(0.75) | ||||
|  | ||||
| .ui-input[data-darkmode] | ||||
| 	&.fill | ||||
| 		root(true, true) | ||||
| 	&:not(.fill) | ||||
| 		root(true, false) | ||||
|  | ||||
| .ui-input:not([data-darkmode]) | ||||
| 	&.fill | ||||
| 		root(false, true) | ||||
| 	&:not(.fill) | ||||
| 		root(false, false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										120
									
								
								src/client/app/common/views/components/ui/radio.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/client/app/common/views/components/ui/radio.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| <template> | ||||
| <div | ||||
| 	class="ui-radio" | ||||
| 	:class="{ disabled, checked }" | ||||
| 	:aria-checked="checked" | ||||
| 	:aria-disabled="disabled" | ||||
| 	@click="toggle" | ||||
| > | ||||
| 	<input type="radio" | ||||
| 		:disabled="disabled" | ||||
| 	> | ||||
| 	<span class="button"> | ||||
| 		<span></span> | ||||
| 	</span> | ||||
| 	<span class="label"><slot></slot></span> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| 	model: { | ||||
| 		prop: 'model', | ||||
| 		event: 'change' | ||||
| 	}, | ||||
| 	props: { | ||||
| 		model: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		value: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		disabled: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		checked(): boolean { | ||||
| 			return this.model === this.value; | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		toggle() { | ||||
| 			this.$emit('change', this.value); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark) | ||||
| 	display inline-block | ||||
| 	margin 32px 32px 32px 0 | ||||
| 	cursor pointer | ||||
| 	transition all 0.3s | ||||
|  | ||||
| 	> * | ||||
| 		user-select none | ||||
|  | ||||
| 	&.disabled | ||||
| 		opacity 0.6 | ||||
| 		cursor not-allowed | ||||
|  | ||||
| 	&.checked | ||||
| 		> .button | ||||
| 			border-color $theme-color | ||||
|  | ||||
| 			&:after | ||||
| 				background-color $theme-color | ||||
| 				transform scale(1) | ||||
| 				opacity 1 | ||||
|  | ||||
| 	> input | ||||
| 		position absolute | ||||
| 		width 0 | ||||
| 		height 0 | ||||
| 		opacity 0 | ||||
| 		margin 0 | ||||
|  | ||||
| 	> .button | ||||
| 		position absolute | ||||
| 		width 20px | ||||
| 		height 20px | ||||
| 		background none | ||||
| 		border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) | ||||
| 		border-radius 100% | ||||
| 		transition inherit | ||||
|  | ||||
| 		&:after | ||||
| 			content '' | ||||
| 			display block | ||||
| 			position absolute | ||||
| 			top 3px | ||||
| 			right 3px | ||||
| 			bottom 3px | ||||
| 			left 3px | ||||
| 			border-radius 100% | ||||
| 			opacity 0 | ||||
| 			transform scale(0) | ||||
| 			transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) | ||||
|  | ||||
| 	> .label | ||||
| 		margin-left 28px | ||||
| 		display block | ||||
| 		font-size 16px | ||||
| 		line-height 20px | ||||
| 		cursor pointer | ||||
|  | ||||
| .ui-radio[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .ui-radio:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										215
									
								
								src/client/app/common/views/components/ui/select.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								src/client/app/common/views/components/ui/select.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | ||||
| <template> | ||||
| <div class="ui-select" :class="[{ focused, filled }, styl]"> | ||||
| 	<div class="icon" ref="icon"><slot name="icon"></slot></div> | ||||
| 	<div class="input" @click="focus"> | ||||
| 		<span class="label" ref="label"><slot name="label"></slot></span> | ||||
| 		<div class="prefix" ref="prefix"><slot name="prefix"></slot></div> | ||||
| 		<select ref="input" | ||||
| 				:value="v" | ||||
| 				:required="required" | ||||
| 				@input="$emit('input', $event.target.value)" | ||||
| 				@focus="focused = true" | ||||
| 				@blur="focused = false"> | ||||
| 			<slot></slot> | ||||
| 		</select> | ||||
| 		<div class="suffix"><slot name="suffix"></slot></div> | ||||
| 	</div> | ||||
| 	<div class="text"><slot name="text"></slot></div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		value: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 		required: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			v: this.value, | ||||
| 			focused: false, | ||||
| 			styl: 'fill' | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		filled(): boolean { | ||||
| 			return this.v != '' && this.v != null; | ||||
| 		} | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		value(v) { | ||||
| 			this.v = v; | ||||
| 		} | ||||
| 	}, | ||||
| 	inject: { | ||||
| 		isCardChild: { default: false } | ||||
| 	}, | ||||
| 	created() { | ||||
| 		if (this.isCardChild) { | ||||
| 			this.styl = 'line'; | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		if (this.$refs.prefix) { | ||||
| 			this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		focus() { | ||||
| 			this.$refs.input.focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark, fill) | ||||
| 	margin 32px 0 | ||||
|  | ||||
| 	> .icon | ||||
| 		position absolute | ||||
| 		top 0 | ||||
| 		left 0 | ||||
| 		width 24px | ||||
| 		text-align center | ||||
| 		line-height 32px | ||||
| 		color rgba(#000, 0.54) | ||||
|  | ||||
| 		&:not(:empty) + .input | ||||
| 			margin-left 28px | ||||
|  | ||||
| 	> .input | ||||
| 		display flex | ||||
|  | ||||
| 		if fill | ||||
| 			padding 6px 12px | ||||
| 			background rgba(#000, 0.035) | ||||
| 			border-radius 6px | ||||
| 		else | ||||
| 			&:before | ||||
| 				content '' | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				bottom 0 | ||||
| 				left 0 | ||||
| 				right 0 | ||||
| 				height 1px | ||||
| 				background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42) | ||||
|  | ||||
| 			&:after | ||||
| 				content '' | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				bottom 0 | ||||
| 				left 0 | ||||
| 				right 0 | ||||
| 				height 2px | ||||
| 				background $theme-color | ||||
| 				opacity 0 | ||||
| 				transform scaleX(0.12) | ||||
| 				transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) | ||||
| 				will-change border opacity transform | ||||
|  | ||||
| 		> .label | ||||
| 			position absolute | ||||
| 			top fill ? 6px : 0 | ||||
| 			left 0 | ||||
| 			pointer-events none | ||||
| 			transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) | ||||
| 			transition-duration 0.3s | ||||
| 			font-size 16px | ||||
| 			line-height 32px | ||||
| 			color rgba(#000, 0.54) | ||||
| 			pointer-events none | ||||
| 			//will-change transform | ||||
| 			transform-origin top left | ||||
| 			transform scale(1) | ||||
|  | ||||
| 		> select | ||||
| 			display block | ||||
| 			flex 1 | ||||
| 			width 100% | ||||
| 			padding 0 | ||||
| 			font inherit | ||||
| 			font-weight fill ? bold : normal | ||||
| 			font-size 16px | ||||
| 			height 32px | ||||
| 			color isDark ? #fff : #000 | ||||
| 			background transparent | ||||
| 			border none | ||||
| 			border-radius 0 | ||||
| 			outline none | ||||
| 			box-shadow none | ||||
|  | ||||
| 			* | ||||
| 				color #000 | ||||
|  | ||||
| 		> .prefix | ||||
| 		> .suffix | ||||
| 			display block | ||||
| 			align-self center | ||||
| 			justify-self center | ||||
| 			font-size 16px | ||||
| 			line-height 32px | ||||
| 			color rgba(#000, 0.54) | ||||
| 			pointer-events none | ||||
|  | ||||
| 			> * | ||||
| 				display block | ||||
| 				min-width 16px | ||||
|  | ||||
| 		> .prefix | ||||
| 			padding-right 4px | ||||
|  | ||||
| 		> .suffix | ||||
| 			padding-left 4px | ||||
|  | ||||
| 	> .text | ||||
| 		margin 6px 0 | ||||
| 		font-size 13px | ||||
|  | ||||
| 		* | ||||
| 			margin 0 | ||||
|  | ||||
| 	&.focused | ||||
| 		> .input | ||||
| 			if fill | ||||
| 				background rgba(#000, 0.05) | ||||
| 			else | ||||
| 				&:after | ||||
| 					opacity 1 | ||||
| 					transform scaleX(1) | ||||
|  | ||||
| 			> .label | ||||
| 				color $theme-color | ||||
|  | ||||
| 	&.focused | ||||
| 	&.filled | ||||
| 		> .input | ||||
| 			> .label | ||||
| 				top fill ? -24px : -17px | ||||
| 				left 0 !important | ||||
| 				transform scale(0.75) | ||||
|  | ||||
| .ui-select[data-darkmode] | ||||
| 	&.fill | ||||
| 		root(true, true) | ||||
| 	&:not(.fill) | ||||
| 		root(true, false) | ||||
|  | ||||
| .ui-select:not([data-darkmode]) | ||||
| 	&.fill | ||||
| 		root(false, true) | ||||
| 	&:not(.fill) | ||||
| 		root(false, false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										135
									
								
								src/client/app/common/views/components/ui/switch.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/client/app/common/views/components/ui/switch.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| <template> | ||||
| <div | ||||
| 	class="ui-switch" | ||||
| 	:class="{ disabled, checked }" | ||||
| 	role="switch" | ||||
| 	:aria-checked="checked" | ||||
| 	:aria-disabled="disabled" | ||||
| 	@click="toggle" | ||||
| > | ||||
| 	<input | ||||
| 		type="checkbox" | ||||
| 		ref="input" | ||||
| 		:disabled="disabled" | ||||
| 		@keydown.enter="toggle" | ||||
| 	> | ||||
| 	<span class="button"> | ||||
| 		<span></span> | ||||
| 	</span> | ||||
| 	<span class="label"> | ||||
| 		<span :aria-hidden="!checked"><slot></slot></span> | ||||
| 		<p :aria-hidden="!checked"> | ||||
| 			<slot name="text"></slot> | ||||
| 		</p> | ||||
| 	</span> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| 	model: { | ||||
| 		prop: 'value', | ||||
| 		event: 'change' | ||||
| 	}, | ||||
| 	props: { | ||||
| 		value: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		disabled: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		checked(): boolean { | ||||
| 			return this.value; | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		toggle() { | ||||
| 			this.$emit('change', !this.checked); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark) | ||||
| 	display flex | ||||
| 	margin 32px 0 | ||||
| 	cursor pointer | ||||
| 	transition all 0.3s | ||||
|  | ||||
| 	> * | ||||
| 		user-select none | ||||
|  | ||||
| 	&.disabled | ||||
| 		opacity 0.6 | ||||
| 		cursor not-allowed | ||||
|  | ||||
| 	&.checked | ||||
| 		> .button | ||||
| 			background-color rgba($theme-color, 0.4) | ||||
| 			border-color rgba($theme-color, 0.4) | ||||
|  | ||||
| 			> * | ||||
| 				background-color $theme-color | ||||
| 				transform translateX(14px) | ||||
|  | ||||
| 	> input | ||||
| 		position absolute | ||||
| 		width 0 | ||||
| 		height 0 | ||||
| 		opacity 0 | ||||
| 		margin 0 | ||||
|  | ||||
| 	> .button | ||||
| 		display inline-block | ||||
| 		margin 3px 0 0 0 | ||||
| 		width 34px | ||||
| 		height 14px | ||||
| 		background isDark ? rgba(#fff, 0.15) : rgba(#000, 0.25) | ||||
| 		outline none | ||||
| 		border-radius 14px | ||||
| 		transition inherit | ||||
|  | ||||
| 		> * | ||||
| 			position absolute | ||||
| 			top -3px | ||||
| 			left 0 | ||||
| 			border-radius 100% | ||||
| 			transition background-color 0.3s, transform 0.3s | ||||
| 			width 20px | ||||
| 			height 20px | ||||
| 			background-color #fff | ||||
| 			box-shadow 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12) | ||||
|  | ||||
| 	> .label | ||||
| 		margin-left 8px | ||||
| 		display block | ||||
| 		font-size 16px | ||||
| 		cursor pointer | ||||
| 		transition inherit | ||||
|  | ||||
| 		> span | ||||
| 			display block | ||||
| 			line-height 20px | ||||
| 			color isDark ? #c4ccd2 : rgba(#000, 0.75) | ||||
| 			transition inherit | ||||
|  | ||||
| 		> p | ||||
| 			margin 0 | ||||
| 			//font-size 90% | ||||
| 			color isDark ? #78858e : #9daab3 | ||||
|  | ||||
| .ui-switch[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .ui-switch:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										174
									
								
								src/client/app/common/views/components/ui/textarea.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								src/client/app/common/views/components/ui/textarea.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| <template> | ||||
| <div class="ui-textarea" :class="{ focused, filled }"> | ||||
| 	<div class="input"> | ||||
| 		<span class="label" ref="label"><slot></slot></span> | ||||
| 		<textarea ref="input" | ||||
| 				:value="value" | ||||
| 				:required="required" | ||||
| 				:readonly="readonly" | ||||
| 				:pattern="pattern" | ||||
| 				:autocomplete="autocomplete" | ||||
| 				@input="$emit('input', $event.target.value)" | ||||
| 				@focus="focused = true" | ||||
| 				@blur="focused = false"> | ||||
| 		</textarea> | ||||
| 	</div> | ||||
| 	<div class="text"><slot name="text"></slot></div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| const getPasswordStrength = require('syuilo-password-strength'); | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		value: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 		required: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		readonly: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		pattern: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		autocomplete: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			focused: false, | ||||
| 			passwordStrength: '' | ||||
| 		} | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		filled(): boolean { | ||||
| 			return this.value != '' && this.value != null; | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		focus() { | ||||
| 			this.$refs.input.focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| root(isDark, fill) | ||||
| 	margin 32px 0 | ||||
|  | ||||
| 	> .input | ||||
| 		padding 12px | ||||
|  | ||||
| 		if fill | ||||
| 			background rgba(#000, 0.035) | ||||
| 			border-radius 6px | ||||
| 		else | ||||
| 			&:before | ||||
| 				content '' | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				top 0 | ||||
| 				bottom 0 | ||||
| 				left 0 | ||||
| 				right 0 | ||||
| 				background none | ||||
| 				border solid 1px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42) | ||||
| 				border-radius 3px | ||||
| 				pointer-events none | ||||
|  | ||||
| 			&:after | ||||
| 				content '' | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				top 0 | ||||
| 				bottom 0 | ||||
| 				left 0 | ||||
| 				right 0 | ||||
| 				background none | ||||
| 				border solid 2px $theme-color | ||||
| 				border-radius 3px | ||||
| 				opacity 0 | ||||
| 				transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1) | ||||
| 				pointer-events none | ||||
|  | ||||
| 		> .label | ||||
| 			position absolute | ||||
| 			top 6px | ||||
| 			left 12px | ||||
| 			pointer-events none | ||||
| 			transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) | ||||
| 			transition-duration 0.3s | ||||
| 			font-size 16px | ||||
| 			line-height 32px | ||||
| 			color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) | ||||
| 			pointer-events none | ||||
| 			//will-change transform | ||||
| 			transform-origin top left | ||||
| 			transform scale(1) | ||||
|  | ||||
| 		> textarea | ||||
| 			display block | ||||
| 			width 100% | ||||
| 			min-height 100px | ||||
| 			padding 0 | ||||
| 			font inherit | ||||
| 			font-weight fill ? bold : normal | ||||
| 			font-size 16px | ||||
| 			color isDark ? #fff : #000 | ||||
| 			background transparent | ||||
| 			border none | ||||
| 			border-radius 0 | ||||
| 			outline none | ||||
| 			box-shadow none | ||||
|  | ||||
| 	> .text | ||||
| 		margin 6px 0 | ||||
| 		font-size 13px | ||||
|  | ||||
| 		* | ||||
| 			margin 0 | ||||
|  | ||||
| 	&.focused | ||||
| 		> .input | ||||
| 			if fill | ||||
| 				background rgba(#000, 0.05) | ||||
| 			else | ||||
| 				&:after | ||||
| 					opacity 1 | ||||
|  | ||||
| 			> .label | ||||
| 				color $theme-color | ||||
|  | ||||
| 	&.focused | ||||
| 	&.filled | ||||
| 		> .input | ||||
| 			> .label | ||||
| 				top -24px | ||||
| 				left 0 !important | ||||
| 				transform scale(0.75) | ||||
|  | ||||
| .ui-textarea[data-darkmode] | ||||
| 	&.fill | ||||
| 		root(true, true) | ||||
| 	&:not(.fill) | ||||
| 		root(true, false) | ||||
|  | ||||
| .ui-textarea:not([data-darkmode]) | ||||
| 	&.fill | ||||
| 		root(false, true) | ||||
| 	&:not(.fill) | ||||
| 		root(false, false) | ||||
|  | ||||
| </style> | ||||
| @@ -76,13 +76,8 @@ root(isDark) | ||||
| 				margin-right 4px | ||||
|  | ||||
| 		> div | ||||
| 			.chart-enter | ||||
| 			.chart-leave-to | ||||
| 				opacity 0 | ||||
| 				transform translateY(-30px) | ||||
|  | ||||
| 			> * | ||||
| 				transition transform .3s ease, opacity .3s ease | ||||
| 			.chart-move | ||||
| 				transition transform 1s ease | ||||
|  | ||||
| 			> div | ||||
| 				display flex | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| declare const _HOST_: string; | ||||
| declare const _HOSTNAME_: string; | ||||
| declare const _URL_: string; | ||||
| declare const _NAME_: string; | ||||
| declare const _DESCRIPTION_: string; | ||||
| declare const _API_URL_: string; | ||||
| declare const _WS_URL_: string; | ||||
| declare const _DOCS_URL_: string; | ||||
| @@ -21,6 +23,8 @@ declare const _GOOGLE_MAPS_API_KEY_: string; | ||||
| export const host = _HOST_; | ||||
| export const hostname = _HOSTNAME_; | ||||
| export const url = _URL_; | ||||
| export const name = _NAME_; | ||||
| export const description = _DESCRIPTION_; | ||||
| export const apiUrl = _API_URL_; | ||||
| export const wsUrl = _WS_URL_; | ||||
| export const docsUrl = _DOCS_URL_; | ||||
|   | ||||
| @@ -145,7 +145,7 @@ export default Vue.extend({ | ||||
| 				(this as any).api('drive/files/update', { | ||||
| 					fileId: this.file.id, | ||||
| 					name: name | ||||
| 				}) | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| @@ -173,7 +173,9 @@ export default Vue.extend({ | ||||
| 		}, | ||||
|  | ||||
| 		deleteFile() { | ||||
| 			alert('not implemented yet'); | ||||
| 			(this as any).api('drive/files/delete', { | ||||
| 				fileId: this.file.id | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -118,6 +118,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		this.connection.on('file_created', this.onStreamDriveFileCreated); | ||||
| 		this.connection.on('file_updated', this.onStreamDriveFileUpdated); | ||||
| 		this.connection.on('file_deleted', this.onStreamDriveFileDeleted); | ||||
| 		this.connection.on('folder_created', this.onStreamDriveFolderCreated); | ||||
| 		this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); | ||||
|  | ||||
| @@ -130,6 +131,7 @@ export default Vue.extend({ | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('file_created', this.onStreamDriveFileCreated); | ||||
| 		this.connection.off('file_updated', this.onStreamDriveFileUpdated); | ||||
| 		this.connection.off('file_deleted', this.onStreamDriveFileDeleted); | ||||
| 		this.connection.off('folder_created', this.onStreamDriveFolderCreated); | ||||
| 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); | ||||
| 		(this as any).os.streams.driveStream.dispose(this.connectionId); | ||||
| @@ -167,6 +169,10 @@ export default Vue.extend({ | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		onStreamDriveFileDeleted(fileId) { | ||||
| 			this.removeFile(fileId); | ||||
| 		}, | ||||
|  | ||||
| 		onStreamDriveFolderCreated(folder) { | ||||
| 			this.addFolder(folder, true); | ||||
| 		}, | ||||
|   | ||||
| @@ -50,6 +50,7 @@ import * as XDraggable from 'vuedraggable'; | ||||
| import getKao from '../../../common/scripts/get-kao'; | ||||
| import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; | ||||
| import parse from '../../../../../text/parse'; | ||||
| import { host } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -129,6 +130,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 				// 自分は除外 | ||||
| 				if (this.$store.state.i.username == x.username && x.host == null) return; | ||||
| 				if (this.$store.state.i.username == x.username && x.host == host) return; | ||||
|  | ||||
| 				// 重複は除外 | ||||
| 				if (this.text.indexOf(`${mention} `) != -1) return; | ||||
|   | ||||
| @@ -2,17 +2,11 @@ | ||||
|  * Mobile Client | ||||
|  */ | ||||
|  | ||||
| import Vue from 'vue'; | ||||
| import VueRouter from 'vue-router'; | ||||
|  | ||||
| import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch, MdSubheader, MdDialog, MdDialogAlert, MdRadio } from 'vue-material/dist/components'; | ||||
| import 'vue-material/dist/vue-material.min.css'; | ||||
| import 'vue-material/dist/theme/default.css'; | ||||
|  | ||||
| // Style | ||||
| import './style.styl'; | ||||
| import '../../element.scss'; | ||||
| import '../../md.scss'; | ||||
|  | ||||
| import init from '../init'; | ||||
|  | ||||
| @@ -42,17 +36,7 @@ import MkUserLists from './views/pages/user-lists.vue'; | ||||
| import MkUserList from './views/pages/user-list.vue'; | ||||
| import MkSettings from './views/pages/settings.vue'; | ||||
| import MkOthello from './views/pages/othello.vue'; | ||||
|  | ||||
| Vue.use(MdCard); | ||||
| Vue.use(MdButton); | ||||
| Vue.use(MdField); | ||||
| Vue.use(MdMenu); | ||||
| Vue.use(MdList); | ||||
| Vue.use(MdSwitch); | ||||
| Vue.use(MdSubheader); | ||||
| Vue.use(MdDialog); | ||||
| Vue.use(MdDialogAlert); | ||||
| Vue.use(MdRadio); | ||||
| import MkTag from './views/pages/tag.vue'; | ||||
|  | ||||
| /** | ||||
|  * init | ||||
| @@ -88,6 +72,7 @@ init((launch) => { | ||||
| 			{ path: '/i/drive/file/:file', component: MkDrive }, | ||||
| 			{ path: '/selectdrive', component: MkSelectDrive }, | ||||
| 			{ path: '/search', component: MkSearch }, | ||||
| 			{ path: '/tags/:tag', component: MkTag }, | ||||
| 			{ path: '/othello', name: 'othello', component: MkOthello }, | ||||
| 			{ path: '/othello/:game', component: MkOthello }, | ||||
| 			{ path: '/@:user', component: MkUser }, | ||||
|   | ||||
| @@ -10,9 +10,6 @@ html | ||||
| 	height 100% | ||||
| 	background #ececed !important | ||||
|  | ||||
| 	// for md | ||||
| 	transition none !important | ||||
|  | ||||
| 	&[data-darkmode] | ||||
| 		background #191B22 !important | ||||
|  | ||||
|   | ||||
| @@ -34,15 +34,10 @@ | ||||
| 	</div> | ||||
| 	<div class="menu"> | ||||
| 		<div> | ||||
| 			<a :href="`${file.url}?download`" :download="file.name"> | ||||
| 				%fa:download%%i18n:@download% | ||||
| 			</a> | ||||
| 			<button @click="rename"> | ||||
| 				%fa:pencil-alt%%i18n:@rename% | ||||
| 			</button> | ||||
| 			<button @click="move"> | ||||
| 				%fa:R folder-open%%i18n:@move% | ||||
| 			</button> | ||||
| 			<a :href="`${file.url}?download`" :download="file.name">%fa:download%%i18n:@download%</a> | ||||
| 			<button @click="rename">%fa:pencil-alt%%i18n:@rename%</button> | ||||
| 			<button @click="move">%fa:R folder-open%%i18n:@move%</button> | ||||
| 			<button @click="del">%fa:trash-alt R%%i18n:@delete%</button> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="exif" v-show="exif"> | ||||
| @@ -112,6 +107,13 @@ export default Vue.extend({ | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 		del() { | ||||
| 			(this as any).api('drive/files/delete', { | ||||
| 				fileId: this.file.id | ||||
| 			}).then(() => { | ||||
| 				this.browser.cd(this.file.folderId, true); | ||||
| 			}); | ||||
| 		}, | ||||
| 		showCreatedAt() { | ||||
| 			alert(new Date(this.file.createdAt).toLocaleString()); | ||||
| 		}, | ||||
|   | ||||
| @@ -100,6 +100,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		this.connection.on('file_created', this.onStreamDriveFileCreated); | ||||
| 		this.connection.on('file_updated', this.onStreamDriveFileUpdated); | ||||
| 		this.connection.on('file_deleted', this.onStreamDriveFileDeleted); | ||||
| 		this.connection.on('folder_created', this.onStreamDriveFolderCreated); | ||||
| 		this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); | ||||
|  | ||||
| @@ -118,6 +119,7 @@ export default Vue.extend({ | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('file_created', this.onStreamDriveFileCreated); | ||||
| 		this.connection.off('file_updated', this.onStreamDriveFileUpdated); | ||||
| 		this.connection.off('file_deleted', this.onStreamDriveFileDeleted); | ||||
| 		this.connection.off('folder_created', this.onStreamDriveFolderCreated); | ||||
| 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); | ||||
| 		(this as any).os.streams.driveStream.dispose(this.connectionId); | ||||
| @@ -136,6 +138,10 @@ export default Vue.extend({ | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		onStreamDriveFileDeleted(fileId) { | ||||
| 			this.removeFile(fileId); | ||||
| 		}, | ||||
|  | ||||
| 		onStreamDriveFolderCreated(folder) { | ||||
| 			this.addFolder(folder, true); | ||||
| 		}, | ||||
|   | ||||
| @@ -46,6 +46,7 @@ import * as XDraggable from 'vuedraggable'; | ||||
| import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; | ||||
| import getKao from '../../../common/scripts/get-kao'; | ||||
| import parse from '../../../../../text/parse'; | ||||
| import { host } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -123,6 +124,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 				// 自分は除外 | ||||
| 				if (this.$store.state.i.username == x.username && x.host == null) return; | ||||
| 				if (this.$store.state.i.username == x.username && x.host == host) return; | ||||
|  | ||||
| 				// 重複は除外 | ||||
| 				if (this.text.indexOf(`${mention} `) != -1) return; | ||||
|   | ||||
| @@ -1,132 +1,84 @@ | ||||
| <template> | ||||
| <mk-ui> | ||||
| 	<span slot="header">%fa:cog%%i18n:@settings%</span> | ||||
| 	<main> | ||||
| 		<p v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p> | ||||
| 	<main :data-darkmode="$store.state.device.darkmode"> | ||||
| 		<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></div> | ||||
|  | ||||
| 		<div> | ||||
| 			<x-profile/> | ||||
|  | ||||
| 			<md-card> | ||||
| 				<md-card-header> | ||||
| 					<div class="md-title">%fa:palette% %i18n:@design%</div> | ||||
| 				</md-card-header> | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:palette% %i18n:@design%</div> | ||||
|  | ||||
| 				<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch> | ||||
| 				<ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch> | ||||
|  | ||||
| 				<md-card-content> | ||||
| 				<div> | ||||
| 						<md-switch v-model="darkmode">%i18n:@dark-mode%</md-switch> | ||||
| 					<div>%i18n:@timeline%</div> | ||||
| 					<ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch> | ||||
| 					<ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch> | ||||
| 					<ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch> | ||||
| 				</div> | ||||
|  | ||||
| 				<div> | ||||
| 						<md-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</md-switch> | ||||
| 					<div>%i18n:@post-style%</div> | ||||
| 					<ui-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</ui-radio> | ||||
| 					<ui-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</ui-radio> | ||||
| 				</div> | ||||
| 			</ui-card> | ||||
|  | ||||
| 					<div> | ||||
| 						<div class="md-body-2">%i18n:@timeline%</div> | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:cog% %i18n:@behavior%</div> | ||||
| 				<ui-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch> | ||||
| 				<ui-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</ui-switch> | ||||
| 				<ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch> | ||||
| 				<ui-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</ui-switch> | ||||
| 				<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch> | ||||
| 			</ui-card> | ||||
|  | ||||
| 						<div> | ||||
| 							<md-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</md-switch> | ||||
| 						</div> | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:language% %i18n:@lang%</div> | ||||
|  | ||||
| 						<div> | ||||
| 							<md-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</md-switch> | ||||
| 						</div> | ||||
| 				<ui-select v-model="lang" placeholder="%i18n:@auto%"> | ||||
| 					<optgroup label="%i18n:@recommended%"> | ||||
| 						<option value="">%i18n:@auto%</option> | ||||
| 					</optgroup> | ||||
|  | ||||
| 						<div> | ||||
| 							<md-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</md-switch> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<optgroup label="%i18n:@specify-language%"> | ||||
| 						<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> | ||||
| 					</optgroup> | ||||
| 				</ui-select> | ||||
| 				<span>%fa:info-circle% %i18n:@lang-tip%</span> | ||||
| 			</ui-card> | ||||
|  | ||||
| 					<div> | ||||
| 						<div class="md-body-2">%i18n:@post-style%</div> | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:B twitter% %i18n:@twitter%</div> | ||||
|  | ||||
| 						<md-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</md-radio> | ||||
| 						<md-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</md-radio> | ||||
| 					</div> | ||||
| 				</md-card-content> | ||||
| 			</md-card> | ||||
|  | ||||
| 			<md-card> | ||||
| 				<md-card-header> | ||||
| 					<div class="md-title">%fa:cog% %i18n:@behavior%</div> | ||||
| 				</md-card-header> | ||||
|  | ||||
| 				<md-card-content> | ||||
| 					<div> | ||||
| 						<md-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</md-switch> | ||||
| 					</div> | ||||
|  | ||||
| 					<div> | ||||
| 						<md-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch> | ||||
| 					</div> | ||||
|  | ||||
| 					<div> | ||||
| 						<md-switch v-model="loadRawImages">%i18n:@load-raw-images%</md-switch> | ||||
| 					</div> | ||||
|  | ||||
| 					<div> | ||||
| 						<md-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</md-switch> | ||||
| 					</div> | ||||
|  | ||||
| 					<div> | ||||
| 						<md-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</md-switch> | ||||
| 					</div> | ||||
| 				</md-card-content> | ||||
| 			</md-card> | ||||
|  | ||||
| 			<md-card> | ||||
| 				<md-card-header> | ||||
| 					<div class="md-title">%fa:language% %i18n:@lang%</div> | ||||
| 				</md-card-header> | ||||
|  | ||||
| 				<md-card-content> | ||||
| 					<md-field> | ||||
| 						<md-select v-model="lang" placeholder="%i18n:@auto%"> | ||||
| 							<md-optgroup label="%i18n:@recommended%"> | ||||
| 								<md-option value="">%i18n:@auto%</md-option> | ||||
| 							</md-optgroup> | ||||
|  | ||||
| 							<md-optgroup label="%i18n:@specify-language%"> | ||||
| 								<md-option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</md-option> | ||||
| 							</md-optgroup> | ||||
| 						</md-select> | ||||
| 					</md-field> | ||||
| 					<span class="md-helper-text">%fa:info-circle% %i18n:@lang-tip%</span> | ||||
| 				</md-card-content> | ||||
| 			</md-card> | ||||
|  | ||||
| 			<md-card> | ||||
| 				<md-card-header> | ||||
| 					<div class="md-title">%fa:B twitter% %i18n:@twitter%</div> | ||||
| 				</md-card-header> | ||||
|  | ||||
| 				<md-card-content> | ||||
| 				<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p> | ||||
| 				<p> | ||||
| 					<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a> | ||||
| 					<span v-if="$store.state.i.twitter"> or </span> | ||||
| 					<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a> | ||||
| 				</p> | ||||
| 				</md-card-content> | ||||
| 			</md-card> | ||||
| 			</ui-card> | ||||
|  | ||||
| 			<md-card> | ||||
| 				<md-card-header> | ||||
| 					<div class="md-title">%fa:sync-alt% %i18n:@update%</div> | ||||
| 				</md-card-header> | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:sync-alt% %i18n:@update%</div> | ||||
|  | ||||
| 				<md-card-content> | ||||
| 				<div>%i18n:@version% <i>{{ version }}</i></div> | ||||
| 				<template v-if="latestVersion !== undefined"> | ||||
| 					<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div> | ||||
| 				</template> | ||||
| 					<md-button class="md-raised md-primary" @click="checkForUpdate" :disabled="checkingForUpdate"> | ||||
| 				<ui-button @click="checkForUpdate" :disabled="checkingForUpdate"> | ||||
| 					<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> | ||||
| 					<template v-else>%i18n:@check-for-updates%</template> | ||||
| 					</md-button> | ||||
| 				</md-card-content> | ||||
| 			</md-card> | ||||
| 				</ui-button> | ||||
| 			</ui-card> | ||||
| 		</div> | ||||
| 		<p><small>ver {{ version }} ({{ codename }})</small></p> | ||||
|  | ||||
| 		<footer> | ||||
| 			<small>ver {{ version }} ({{ codename }})</small> | ||||
| 		</footer> | ||||
| 	</main> | ||||
| </mk-ui> | ||||
| </template> | ||||
| @@ -267,20 +219,22 @@ export default Vue.extend({ | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
| 	padding 0 16px | ||||
| 	margin 0 auto | ||||
| 	max-width 500px | ||||
| 	width 100% | ||||
|  | ||||
| 	> div | ||||
| 		> * | ||||
| 			margin-bottom 16px | ||||
|  | ||||
| 	> p | ||||
| 		display block | ||||
| 		margin 24px | ||||
| 	> .signin-as | ||||
| 		margin 16px | ||||
| 		padding 16px | ||||
| 		text-align center | ||||
| 		color isDark ? #cad2da : #a2a9b1 | ||||
| 		color isDark ? #49ab63 : #2c662d | ||||
| 		background isDark ? #273c34 : #fcfff5 | ||||
| 		box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) | ||||
|  | ||||
| 	> footer | ||||
| 		margin 16px | ||||
| 		text-align center | ||||
| 		color isDark ? #c9d2e0 : #888 | ||||
|  | ||||
| main[data-darkmode] | ||||
| 	root(true) | ||||
|   | ||||
| @@ -1,62 +1,49 @@ | ||||
| <template> | ||||
| 	<md-card> | ||||
| 		<md-card-header> | ||||
| 			<div class="md-title">%fa:pencil-alt% %i18n:@title%</div> | ||||
| 		</md-card-header> | ||||
| <ui-card> | ||||
| 	<div slot="title">%fa:user% %i18n:@title%</div> | ||||
|  | ||||
| 		<md-card-content> | ||||
| 			<md-field> | ||||
| 				<label>%i18n:@name%</label> | ||||
| 				<md-input v-model="name" :disabled="saving" md-counter="30"/> | ||||
| 			</md-field> | ||||
| 	<ui-form :disabled="saving"> | ||||
| 		<ui-input v-model="name" :max="30"> | ||||
| 			<span>%i18n:@name%</span> | ||||
| 		</ui-input> | ||||
|  | ||||
| 			<md-field> | ||||
| 				<label>%i18n:@account%</label> | ||||
| 				<span class="md-prefix">@</span> | ||||
| 				<md-input v-model="username" readonly></md-input> | ||||
| 				<span class="md-suffix">@{{ host }}</span> | ||||
| 			</md-field> | ||||
| 		<ui-input v-model="username" readonly> | ||||
| 			<span>%i18n:@account%</span> | ||||
| 			<span slot="prefix">@</span> | ||||
| 			<span slot="suffix">@{{ host }}</span> | ||||
| 		</ui-input> | ||||
|  | ||||
| 			<md-field> | ||||
| 				<md-icon>%fa:map-marker-alt%</md-icon> | ||||
| 				<label>%i18n:@location%</label> | ||||
| 				<md-input v-model="location" :disabled="saving"/> | ||||
| 			</md-field> | ||||
| 		<ui-input v-model="location"> | ||||
| 			<span>%i18n:@location%</span> | ||||
| 			<span slot="prefix">%fa:map-marker-alt%</span> | ||||
| 		</ui-input> | ||||
|  | ||||
| 			<md-field> | ||||
| 				<md-icon>%fa:birthday-cake%</md-icon> | ||||
| 				<label>%i18n:@birthday%</label> | ||||
| 				<md-input type="date" v-model="birthday" :disabled="saving"/> | ||||
| 			</md-field> | ||||
| 		<ui-input v-model="birthday" type="date"> | ||||
| 			<span>%i18n:@birthday%</span> | ||||
| 			<span slot="prefix">%fa:birthday-cake%</span> | ||||
| 		</ui-input> | ||||
|  | ||||
| 			<md-field> | ||||
| 				<label>%i18n:@description%</label> | ||||
| 				<md-textarea v-model="description" :disabled="saving" md-counter="500"/> | ||||
| 			</md-field> | ||||
| 		<ui-textarea v-model="description" :max="500"> | ||||
| 			<span>%i18n:@description%</span> | ||||
| 		</ui-textarea> | ||||
|  | ||||
| 			<md-field> | ||||
| 				<label>%i18n:@avatar%</label> | ||||
| 				<md-file @md-change="onAvatarChange"/> | ||||
| 			</md-field> | ||||
| 		<ui-input type="file" @change="onAvatarChange"> | ||||
| 			<span>%i18n:@avatar%</span> | ||||
| 			<span slot="icon">%fa:image%</span> | ||||
| 			<span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span> | ||||
| 		</ui-input> | ||||
|  | ||||
| 			<md-field> | ||||
| 				<label>%i18n:@banner%</label> | ||||
| 				<md-file @md-change="onBannerChange"/> | ||||
| 			</md-field> | ||||
| 		<ui-input type="file" @change="onBannerChange"> | ||||
| 			<span>%i18n:@banner%</span> | ||||
| 			<span slot="icon">%fa:image%</span> | ||||
| 			<span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span> | ||||
| 		</ui-input> | ||||
|  | ||||
| 			<md-dialog-alert | ||||
| 					:md-active.sync="uploading" | ||||
| 					md-content="%18n:!@uploading%"/> | ||||
| 		<ui-switch v-model="isCat">%i18n:@is-cat%</ui-switch> | ||||
|  | ||||
| 			<div> | ||||
| 				<md-switch v-model="isCat">%i18n:@is-cat%</md-switch> | ||||
| 			</div> | ||||
| 		</md-card-content> | ||||
|  | ||||
| 		<md-card-actions> | ||||
| 			<md-button class="md-primary" :disabled="saving" @click="save">%i18n:@save%</md-button> | ||||
| 		</md-card-actions> | ||||
| 	</md-card> | ||||
| 		<ui-button @click="save">%i18n:@save%</ui-button> | ||||
| 	</ui-form> | ||||
| </ui-card> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| @@ -77,7 +64,8 @@ export default Vue.extend({ | ||||
| 			isBot: false, | ||||
| 			isCat: false, | ||||
| 			saving: false, | ||||
| 			uploading: false | ||||
| 			avatarUploading: false, | ||||
| 			bannerUploading: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| @@ -95,7 +83,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	methods: { | ||||
| 		onAvatarChange([file]) { | ||||
| 			this.uploading = true; | ||||
| 			this.avatarUploading = true; | ||||
|  | ||||
| 			const data = new FormData(); | ||||
| 			data.append('file', file); | ||||
| @@ -108,16 +96,16 @@ export default Vue.extend({ | ||||
| 			.then(response => response.json()) | ||||
| 			.then(f => { | ||||
| 				this.avatarId = f.id; | ||||
| 				this.uploading = false; | ||||
| 				this.avatarUploading = false; | ||||
| 			}) | ||||
| 			.catch(e => { | ||||
| 				this.uploading = false; | ||||
| 				this.avatarUploading = false; | ||||
| 				alert('%18n:!@upload-failed%'); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onBannerChange([file]) { | ||||
| 			this.uploading = true; | ||||
| 			this.bannerUploading = true; | ||||
|  | ||||
| 			const data = new FormData(); | ||||
| 			data.append('file', file); | ||||
| @@ -130,10 +118,10 @@ export default Vue.extend({ | ||||
| 			.then(response => response.json()) | ||||
| 			.then(f => { | ||||
| 				this.bannerId = f.id; | ||||
| 				this.uploading = false; | ||||
| 				this.bannerUploading = false; | ||||
| 			}) | ||||
| 			.catch(e => { | ||||
| 				this.uploading = false; | ||||
| 				this.bannerUploading = false; | ||||
| 				alert('%18n:!@upload-failed%'); | ||||
| 			}); | ||||
| 		}, | ||||
|   | ||||
| @@ -1,57 +1,26 @@ | ||||
| <template> | ||||
| <div class="signup"> | ||||
| 	<h1>Misskeyをはじめる</h1> | ||||
| 	<p>いつでも、どこからでもMisskeyを利用できます。もちろん、無料です。</p> | ||||
| 	<div class="form"> | ||||
| 		<p>新規登録</p> | ||||
| 		<div> | ||||
| 	<mk-signup/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| 	mounted() { | ||||
| 		document.documentElement.style.background = '#293946'; | ||||
| 	} | ||||
| }); | ||||
| export default Vue.extend({}); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .signup | ||||
| 	padding 16px | ||||
| 	padding 32px | ||||
| 	margin 0 auto | ||||
| 	max-width 500px | ||||
|  | ||||
| 	h1 | ||||
| 		margin 0 | ||||
| 		padding 8px | ||||
| 		padding 8px 0 0 0 | ||||
| 		font-size 1.5em | ||||
| 		font-weight normal | ||||
| 		color #c3c6ca | ||||
|  | ||||
| 		& + p | ||||
| 			margin 0 0 16px 0 | ||||
| 			padding 0 8px 0 8px | ||||
| 			color #949fa9 | ||||
|  | ||||
| 	.form | ||||
| 		background #fff | ||||
| 		border solid 1px rgba(#000, 0.2) | ||||
| 		border-radius 8px | ||||
| 		overflow hidden | ||||
|  | ||||
| 		> p | ||||
| 			margin 0 | ||||
| 			padding 12px 20px | ||||
| 			color #555 | ||||
| 			background #f5f5f5 | ||||
| 			border-bottom solid 1px #ddd | ||||
|  | ||||
| 		> div | ||||
| 			padding 16px | ||||
| 		font-weight bold | ||||
| 		color #444 | ||||
|  | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										81
									
								
								src/client/app/mobile/views/pages/tag.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/client/app/mobile/views/pages/tag.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| <template> | ||||
| <mk-ui> | ||||
| 	<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span> | ||||
|  | ||||
| 	<main> | ||||
| 		<p v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p> | ||||
| 		<mk-notes ref="timeline" :more="existMore ? more : null"/> | ||||
| 	</main> | ||||
| </mk-ui> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import Progress from '../../../common/scripts/loading'; | ||||
|  | ||||
| const limit = 20; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			offset: 0, | ||||
| 			empty: false | ||||
| 		}; | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		$route: 'fetch' | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.$nextTick(() => { | ||||
| 			this.fetch(); | ||||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 			Progress.start(); | ||||
|  | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api('notes/search_by_tag', { | ||||
| 					limit: limit + 1, | ||||
| 					offset: this.offset, | ||||
| 					tag: this.$route.params.tag | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == 0) this.empty = true; | ||||
| 					if (notes.length == limit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
| 					} | ||||
| 					res(notes); | ||||
| 					this.fetching = false; | ||||
| 					Progress.done(); | ||||
| 				}, rej); | ||||
| 			})); | ||||
| 		}, | ||||
| 		more() { | ||||
| 			this.offset += limit; | ||||
|  | ||||
| 			const promise = (this as any).api('notes/search_by_tag', { | ||||
| 				limit: limit + 1, | ||||
| 				offset: this.offset, | ||||
| 				tag: this.$route.params.tag | ||||
| 			}); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == limit + 1) { | ||||
| 					notes.pop(); | ||||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				notes.forEach(n => (this.$refs.timeline as any).append(n)); | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
|  | ||||
| 			return promise; | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| @@ -1,29 +1,31 @@ | ||||
| <template> | ||||
| <div class="welcome"> | ||||
| 	<div> | ||||
| 		<h1><b>Misskey</b>へようこそ</h1> | ||||
| 		<p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p> | ||||
| 		<div class="form"> | ||||
| 			<p>%fa:lock% ログイン</p> | ||||
| 			<div> | ||||
| 		<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey"> | ||||
| 		<p class="host">{{ host }}</p> | ||||
| 		<div class="about"> | ||||
| 			<h2>{{ name || 'unidentified' }}</h2> | ||||
| 			<p v-html="description || '%i18n:common.about%'"></p> | ||||
| 			<router-link class="signup" to="/signup">新規登録</router-link> | ||||
| 		</div> | ||||
| 		<div class="login"> | ||||
| 			<form @submit.prevent="onSubmit"> | ||||
| 					<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/> | ||||
| 					<input v-model="password" type="password" placeholder="パスワード" required/> | ||||
| 					<input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/> | ||||
| 					<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button> | ||||
| 				<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" autofocus required @change="onUsernameChange"> | ||||
| 					<span>ユーザー名</span> | ||||
| 					<span slot="prefix">@</span> | ||||
| 					<span slot="suffix">@{{ host }}</span> | ||||
| 				</ui-input> | ||||
| 				<ui-input v-model="password" type="password" required> | ||||
| 					<span>パスワード</span> | ||||
| 					<span slot="prefix">%fa:lock%</span> | ||||
| 				</ui-input> | ||||
| 				<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/> | ||||
| 				<ui-button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</ui-button> | ||||
| 			</form> | ||||
| 			<div> | ||||
| 				<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		</div> | ||||
| 		<div class="tl"> | ||||
| 			<p>%fa:comments R% タイムラインを見てみる</p> | ||||
| 			<mk-welcome-timeline/> | ||||
| 		</div> | ||||
| 		<div class="users"> | ||||
| 			<mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/> | ||||
| 		</div> | ||||
| 		<footer> | ||||
| 			<small>{{ copyright }}</small> | ||||
| 		</footer> | ||||
| @@ -33,7 +35,7 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { apiUrl, copyright } from '../../../config'; | ||||
| import { apiUrl, copyright, host, name, description } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| @@ -45,7 +47,10 @@ export default Vue.extend({ | ||||
| 			token: '', | ||||
| 			apiUrl, | ||||
| 			copyright, | ||||
| 			users: [] | ||||
| 			users: [], | ||||
| 			host, | ||||
| 			name, | ||||
| 			description | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| @@ -84,56 +89,49 @@ export default Vue.extend({ | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .welcome | ||||
| 	background linear-gradient(to bottom, #1e1d65, #bd6659) | ||||
| 	text-align center | ||||
| 	//background #fff | ||||
|  | ||||
| 	> div | ||||
| 		padding 16px | ||||
| 		padding 32px | ||||
| 		margin 0 auto | ||||
| 		max-width 500px | ||||
|  | ||||
| 		h1 | ||||
| 			margin 0 | ||||
| 			padding 8px | ||||
| 			font-size 1.5em | ||||
| 			font-weight normal | ||||
| 			color #cacac3 | ||||
| 		> img | ||||
| 			display block | ||||
| 			max-width 200px | ||||
| 			margin 0 auto | ||||
|  | ||||
| 			& + p | ||||
| 				margin 0 0 16px 0 | ||||
| 				padding 0 8px 0 8px | ||||
| 				color #949fa9 | ||||
| 		> .host | ||||
| 			display block | ||||
| 			text-align center | ||||
| 			padding 6px 12px | ||||
| 			line-height 32px | ||||
| 			font-weight bold | ||||
| 			color #333 | ||||
| 			background rgba(#000, 0.035) | ||||
| 			border-radius 6px | ||||
|  | ||||
| 		.form | ||||
| 			margin-bottom 16px | ||||
| 		> .about | ||||
| 			margin-top 16px | ||||
| 			padding 16px | ||||
| 			color #555 | ||||
| 			background #fff | ||||
| 			border solid 1px rgba(#000, 0.2) | ||||
| 			border-radius 8px | ||||
| 			overflow hidden | ||||
| 			border-radius 6px | ||||
|  | ||||
| 			> h2 | ||||
| 				margin 0 | ||||
|  | ||||
| 			> p | ||||
| 				margin 0 | ||||
| 				padding 12px 20px | ||||
| 				color #555 | ||||
| 				background #f5f5f5 | ||||
| 				border-bottom solid 1px #ddd | ||||
| 				margin 8px | ||||
|  | ||||
| 			> div | ||||
| 			> .signup | ||||
| 				font-weight bold | ||||
|  | ||||
| 		> .login | ||||
| 			margin 16px 0 | ||||
|  | ||||
| 			> form | ||||
| 					padding 16px | ||||
| 					border-bottom solid 1px #ddd | ||||
|  | ||||
| 					input | ||||
| 						display block | ||||
| 						padding 12px | ||||
| 						margin 0 0 16px 0 | ||||
| 						width 100% | ||||
| 						font-size 1em | ||||
| 						color rgba(#000, 0.7) | ||||
| 						background #fff | ||||
| 						outline none | ||||
| 						border solid 1px #ddd | ||||
| 						border-radius 4px | ||||
|  | ||||
| 				button | ||||
| 					display block | ||||
| @@ -156,40 +154,9 @@ export default Vue.extend({ | ||||
| 						border-color #444 | ||||
| 						box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2) | ||||
|  | ||||
| 				> div | ||||
| 					padding 16px | ||||
| 					text-align center | ||||
|  | ||||
| 		> .tl | ||||
| 			background #fff | ||||
| 			border solid 1px rgba(#000, 0.2) | ||||
| 			border-radius 8px | ||||
| 			overflow hidden | ||||
|  | ||||
| 			> p | ||||
| 				margin 0 | ||||
| 				padding 12px 20px | ||||
| 				color #555 | ||||
| 				background #f5f5f5 | ||||
| 				border-bottom solid 1px #ddd | ||||
|  | ||||
| 			> .mk-welcome-timeline | ||||
| 				max-height 300px | ||||
| 				overflow auto | ||||
|  | ||||
| 		> .users | ||||
| 			margin 12px 0 0 0 | ||||
|  | ||||
| 			> * | ||||
| 				display inline-block | ||||
| 				margin 4px | ||||
| 				width 38px | ||||
| 				height 38px | ||||
| 				border-radius 6px | ||||
|  | ||||
| 		> footer | ||||
| 			text-align center | ||||
| 			color #fff | ||||
| 			color #444 | ||||
|  | ||||
| 			> small | ||||
| 				display block | ||||
|   | ||||
| @@ -56,7 +56,7 @@ export default define({ | ||||
| 	left 92px | ||||
| 	margin 0 | ||||
| 	line-height 100px | ||||
| 	color #fff !important // !important is for md | ||||
| 	color #fff | ||||
| 	font-weight bold | ||||
| 	text-shadow 0 0 8px rgba(#000, 0.5) | ||||
|  | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| /* SEE: https://vuematerial.io/themes/configuration */ | ||||
|  | ||||
| @import '../const.json'; | ||||
|  | ||||
| @import "~vue-material/dist/theme/engine"; | ||||
|  | ||||
| @include md-register-theme("default", ( | ||||
| 	primary: $themeColor, | ||||
| 	accent: $themeColor | ||||
| )); | ||||
|  | ||||
| @import "~vue-material/dist/components/MdButton/theme"; | ||||
| @import "~vue-material/dist/components/MdField/theme"; | ||||
| @@ -15,6 +15,8 @@ export type Source = { | ||||
| 		 */ | ||||
| 		url: string; | ||||
| 	}; | ||||
| 	name?: string; | ||||
| 	description?: string; | ||||
| 	url: string; | ||||
| 	port: number; | ||||
| 	https?: { [x: string]: string }; | ||||
|   | ||||
| @@ -48,6 +48,11 @@ export type INote = { | ||||
| 	repliesCount: number; | ||||
| 	reactionCounts: any; | ||||
| 	mentions: mongo.ObjectID[]; | ||||
| 	mentionedRemoteUsers: Array<{ | ||||
| 		uri: string; | ||||
| 		username: string; | ||||
| 		host: string; | ||||
| 	}>; | ||||
|  | ||||
| 	/** | ||||
| 	 * public ... 公開 | ||||
| @@ -289,7 +294,7 @@ export const pack = async ( | ||||
|  | ||||
| 		// Poll | ||||
| 		if (meId && _note.poll && !hide) { | ||||
| 			_note.poll = (async (poll) => { | ||||
| 			_note.poll = (async poll => { | ||||
| 				const vote = await PollVote | ||||
| 					.findOne({ | ||||
| 						userId: meId, | ||||
|   | ||||
| @@ -15,6 +15,11 @@ const log = debug('misskey:activitypub'); | ||||
| export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> { | ||||
| 	const uri = activity.id || activity; | ||||
|  | ||||
| 	// アナウンサーが凍結されていたらスキップ | ||||
| 	if (actor.isSuspended) { | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	if (typeof uri !== 'string') { | ||||
| 		throw new Error('invalid announce'); | ||||
| 	} | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import config from '../../../config'; | ||||
|  | ||||
| export default tag => ({ | ||||
| export default (tag: string) => ({ | ||||
| 	type: 'Hashtag', | ||||
| 	href: `${config.url}/tags/${encodeURIComponent(tag)}`, | ||||
| 	name: '#' + tag | ||||
|   | ||||
							
								
								
									
										9
									
								
								src/remote/activitypub/renderer/mention.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/remote/activitypub/renderer/mention.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| export default (mention: { | ||||
| 	uri: string; | ||||
| 	username: string; | ||||
| 	host: string; | ||||
| }) => ({ | ||||
| 	type: 'Mention', | ||||
| 	href: mention.uri, | ||||
| 	name: `@${mention.username}@${mention.host}` | ||||
| }); | ||||
| @@ -1,5 +1,6 @@ | ||||
| import renderDocument from './document'; | ||||
| import renderHashtag from './hashtag'; | ||||
| import renderMention from './mention'; | ||||
| import config from '../../../config'; | ||||
| import DriveFile from '../../../models/drive-file'; | ||||
| import Note, { INote } from '../../../models/note'; | ||||
| @@ -45,6 +46,18 @@ export default async function renderNote(note: INote, dive = true) { | ||||
|  | ||||
| 	const attributedTo = `${config.url}/users/${user._id}`; | ||||
|  | ||||
| 	const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0 | ||||
| 		? note.mentionedRemoteUsers.map(x => x.uri) | ||||
| 		: []; | ||||
|  | ||||
| 	const cc = ['public', 'home', 'followers'].includes(note.visibility) | ||||
| 		? [`${attributedTo}/followers`].concat(mentions) | ||||
| 		: []; | ||||
|  | ||||
| 	const hashtagTags = (note.tags || []).map(renderHashtag); | ||||
| 	const mentionTags = (note.mentionedRemoteUsers || []).map(renderMention); | ||||
| 	const tag = hashtagTags.concat(mentionTags) | ||||
|  | ||||
| 	return { | ||||
| 		id: `${config.url}/notes/${note._id}`, | ||||
| 		type: 'Note', | ||||
| @@ -52,9 +65,9 @@ export default async function renderNote(note: INote, dive = true) { | ||||
| 		content: toHtml(note), | ||||
| 		published: note.createdAt.toISOString(), | ||||
| 		to: 'https://www.w3.org/ns/activitystreams#Public', | ||||
| 		cc: `${attributedTo}/followers`, | ||||
| 		cc, | ||||
| 		inReplyTo, | ||||
| 		attachment: (await promisedFiles).map(renderDocument), | ||||
| 		tag: (note.tags || []).map(renderHashtag) | ||||
| 		tag | ||||
| 	}; | ||||
| } | ||||
|   | ||||
| @@ -1,34 +1,32 @@ | ||||
| /** | ||||
|  * Module dependencies | ||||
|  */ | ||||
| import DriveFile from '../../../models/drive-file'; | ||||
|  | ||||
| /** | ||||
|  * Get drive information | ||||
|  * | ||||
|  * @param {any} params | ||||
|  * @param {any} user | ||||
|  * @return {Promise<any>} | ||||
|  */ | ||||
| module.exports = (params, user) => new Promise(async (res, rej) => { | ||||
| 	// Calculate drive usage | ||||
| 	const usage = ((await DriveFile | ||||
| 		.aggregate([ | ||||
| 			{ $match: { 'metadata.userId': user._id } }, | ||||
| 			{ | ||||
| 	const usage = await DriveFile | ||||
| 		.aggregate([{ | ||||
| 			$match: { | ||||
| 				'metadata.userId': user._id, | ||||
| 				'metadata.deletedAt': { $exists: false } | ||||
| 			} | ||||
| 		}, { | ||||
| 			$project: { | ||||
| 				length: true | ||||
| 			} | ||||
| 			}, | ||||
| 			{ | ||||
| 		}, { | ||||
| 			$group: { | ||||
| 				_id: null, | ||||
| 				usage: { $sum: '$length' } | ||||
| 			} | ||||
| 		}]) | ||||
| 		.then((aggregates: any[]) => { | ||||
| 			if (aggregates.length > 0) { | ||||
| 				return aggregates[0].usage; | ||||
| 			} | ||||
| 		]))[0] || { | ||||
| 			usage: 0 | ||||
| 		}).usage; | ||||
| 			return 0; | ||||
| 		}); | ||||
|  | ||||
| 	res({ | ||||
| 		capacity: user.driveCapacity, | ||||
|   | ||||
| @@ -37,10 +37,13 @@ module.exports = async (params, user, app) => { | ||||
| 	const sort = { | ||||
| 		_id: -1 | ||||
| 	}; | ||||
|  | ||||
| 	const query = { | ||||
| 		'metadata.userId': user._id, | ||||
| 		'metadata.folderId': folderId | ||||
| 		'metadata.folderId': folderId, | ||||
| 		'metadata.deletedAt': { $exists: false } | ||||
| 	} as any; | ||||
|  | ||||
| 	if (sinceId) { | ||||
| 		sort._id = 1; | ||||
| 		query._id = { | ||||
| @@ -51,6 +54,7 @@ module.exports = async (params, user, app) => { | ||||
| 			$lt: untilId | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	if (type) { | ||||
| 		query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/server/api/endpoints/drive/files/delete.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/server/api/endpoints/drive/files/delete.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import $ from 'cafy'; import ID from '../../../../../cafy-id'; | ||||
| import DriveFile from '../../../../../models/drive-file'; | ||||
| import del from '../../../../../services/drive/delete-file'; | ||||
| import { publishDriveStream } from '../../../../../publishers/stream'; | ||||
|  | ||||
| /** | ||||
|  * Delete a file | ||||
|  */ | ||||
| module.exports = async (params, user) => { | ||||
| 	// Get 'fileId' parameter | ||||
| 	const [fileId, fileIdErr] = $.type(ID).get(params.fileId); | ||||
| 	if (fileIdErr) throw 'invalid fileId param'; | ||||
|  | ||||
| 	// Fetch file | ||||
| 	const file = await DriveFile | ||||
| 		.findOne({ | ||||
| 			_id: fileId, | ||||
| 			'metadata.userId': user._id | ||||
| 		}); | ||||
|  | ||||
| 	if (file === null) { | ||||
| 		throw 'file-not-found'; | ||||
| 	} | ||||
|  | ||||
| 	// Delete | ||||
| 	await del(file); | ||||
|  | ||||
| 	// Publish file_deleted event | ||||
| 	publishDriveStream(user._id, 'file_deleted', file._id); | ||||
|  | ||||
| 	return; | ||||
| }; | ||||
| @@ -4,7 +4,7 @@ import * as debug from 'debug'; | ||||
|  | ||||
| import User, { IUser } from '../../../models/user'; | ||||
| import Mute from '../../../models/mute'; | ||||
| import { pack as packNote } from '../../../models/note'; | ||||
| import { pack as packNote, pack } from '../../../models/note'; | ||||
| import readNotification from '../common/read-notification'; | ||||
| import call from '../call'; | ||||
| import { IApp } from '../../../models/app'; | ||||
| @@ -48,6 +48,14 @@ export default async function( | ||||
| 					} | ||||
| 					//#endregion | ||||
|  | ||||
| 					// Renoteなら再pack | ||||
| 					if (x.type == 'note' && x.body.renoteId != null) { | ||||
| 						x.body.renote = await pack(x.body.renoteId, user, { | ||||
| 							detail: true | ||||
| 						}); | ||||
| 						data = JSON.stringify(x); | ||||
| 					} | ||||
|  | ||||
| 					connection.send(data); | ||||
| 				} catch (e) { | ||||
| 					connection.send(data); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import * as redis from 'redis'; | ||||
|  | ||||
| import { IUser } from '../../../models/user'; | ||||
| import Mute from '../../../models/mute'; | ||||
| import { pack } from '../../../models/note'; | ||||
|  | ||||
| export default async function( | ||||
| 	request: websocket.request, | ||||
| @@ -31,6 +32,13 @@ export default async function( | ||||
| 		} | ||||
| 		//#endregion | ||||
|  | ||||
| 		// Renoteなら再pack | ||||
| 		if (note.renoteId != null) { | ||||
| 			note.renote = await pack(note.renoteId, user, { | ||||
| 				detail: true | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		connection.send(JSON.stringify({ | ||||
| 			type: 'note', | ||||
| 			body: note | ||||
|   | ||||
| @@ -9,13 +9,14 @@ import * as debug from 'debug'; | ||||
| import fileType = require('file-type'); | ||||
| import prominence = require('prominence'); | ||||
|  | ||||
| import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile, DriveFileChunk } from '../../models/drive-file'; | ||||
| import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file'; | ||||
| import DriveFolder from '../../models/drive-folder'; | ||||
| import { pack } from '../../models/drive-file'; | ||||
| import event, { publishDriveStream } from '../../publishers/stream'; | ||||
| import { isLocalUser, IUser, IRemoteUser } from '../../models/user'; | ||||
| import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; | ||||
| import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; | ||||
| import genThumbnail from '../../drive/gen-thumbnail'; | ||||
| import delFile from './delete-file'; | ||||
|  | ||||
| const gm = _gm.subClass({ | ||||
| 	imageMagick: true | ||||
| @@ -58,31 +59,7 @@ async function deleteOldFile(user: IRemoteUser) { | ||||
| 	}); | ||||
|  | ||||
| 	if (oldFile) { | ||||
| 		// チャンクをすべて削除 | ||||
| 		DriveFileChunk.remove({ | ||||
| 			files_id: oldFile._id | ||||
| 		}); | ||||
|  | ||||
| 		DriveFile.update({ _id: oldFile._id }, { | ||||
| 			$set: { | ||||
| 				'metadata.deletedAt': new Date(), | ||||
| 				'metadata.isExpired': true | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		//#region サムネイルもあれば削除 | ||||
| 		const thumbnail = await DriveFileThumbnail.findOne({ | ||||
| 			'metadata.originalId': oldFile._id | ||||
| 		}); | ||||
|  | ||||
| 		if (thumbnail) { | ||||
| 			DriveFileThumbnailChunk.remove({ | ||||
| 				files_id: thumbnail._id | ||||
| 			}); | ||||
|  | ||||
| 			DriveFileThumbnail.remove({ _id: thumbnail._id }); | ||||
| 		} | ||||
| 		//#endregion | ||||
| 		delFile(oldFile, true); | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										30
									
								
								src/services/drive/delete-file.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/services/drive/delete-file.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import DriveFile, { DriveFileChunk, IDriveFile } from "../../models/drive-file"; | ||||
| import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; | ||||
|  | ||||
| export default async function(file: IDriveFile, isExpired = false) { | ||||
| 	// チャンクをすべて削除 | ||||
| 	await DriveFileChunk.remove({ | ||||
| 		files_id: file._id | ||||
| 	}); | ||||
|  | ||||
| 	await DriveFile.update({ _id: file._id }, { | ||||
| 		$set: { | ||||
| 			'metadata.deletedAt': new Date(), | ||||
| 			'metadata.isExpired': isExpired | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	//#region サムネイルもあれば削除 | ||||
| 	const thumbnail = await DriveFileThumbnail.findOne({ | ||||
| 		'metadata.originalId': file._id | ||||
| 	}); | ||||
|  | ||||
| 	if (thumbnail) { | ||||
| 		await DriveFileThumbnailChunk.remove({ | ||||
| 			files_id: thumbnail._id | ||||
| 		}); | ||||
|  | ||||
| 		await DriveFileThumbnail.remove({ _id: thumbnail._id }); | ||||
| 	} | ||||
| 	//#endregion | ||||
| } | ||||
| @@ -204,6 +204,62 @@ export default async (user: IUser, data: { | ||||
| 		return packAp(content); | ||||
| 	}; | ||||
|  | ||||
| 	//#region メンション | ||||
| 	if (data.text) { | ||||
| 		// TODO: Drop dupulicates | ||||
| 		const mentionTokens = tokens | ||||
| 			.filter(t => t.type == 'mention'); | ||||
|  | ||||
| 		// TODO: Drop dupulicates | ||||
| 		const mentionedUsers = (await Promise.all(mentionTokens.map(async m => { | ||||
| 			try { | ||||
| 				return await resolveUser(m.username, m.host); | ||||
| 			} catch (e) { | ||||
| 				return null; | ||||
| 			} | ||||
| 		}))).filter(x => x != null); | ||||
|  | ||||
| 		// Append mentions data | ||||
| 		if (mentionedUsers.length > 0) { | ||||
| 			const set = { | ||||
| 				mentions: mentionedUsers.map(u => u._id), | ||||
| 				mentionedRemoteUsers: mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({ | ||||
| 					uri: (u as IRemoteUser).uri, | ||||
| 					username: u.username, | ||||
| 					host: u.host | ||||
| 				})) | ||||
| 			}; | ||||
|  | ||||
| 			Note.update({ _id: note._id }, { | ||||
| 				$set: set | ||||
| 			}); | ||||
|  | ||||
| 			Object.assign(note, set); | ||||
| 		} | ||||
|  | ||||
| 		mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => { | ||||
| 			event(u, 'mention', noteObj); | ||||
|  | ||||
| 			// 既に言及されたユーザーに対する返信や引用renoteの場合も無視 | ||||
| 			if (data.reply && data.reply.userId.equals(u._id)) return; | ||||
| 			if (data.renote && data.renote.userId.equals(u._id)) return; | ||||
|  | ||||
| 			// Create notification | ||||
| 			notify(u._id, user._id, 'mention', { | ||||
| 				noteId: note._id | ||||
| 			}); | ||||
|  | ||||
| 			nm.push(u._id, 'mention'); | ||||
| 		}); | ||||
|  | ||||
| 		if (isLocalUser(user)) { | ||||
| 			mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => { | ||||
| 				deliver(user, await render(), (u as IRemoteUser).inbox); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 	//#endregion | ||||
|  | ||||
| 	if (!silent) { | ||||
| 		if (isLocalUser(user)) { | ||||
| 			if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') { | ||||
| @@ -287,55 +343,6 @@ export default async (user: IUser, data: { | ||||
| 	} | ||||
| 	//#endergion | ||||
|  | ||||
| 	//#region メンション | ||||
| 	if (data.text) { | ||||
| 		// TODO: Drop dupulicates | ||||
| 		const mentions = tokens | ||||
| 			.filter(t => t.type == 'mention'); | ||||
|  | ||||
| 		let mentionedUsers = await Promise.all(mentions.map(async m => { | ||||
| 			try { | ||||
| 				return await resolveUser(m.username, m.host); | ||||
| 			} catch (e) { | ||||
| 				return null; | ||||
| 			} | ||||
| 		})); | ||||
|  | ||||
| 		// TODO: Drop dupulicates | ||||
| 		mentionedUsers = mentionedUsers.filter(x => x != null); | ||||
|  | ||||
| 		mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => { | ||||
| 			event(u, 'mention', noteObj); | ||||
|  | ||||
| 			// 既に言及されたユーザーに対する返信や引用renoteの場合も無視 | ||||
| 			if (data.reply && data.reply.userId.equals(u._id)) return; | ||||
| 			if (data.renote && data.renote.userId.equals(u._id)) return; | ||||
|  | ||||
| 			// Create notification | ||||
| 			notify(u._id, user._id, 'mention', { | ||||
| 				noteId: note._id | ||||
| 			}); | ||||
|  | ||||
| 			nm.push(u._id, 'mention'); | ||||
| 		}); | ||||
|  | ||||
| 		if (isLocalUser(user)) { | ||||
| 			mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => { | ||||
| 				deliver(user, await render(), (u as IRemoteUser).inbox); | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		// Append mentions data | ||||
| 		if (mentionedUsers.length > 0) { | ||||
| 			Note.update({ _id: note._id }, { | ||||
| 				$set: { | ||||
| 					mentions: mentionedUsers.map(u => u._id) | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 	//#endregion | ||||
|  | ||||
| 	// If has in reply to note | ||||
| 	if (data.reply) { | ||||
| 		// Increment replies count | ||||
|   | ||||
| @@ -20,6 +20,7 @@ export default async function(user: IUser, note: INote) { | ||||
| 		$set: { | ||||
| 			deletedAt: new Date(), | ||||
| 			text: null, | ||||
| 			tags: [], | ||||
| 			mediaIds: [], | ||||
| 			poll: null | ||||
| 		} | ||||
|   | ||||
| @@ -79,6 +79,8 @@ const consts = { | ||||
| 	_DEV_URL_: config.dev_url, | ||||
| 	_LANG_: '%lang%', | ||||
| 	_LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]), | ||||
| 	_NAME_: config.name, | ||||
| 	_DESCRIPTION_: config.description, | ||||
| 	_HOST_: config.host, | ||||
| 	_HOSTNAME_: config.hostname, | ||||
| 	_URL_: config.url, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user