Merge branch 'develop' into math-block
This commit is contained in:
		
						commit
						1af1638e2b
					
				
					 48 changed files with 791 additions and 852 deletions
				
			
		
							
								
								
									
										82
									
								
								src/client/app/admin/views/users.user.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/client/app/admin/views/users.user.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,82 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="kofvwchc">
 | 
			
		||||
	<div>
 | 
			
		||||
		<a :href="user | userPage(null, true)">
 | 
			
		||||
			<mk-avatar class="avatar" :user="user" :disable-link="true"/>
 | 
			
		||||
		</a>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div>
 | 
			
		||||
		<header>
 | 
			
		||||
			<b><mk-user-name :user="user"/></b>
 | 
			
		||||
			<span class="username">@{{ user | acct }}</span>
 | 
			
		||||
			<span class="is-admin" v-if="user.isAdmin">admin</span>
 | 
			
		||||
			<span class="is-moderator" v-if="user.isModerator">moderator</span>
 | 
			
		||||
			<span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span>
 | 
			
		||||
			<span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span>
 | 
			
		||||
		</header>
 | 
			
		||||
		<div>
 | 
			
		||||
			<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div>
 | 
			
		||||
			<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../i18n';
 | 
			
		||||
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('admin/views/users.vue'),
 | 
			
		||||
	props: ['user'],
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			faSnowflake
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.kofvwchc
 | 
			
		||||
	display flex
 | 
			
		||||
	padding 16px 0
 | 
			
		||||
	border-top solid 1px var(--faceDivider)
 | 
			
		||||
 | 
			
		||||
	> div:first-child
 | 
			
		||||
		> a
 | 
			
		||||
			> .avatar
 | 
			
		||||
				width 64px
 | 
			
		||||
				height 64px
 | 
			
		||||
 | 
			
		||||
	> div:last-child
 | 
			
		||||
		flex 1
 | 
			
		||||
		padding-left 16px
 | 
			
		||||
 | 
			
		||||
		@media (max-width 500px)
 | 
			
		||||
			font-size 14px
 | 
			
		||||
 | 
			
		||||
		> header
 | 
			
		||||
			> .username
 | 
			
		||||
				margin-left 8px
 | 
			
		||||
				opacity 0.7
 | 
			
		||||
 | 
			
		||||
			> .is-admin
 | 
			
		||||
			> .is-moderator
 | 
			
		||||
				flex-shrink 0
 | 
			
		||||
				align-self center
 | 
			
		||||
				margin 0 0 0 .5em
 | 
			
		||||
				padding 1px 6px
 | 
			
		||||
				font-size 80%
 | 
			
		||||
				border-radius 3px
 | 
			
		||||
				background var(--noteHeaderAdminBg)
 | 
			
		||||
				color var(--noteHeaderAdminFg)
 | 
			
		||||
 | 
			
		||||
			> .is-verified
 | 
			
		||||
			> .is-suspended
 | 
			
		||||
				margin 0 0 0 .5em
 | 
			
		||||
				color #4dabf7
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -3,20 +3,27 @@
 | 
			
		|||
	<ui-card>
 | 
			
		||||
		<div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div>
 | 
			
		||||
		<section class="fit-top">
 | 
			
		||||
			<ui-input v-model="target" type="text">
 | 
			
		||||
			<ui-input class="target" v-model="target" type="text">
 | 
			
		||||
				<span>{{ $t('username-or-userid') }}</span>
 | 
			
		||||
			</ui-input>
 | 
			
		||||
			<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
 | 
			
		||||
			<ui-horizon-group>
 | 
			
		||||
				<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
 | 
			
		||||
				<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
 | 
			
		||||
			</ui-horizon-group>
 | 
			
		||||
			<ui-horizon-group>
 | 
			
		||||
				<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
 | 
			
		||||
				<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
 | 
			
		||||
			</ui-horizon-group>
 | 
			
		||||
			<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
 | 
			
		||||
			<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
 | 
			
		||||
 | 
			
		||||
			<div class="user" v-if="user">
 | 
			
		||||
				<x-user :user='user'/>
 | 
			
		||||
				<div class="actions">
 | 
			
		||||
					<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
 | 
			
		||||
					<ui-horizon-group>
 | 
			
		||||
						<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
 | 
			
		||||
						<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
 | 
			
		||||
					</ui-horizon-group>
 | 
			
		||||
					<ui-horizon-group>
 | 
			
		||||
						<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
 | 
			
		||||
						<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
 | 
			
		||||
					</ui-horizon-group>
 | 
			
		||||
					<ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button>
 | 
			
		||||
					<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</section>
 | 
			
		||||
	</ui-card>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,29 +54,7 @@
 | 
			
		|||
				</ui-select>
 | 
			
		||||
			</ui-horizon-group>
 | 
			
		||||
			<sequential-entrance animation="entranceFromTop" delay="25">
 | 
			
		||||
				<div class="kofvwchc" v-for="user in users" :key="user.id">
 | 
			
		||||
					<div>
 | 
			
		||||
						<a :href="user | userPage(null, true)">
 | 
			
		||||
							<mk-avatar class="avatar" :user="user" :disable-link="true"/>
 | 
			
		||||
						</a>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div>
 | 
			
		||||
						<header>
 | 
			
		||||
							<b><mk-user-name :user="user"/></b>
 | 
			
		||||
							<span class="username">@{{ user | acct }}</span>
 | 
			
		||||
							<span class="is-admin" v-if="user.isAdmin">admin</span>
 | 
			
		||||
							<span class="is-moderator" v-if="user.isModerator">moderator</span>
 | 
			
		||||
							<span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span>
 | 
			
		||||
							<span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span>
 | 
			
		||||
						</header>
 | 
			
		||||
						<div>
 | 
			
		||||
							<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<x-user v-for="user in users" :user='user' :key="user.id"/>
 | 
			
		||||
			</sequential-entrance>
 | 
			
		||||
			<ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button>
 | 
			
		||||
		</section>
 | 
			
		||||
| 
						 | 
				
			
			@ -81,12 +66,15 @@
 | 
			
		|||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../i18n';
 | 
			
		||||
import parseAcct from "../../../../misc/acct/parse";
 | 
			
		||||
import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faCertificate, faUsers, faTerminal, faSearch, faKey, faSync } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import XUser from './users.user.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('admin/views/users.vue'),
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		XUser
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			user: null,
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +90,7 @@ export default Vue.extend({
 | 
			
		|||
			offset: 0,
 | 
			
		||||
			users: [],
 | 
			
		||||
			existMore: false,
 | 
			
		||||
			faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey
 | 
			
		||||
			faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey, faSync
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -131,6 +119,7 @@ export default Vue.extend({
 | 
			
		|||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		/** テキストエリアのユーザーを解決する */
 | 
			
		||||
		async fetchUser() {
 | 
			
		||||
			try {
 | 
			
		||||
				return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target });
 | 
			
		||||
| 
						 | 
				
			
			@ -149,16 +138,27 @@ export default Vue.extend({
 | 
			
		|||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		/** テキストエリアから処理対象ユーザーを設定する */
 | 
			
		||||
		async showUser() {
 | 
			
		||||
			this.user = null;
 | 
			
		||||
			const user = await this.fetchUser();
 | 
			
		||||
			this.$root.api('admin/show-user', { userId: user.id }).then(info => {
 | 
			
		||||
				this.user = info;
 | 
			
		||||
			});
 | 
			
		||||
			this.target = '';
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		/** 処理対象ユーザーの情報を更新する */
 | 
			
		||||
		async refreshUser() {
 | 
			
		||||
			this.$root.api('admin/show-user', { userId: this.user._id }).then(info => {
 | 
			
		||||
				this.user = info;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async resetPassword() {
 | 
			
		||||
			const user = await this.fetchUser();
 | 
			
		||||
			this.$root.api('admin/reset-password', { userId: user.id }).then(res => {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: this.$t('password-updated', { password: res.password })
 | 
			
		||||
| 
						 | 
				
			
			@ -167,11 +167,12 @@ export default Vue.extend({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		async verifyUser() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t('verify-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			this.verifying = true;
 | 
			
		||||
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				const user = await this.fetchUser();
 | 
			
		||||
				await this.$root.api('admin/verify-user', { userId: user.id });
 | 
			
		||||
				await this.$root.api('admin/verify-user', { userId: this.user._id });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: this.$t('verified')
 | 
			
		||||
| 
						 | 
				
			
			@ -186,14 +187,17 @@ export default Vue.extend({
 | 
			
		|||
			});
 | 
			
		||||
 | 
			
		||||
			this.verifying = false;
 | 
			
		||||
 | 
			
		||||
			this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async unverifyUser() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t('unverify-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			this.unverifying = true;
 | 
			
		||||
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				const user = await this.fetchUser();
 | 
			
		||||
				await this.$root.api('admin/unverify-user', { userId: user.id });
 | 
			
		||||
				await this.$root.api('admin/unverify-user', { userId: this.user._id });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: this.$t('unverified')
 | 
			
		||||
| 
						 | 
				
			
			@ -208,14 +212,17 @@ export default Vue.extend({
 | 
			
		|||
			});
 | 
			
		||||
 | 
			
		||||
			this.unverifying = false;
 | 
			
		||||
 | 
			
		||||
			this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async suspendUser() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t('suspend-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			this.suspending = true;
 | 
			
		||||
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				const user = await this.fetchUser();
 | 
			
		||||
				await this.$root.api('admin/suspend-user', { userId: user.id });
 | 
			
		||||
				await this.$root.api('admin/suspend-user', { userId: this.user._id });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: this.$t('suspended')
 | 
			
		||||
| 
						 | 
				
			
			@ -230,14 +237,17 @@ export default Vue.extend({
 | 
			
		|||
			});
 | 
			
		||||
 | 
			
		||||
			this.suspending = false;
 | 
			
		||||
 | 
			
		||||
			this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async unsuspendUser() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t('unsuspend-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			this.unsuspending = true;
 | 
			
		||||
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				const user = await this.fetchUser();
 | 
			
		||||
				await this.$root.api('admin/unsuspend-user', { userId: user.id });
 | 
			
		||||
				await this.$root.api('admin/unsuspend-user', { userId: this.user._id });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: this.$t('unsuspended')
 | 
			
		||||
| 
						 | 
				
			
			@ -252,8 +262,32 @@ export default Vue.extend({
 | 
			
		|||
			});
 | 
			
		||||
 | 
			
		||||
			this.unsuspending = false;
 | 
			
		||||
 | 
			
		||||
			this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async updateRemoteUser() {
 | 
			
		||||
			this.$root.api('admin/update-remote-user', { userId: this.user._id }).then(res => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: this.$t('remote-user-updated')
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async getConfirmed(text: string): Promise<Boolean> {
 | 
			
		||||
			const confirm = await this.$root.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				showCancelButton: true,
 | 
			
		||||
				title: 'confirm',
 | 
			
		||||
				text,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return !confirm.canceled;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fetchUsers() {
 | 
			
		||||
			this.$root.api('admin/show-users', {
 | 
			
		||||
				state: this.state,
 | 
			
		||||
| 
						 | 
				
			
			@ -277,42 +311,12 @@ export default Vue.extend({
 | 
			
		|||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.kofvwchc
 | 
			
		||||
	display flex
 | 
			
		||||
	padding 16px 0
 | 
			
		||||
	border-top solid 1px var(--faceDivider)
 | 
			
		||||
.target
 | 
			
		||||
	margin-bottom 16px !important
 | 
			
		||||
 | 
			
		||||
	> div:first-child
 | 
			
		||||
		> a
 | 
			
		||||
			> .avatar
 | 
			
		||||
				width 64px
 | 
			
		||||
				height 64px
 | 
			
		||||
.user
 | 
			
		||||
	margin-top 32px
 | 
			
		||||
 | 
			
		||||
	> div:last-child
 | 
			
		||||
		flex 1
 | 
			
		||||
		padding-left 16px
 | 
			
		||||
 | 
			
		||||
		@media (max-width 500px)
 | 
			
		||||
			font-size 14px
 | 
			
		||||
 | 
			
		||||
		> header
 | 
			
		||||
			> .username
 | 
			
		||||
				margin-left 8px
 | 
			
		||||
				opacity 0.7
 | 
			
		||||
 | 
			
		||||
			> .is-admin
 | 
			
		||||
			> .is-moderator
 | 
			
		||||
				flex-shrink 0
 | 
			
		||||
				align-self center
 | 
			
		||||
				margin 0 0 0 .5em
 | 
			
		||||
				padding 1px 6px
 | 
			
		||||
				font-size 80%
 | 
			
		||||
				border-radius 3px
 | 
			
		||||
				background var(--noteHeaderAdminBg)
 | 
			
		||||
				color var(--noteHeaderAdminFg)
 | 
			
		||||
 | 
			
		||||
			> .is-verified
 | 
			
		||||
			> .is-suspended
 | 
			
		||||
				margin 0 0 0 .5em
 | 
			
		||||
				color #4dabf7
 | 
			
		||||
	> .actions
 | 
			
		||||
		margin-left 80px
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,3 +26,8 @@
 | 
			
		|||
		transform: translateY(0);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
	0% { transform: rotate(0deg); }
 | 
			
		||||
	100% { transform: rotate(360deg); }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,47 +72,6 @@ body
 | 
			
		|||
code
 | 
			
		||||
	font-family Consolas, 'Courier New', Courier, Monaco, monospace
 | 
			
		||||
 | 
			
		||||
	.comment
 | 
			
		||||
		opacity 0.5
 | 
			
		||||
 | 
			
		||||
	.string
 | 
			
		||||
		color #e96900
 | 
			
		||||
 | 
			
		||||
	.regexp
 | 
			
		||||
		color #e9003f
 | 
			
		||||
 | 
			
		||||
	.keyword
 | 
			
		||||
		color #2973b7
 | 
			
		||||
 | 
			
		||||
		&.true
 | 
			
		||||
		&.false
 | 
			
		||||
		&.null
 | 
			
		||||
		&.nil
 | 
			
		||||
		&.undefined
 | 
			
		||||
			color #ae81ff
 | 
			
		||||
 | 
			
		||||
	.symbol
 | 
			
		||||
		color #42b983
 | 
			
		||||
 | 
			
		||||
	.number
 | 
			
		||||
	.nan
 | 
			
		||||
		color #ae81ff
 | 
			
		||||
 | 
			
		||||
	.var:not(.keyword)
 | 
			
		||||
		font-weight bold
 | 
			
		||||
		font-style italic
 | 
			
		||||
		//text-decoration underline
 | 
			
		||||
 | 
			
		||||
	.method
 | 
			
		||||
		font-style italic
 | 
			
		||||
		color #8964c1
 | 
			
		||||
 | 
			
		||||
	.property
 | 
			
		||||
		color #a71d5d
 | 
			
		||||
 | 
			
		||||
	.label
 | 
			
		||||
		color #e9003f
 | 
			
		||||
 | 
			
		||||
pre
 | 
			
		||||
	display block
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -133,6 +133,7 @@ export default prop => ({
 | 
			
		|||
 | 
			
		||||
				case 'deleted': {
 | 
			
		||||
					Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt);
 | 
			
		||||
					Vue.set(this.$_ns_target, 'renote', null);
 | 
			
		||||
					this.$_ns_target.text = null;
 | 
			
		||||
					this.$_ns_target.tags = [];
 | 
			
		||||
					this.$_ns_target.fileIds = [];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										30
									
								
								src/client/app/common/views/components/code-core.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/client/app/common/views/components/code-core.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
<template>
 | 
			
		||||
<prism :inline="inline" :language="lang">{{ code }}</prism>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import 'prismjs';
 | 
			
		||||
import 'prismjs/themes/prism.css';
 | 
			
		||||
import Prism from 'vue-prism-component';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		Prism
 | 
			
		||||
	},
 | 
			
		||||
	props: {
 | 
			
		||||
		code: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		lang: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		inline: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										28
									
								
								src/client/app/common/views/components/code.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/client/app/common/views/components/code.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
<template>
 | 
			
		||||
<x-code :code="code" :lang="lang" :inline="inline"/>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XCode: () => import('./code-core.vue').then(m => m.default)
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		code: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		lang: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		inline: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -5,16 +5,21 @@
 | 
			
		|||
		<span>{{ $t('click-to-show') }}</span>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else :href="image.url" target="_blank" :style="style" :title="image.name" @click.prevent="onClick"></a>
 | 
			
		||||
<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else
 | 
			
		||||
	:href="image.url"
 | 
			
		||||
	:style="style"
 | 
			
		||||
	:title="image.name"
 | 
			
		||||
	@click.prevent="onClick"
 | 
			
		||||
></a>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import ImageViewer from '../../../common/views/components/image-viewer.vue';
 | 
			
		||||
import ImageViewer from './image-viewer.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('mobile/views/components/media-image.vue'),
 | 
			
		||||
	i18n: i18n('common/views/components/media-image.vue'),
 | 
			
		||||
	props: {
 | 
			
		||||
		image: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +63,7 @@ export default Vue.extend({
 | 
			
		|||
<style lang="stylus" scoped>
 | 
			
		||||
.gqnyydlzavusgskkfvwvjiattxdzsqlf
 | 
			
		||||
	display block
 | 
			
		||||
	cursor zoom-in
 | 
			
		||||
	overflow hidden
 | 
			
		||||
	width 100%
 | 
			
		||||
	height 100%
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
		<div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
 | 
			
		||||
			<template v-for="media in mediaList">
 | 
			
		||||
				<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
 | 
			
		||||
				<mk-media-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
 | 
			
		||||
				<x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -17,10 +17,12 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import XBanner from './media-banner.vue';
 | 
			
		||||
import XImage from './media-image.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XBanner
 | 
			
		||||
		XBanner,
 | 
			
		||||
		XImage
 | 
			
		||||
	},
 | 
			
		||||
	props: {
 | 
			
		||||
		mediaList: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,8 @@ import MkUrl from './url.vue';
 | 
			
		|||
import MkMention from './mention.vue';
 | 
			
		||||
import { concat, sum } from '../../../../../prelude/array';
 | 
			
		||||
import MkFormula from './formula.vue';
 | 
			
		||||
import MkCode from './code.vue';
 | 
			
		||||
import MkGoogle from './google.vue';
 | 
			
		||||
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
 | 
			
		||||
import { host } from '../../../config';
 | 
			
		||||
import { preorderF, countNodesF } from '../../../../../prelude/tree';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +124,25 @@ export default Vue.component('misskey-flavored-markdown', {
 | 
			
		|||
					}, genEl(token.children));
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				case 'spin': {
 | 
			
		||||
					motionCount++;
 | 
			
		||||
					const isLong = sumTextsLength(token.children) > 5 || countNodesF(token.children) > 3;
 | 
			
		||||
					const isMany = motionCount > 3;
 | 
			
		||||
					return (createElement as any)('span', {
 | 
			
		||||
						attrs: {
 | 
			
		||||
							style: (this.$store.state.settings.disableAnimatedMfm || isLong || isMany) ? 'display: inline-block;' : 'display: inline-block; animation: spin 1.5s linear infinite;'
 | 
			
		||||
						},
 | 
			
		||||
					}, genEl(token.children));
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				case 'flip': {
 | 
			
		||||
					return (createElement as any)('span', {
 | 
			
		||||
						attrs: {
 | 
			
		||||
							style: 'display: inline-block; transform: scaleX(-1);'
 | 
			
		||||
						},
 | 
			
		||||
					}, genEl(token.children));
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				case 'url': {
 | 
			
		||||
					return [createElement(MkUrl, {
 | 
			
		||||
						key: Math.random(),
 | 
			
		||||
| 
						 | 
				
			
			@ -170,21 +189,22 @@ export default Vue.component('misskey-flavored-markdown', {
 | 
			
		|||
				}
 | 
			
		||||
 | 
			
		||||
				case 'blockCode': {
 | 
			
		||||
					return [createElement('pre', {
 | 
			
		||||
						class: 'code'
 | 
			
		||||
					}, [
 | 
			
		||||
						createElement('code', {
 | 
			
		||||
							domProps: {
 | 
			
		||||
								innerHTML: syntaxHighlight(token.node.props.code)
 | 
			
		||||
							}
 | 
			
		||||
						})
 | 
			
		||||
					])];
 | 
			
		||||
					return [createElement(MkCode, {
 | 
			
		||||
						key: Math.random(),
 | 
			
		||||
						props: {
 | 
			
		||||
							code: token.node.props.code,
 | 
			
		||||
							lang: token.node.props.lang,
 | 
			
		||||
						}
 | 
			
		||||
					})];
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				case 'inlineCode': {
 | 
			
		||||
					return [createElement('code', {
 | 
			
		||||
						domProps: {
 | 
			
		||||
							innerHTML: syntaxHighlight(token.node.props.code)
 | 
			
		||||
					return [createElement(MkCode, {
 | 
			
		||||
						key: Math.random(),
 | 
			
		||||
						props: {
 | 
			
		||||
							code: token.node.props.code,
 | 
			
		||||
							lang: token.node.props.lang,
 | 
			
		||||
							inline: true
 | 
			
		||||
						}
 | 
			
		||||
					})];
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,34 +24,10 @@ export default Vue.extend({
 | 
			
		|||
		background var(--mfmTitleBg)
 | 
			
		||||
		border-radius 4px
 | 
			
		||||
 | 
			
		||||
	>>> .code
 | 
			
		||||
		margin 8px 0
 | 
			
		||||
 | 
			
		||||
	>>> .quote
 | 
			
		||||
		margin 8px
 | 
			
		||||
		padding 6px 0 6px 12px
 | 
			
		||||
		color var(--mfmQuote)
 | 
			
		||||
		border-left solid 3px var(--mfmQuoteLine)
 | 
			
		||||
 | 
			
		||||
	>>> code
 | 
			
		||||
		padding 4px 8px
 | 
			
		||||
		margin 0 0.5em
 | 
			
		||||
		font-size 80%
 | 
			
		||||
		color #525252
 | 
			
		||||
		background rgba(0, 0, 0, 0.05)
 | 
			
		||||
		border-radius 2px
 | 
			
		||||
 | 
			
		||||
	>>> pre > code
 | 
			
		||||
		padding 16px
 | 
			
		||||
		margin 0
 | 
			
		||||
 | 
			
		||||
	>>> [data-is-me]:after
 | 
			
		||||
		content "you"
 | 
			
		||||
		padding 0 4px
 | 
			
		||||
		margin-left 4px
 | 
			
		||||
		font-size 80%
 | 
			
		||||
		color var(--primaryForeground)
 | 
			
		||||
		background var(--primary)
 | 
			
		||||
		border-radius 4px
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,21 +1,23 @@
 | 
			
		|||
<template>
 | 
			
		||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
 | 
			
		||||
	<span slot="header">
 | 
			
		||||
		<span :class="$style.title">{{ $t('choose-prompt') }}</span>
 | 
			
		||||
		<span :class="$style.count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span>
 | 
			
		||||
	<span slot="header" class="jqiaciqv">
 | 
			
		||||
		<span class="title">{{ $t('choose-prompt') }}</span>
 | 
			
		||||
		<span class="count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span>
 | 
			
		||||
	</span>
 | 
			
		||||
 | 
			
		||||
	<x-drive
 | 
			
		||||
		ref="browser"
 | 
			
		||||
		:class="$style.browser"
 | 
			
		||||
		:multiple="multiple"
 | 
			
		||||
		@selected="onSelected"
 | 
			
		||||
		@change-selection="onChangeSelection"
 | 
			
		||||
	/>
 | 
			
		||||
	<div :class="$style.footer">
 | 
			
		||||
		<button :class="$style.upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button>
 | 
			
		||||
		<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
 | 
			
		||||
		<button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</button>
 | 
			
		||||
	<div class="rqsvbumu">
 | 
			
		||||
		<x-drive
 | 
			
		||||
			ref="browser"
 | 
			
		||||
			class="browser"
 | 
			
		||||
			:multiple="multiple"
 | 
			
		||||
			@selected="onSelected"
 | 
			
		||||
			@change-selection="onChangeSelection"
 | 
			
		||||
		/>
 | 
			
		||||
		<div class="footer">
 | 
			
		||||
			<button class="upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button>
 | 
			
		||||
			<ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button>
 | 
			
		||||
			<ui-button inline primary :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</ui-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</mk-window>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -60,120 +62,67 @@ export default Vue.extend({
 | 
			
		|||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" module>
 | 
			
		||||
.title
 | 
			
		||||
	> [data-icon]
 | 
			
		||||
		margin-right 4px
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.jqiaciqv
 | 
			
		||||
	.title
 | 
			
		||||
		> [data-icon]
 | 
			
		||||
			margin-right 4px
 | 
			
		||||
 | 
			
		||||
.count
 | 
			
		||||
	margin-left 8px
 | 
			
		||||
	opacity 0.7
 | 
			
		||||
 | 
			
		||||
.browser
 | 
			
		||||
	height calc(100% - 72px)
 | 
			
		||||
 | 
			
		||||
.footer
 | 
			
		||||
	height 72px
 | 
			
		||||
	background var(--primaryLighten95)
 | 
			
		||||
 | 
			
		||||
.upload
 | 
			
		||||
	display inline-block
 | 
			
		||||
	position absolute
 | 
			
		||||
	top 8px
 | 
			
		||||
	left 16px
 | 
			
		||||
	cursor pointer
 | 
			
		||||
	padding 0
 | 
			
		||||
	margin 8px 4px 0 0
 | 
			
		||||
	width 40px
 | 
			
		||||
	height 40px
 | 
			
		||||
	font-size 1em
 | 
			
		||||
	color var(--primaryAlpha05)
 | 
			
		||||
	background transparent
 | 
			
		||||
	outline none
 | 
			
		||||
	border solid 1px transparent
 | 
			
		||||
	border-radius 4px
 | 
			
		||||
 | 
			
		||||
	&:hover
 | 
			
		||||
		background transparent
 | 
			
		||||
		border-color var(--primaryAlpha03)
 | 
			
		||||
 | 
			
		||||
	&:active
 | 
			
		||||
		color var(--primaryAlpha06)
 | 
			
		||||
		background transparent
 | 
			
		||||
		border-color var(--primaryAlpha05)
 | 
			
		||||
		//box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset
 | 
			
		||||
 | 
			
		||||
	&:focus
 | 
			
		||||
		&:after
 | 
			
		||||
			content ""
 | 
			
		||||
			pointer-events none
 | 
			
		||||
			position absolute
 | 
			
		||||
			top -5px
 | 
			
		||||
			right -5px
 | 
			
		||||
			bottom -5px
 | 
			
		||||
			left -5px
 | 
			
		||||
			border 2px solid var(--primaryAlpha03)
 | 
			
		||||
			border-radius 8px
 | 
			
		||||
 | 
			
		||||
.ok
 | 
			
		||||
.cancel
 | 
			
		||||
	display block
 | 
			
		||||
	position absolute
 | 
			
		||||
	bottom 16px
 | 
			
		||||
	cursor pointer
 | 
			
		||||
	padding 0
 | 
			
		||||
	margin 0
 | 
			
		||||
	width 120px
 | 
			
		||||
	height 40px
 | 
			
		||||
	font-size 1em
 | 
			
		||||
	outline none
 | 
			
		||||
	border-radius 4px
 | 
			
		||||
 | 
			
		||||
	&:focus
 | 
			
		||||
		&:after
 | 
			
		||||
			content ""
 | 
			
		||||
			pointer-events none
 | 
			
		||||
			position absolute
 | 
			
		||||
			top -5px
 | 
			
		||||
			right -5px
 | 
			
		||||
			bottom -5px
 | 
			
		||||
			left -5px
 | 
			
		||||
			border 2px solid var(--primaryAlpha03)
 | 
			
		||||
			border-radius 8px
 | 
			
		||||
 | 
			
		||||
	&:disabled
 | 
			
		||||
	.count
 | 
			
		||||
		margin-left 8px
 | 
			
		||||
		opacity 0.7
 | 
			
		||||
		cursor default
 | 
			
		||||
 | 
			
		||||
.ok
 | 
			
		||||
	right 16px
 | 
			
		||||
	color var(--primaryForeground)
 | 
			
		||||
	background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
 | 
			
		||||
	border solid 1px var(--primaryLighten15)
 | 
			
		||||
.rqsvbumu
 | 
			
		||||
	display flex
 | 
			
		||||
	flex-direction column
 | 
			
		||||
	height 100%
 | 
			
		||||
 | 
			
		||||
	&:not(:disabled)
 | 
			
		||||
		font-weight bold
 | 
			
		||||
	.browser
 | 
			
		||||
		flex 1
 | 
			
		||||
		overflow auto
 | 
			
		||||
 | 
			
		||||
	&:hover:not(:disabled)
 | 
			
		||||
		background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
 | 
			
		||||
		border-color var(--primary)
 | 
			
		||||
	.footer
 | 
			
		||||
		padding 16px
 | 
			
		||||
		background var(--desktopPostFormBg)
 | 
			
		||||
		text-align right
 | 
			
		||||
 | 
			
		||||
	&:active:not(:disabled)
 | 
			
		||||
		background var(--primary)
 | 
			
		||||
		border-color var(--primary)
 | 
			
		||||
	.upload
 | 
			
		||||
		display inline-block
 | 
			
		||||
		position absolute
 | 
			
		||||
		top 8px
 | 
			
		||||
		left 16px
 | 
			
		||||
		cursor pointer
 | 
			
		||||
		padding 0
 | 
			
		||||
		margin 8px 4px 0 0
 | 
			
		||||
		width 40px
 | 
			
		||||
		height 40px
 | 
			
		||||
		font-size 1em
 | 
			
		||||
		color var(--primaryAlpha05)
 | 
			
		||||
		background transparent
 | 
			
		||||
		outline none
 | 
			
		||||
		border solid 1px transparent
 | 
			
		||||
		border-radius 4px
 | 
			
		||||
 | 
			
		||||
.cancel
 | 
			
		||||
	right 148px
 | 
			
		||||
	color #888
 | 
			
		||||
	background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
 | 
			
		||||
	border solid 1px #e2e2e2
 | 
			
		||||
		&:hover
 | 
			
		||||
			background transparent
 | 
			
		||||
			border-color var(--primaryAlpha03)
 | 
			
		||||
 | 
			
		||||
	&:hover
 | 
			
		||||
		background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
 | 
			
		||||
		border-color #dcdcdc
 | 
			
		||||
		&:active
 | 
			
		||||
			color var(--primaryAlpha06)
 | 
			
		||||
			background transparent
 | 
			
		||||
			border-color var(--primaryAlpha05)
 | 
			
		||||
			//box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset
 | 
			
		||||
 | 
			
		||||
	&:active
 | 
			
		||||
		background #ececec
 | 
			
		||||
		border-color #dcdcdc
 | 
			
		||||
		&:focus
 | 
			
		||||
			&:after
 | 
			
		||||
				content ""
 | 
			
		||||
				pointer-events none
 | 
			
		||||
				position absolute
 | 
			
		||||
				top -5px
 | 
			
		||||
				right -5px
 | 
			
		||||
				bottom -5px
 | 
			
		||||
				left -5px
 | 
			
		||||
				border 2px solid var(--primaryAlpha03)
 | 
			
		||||
				border-radius 8px
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,19 @@
 | 
			
		|||
<template>
 | 
			
		||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
 | 
			
		||||
	<span slot="header">
 | 
			
		||||
		<span :class="$style.title">{{ $t('choose-prompt') }}</span>
 | 
			
		||||
		<span>{{ $t('choose-prompt') }}</span>
 | 
			
		||||
	</span>
 | 
			
		||||
 | 
			
		||||
	<x-drive
 | 
			
		||||
		ref="browser"
 | 
			
		||||
		:class="$style.browser"
 | 
			
		||||
		:multiple="false"
 | 
			
		||||
	/>
 | 
			
		||||
	<div :class="$style.footer">
 | 
			
		||||
		<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
 | 
			
		||||
		<button :class="$style.ok" @click="ok">{{ $t('ok') }}</button>
 | 
			
		||||
	<div class="hllkpxxu">
 | 
			
		||||
		<x-drive
 | 
			
		||||
			ref="browser"
 | 
			
		||||
			class="browser"
 | 
			
		||||
			:multiple="false"
 | 
			
		||||
		/>
 | 
			
		||||
		<div class="footer">
 | 
			
		||||
			<ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button>
 | 
			
		||||
			<ui-button inline @click="ok" primary>{{ $t('ok') }}</ui-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</mk-window>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -36,79 +38,19 @@ export default Vue.extend({
 | 
			
		|||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" module>
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.hllkpxxu
 | 
			
		||||
	display flex
 | 
			
		||||
	flex-direction column
 | 
			
		||||
	height 100%
 | 
			
		||||
 | 
			
		||||
	.browser
 | 
			
		||||
		flex 1
 | 
			
		||||
		overflow auto
 | 
			
		||||
 | 
			
		||||
.title
 | 
			
		||||
	> [data-icon]
 | 
			
		||||
		margin-right 4px
 | 
			
		||||
 | 
			
		||||
.browser
 | 
			
		||||
	height calc(100% - 72px)
 | 
			
		||||
 | 
			
		||||
.footer
 | 
			
		||||
	height 72px
 | 
			
		||||
	background var(--primaryLighten95)
 | 
			
		||||
 | 
			
		||||
.ok
 | 
			
		||||
.cancel
 | 
			
		||||
	display block
 | 
			
		||||
	position absolute
 | 
			
		||||
	bottom 16px
 | 
			
		||||
	cursor pointer
 | 
			
		||||
	padding 0
 | 
			
		||||
	margin 0
 | 
			
		||||
	width 120px
 | 
			
		||||
	height 40px
 | 
			
		||||
	font-size 1em
 | 
			
		||||
	outline none
 | 
			
		||||
	border-radius 4px
 | 
			
		||||
 | 
			
		||||
	&:focus
 | 
			
		||||
		&:after
 | 
			
		||||
			content ""
 | 
			
		||||
			pointer-events none
 | 
			
		||||
			position absolute
 | 
			
		||||
			top -5px
 | 
			
		||||
			right -5px
 | 
			
		||||
			bottom -5px
 | 
			
		||||
			left -5px
 | 
			
		||||
			border 2px solid var(--primaryAlpha03)
 | 
			
		||||
			border-radius 8px
 | 
			
		||||
 | 
			
		||||
	&:disabled
 | 
			
		||||
		opacity 0.7
 | 
			
		||||
		cursor default
 | 
			
		||||
 | 
			
		||||
.ok
 | 
			
		||||
	right 16px
 | 
			
		||||
	color var(--primaryForeground)
 | 
			
		||||
	background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
 | 
			
		||||
	border solid 1px var(--primaryLighten15)
 | 
			
		||||
 | 
			
		||||
	&:not(:disabled)
 | 
			
		||||
		font-weight bold
 | 
			
		||||
 | 
			
		||||
	&:hover:not(:disabled)
 | 
			
		||||
		background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
 | 
			
		||||
		border-color var(--primary)
 | 
			
		||||
 | 
			
		||||
	&:active:not(:disabled)
 | 
			
		||||
		background var(--primary)
 | 
			
		||||
		border-color var(--primary)
 | 
			
		||||
 | 
			
		||||
.cancel
 | 
			
		||||
	right 148px
 | 
			
		||||
	color #888
 | 
			
		||||
	background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
 | 
			
		||||
	border solid 1px #e2e2e2
 | 
			
		||||
 | 
			
		||||
	&:hover
 | 
			
		||||
		background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
 | 
			
		||||
		border-color #dcdcdc
 | 
			
		||||
 | 
			
		||||
	&:active
 | 
			
		||||
		background #ececec
 | 
			
		||||
		border-color #dcdcdc
 | 
			
		||||
	.footer
 | 
			
		||||
		padding 16px
 | 
			
		||||
		background var(--desktopPostFormBg)
 | 
			
		||||
		text-align right
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,6 @@ import subNoteContent from './sub-note-content.vue';
 | 
			
		|||
import window from './window.vue';
 | 
			
		||||
import noteFormWindow from './post-form-window.vue';
 | 
			
		||||
import renoteFormWindow from './renote-form-window.vue';
 | 
			
		||||
import mediaImage from './media-image.vue';
 | 
			
		||||
import mediaVideo from './media-video.vue';
 | 
			
		||||
import notifications from './notifications.vue';
 | 
			
		||||
import noteForm from './post-form.vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +31,6 @@ Vue.component('mk-sub-note-content', subNoteContent);
 | 
			
		|||
Vue.component('mk-window', window);
 | 
			
		||||
Vue.component('mk-post-form-window', noteFormWindow);
 | 
			
		||||
Vue.component('mk-renote-form-window', renoteFormWindow);
 | 
			
		||||
Vue.component('mk-media-image', mediaImage);
 | 
			
		||||
Vue.component('mk-media-video', mediaVideo);
 | 
			
		||||
Vue.component('mk-notifications', notifications);
 | 
			
		||||
Vue.component('mk-post-form', noteForm);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,81 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
 | 
			
		||||
	<div>
 | 
			
		||||
		<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b>
 | 
			
		||||
		<span>{{ $t('click-to-show') }}</span>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<a class="lcjomzwbohoelkxsnuqjiaccdbdfiazy" v-else
 | 
			
		||||
	:href="image.url"
 | 
			
		||||
	@click.prevent="onClick"
 | 
			
		||||
	:style="style"
 | 
			
		||||
	:title="image.name"
 | 
			
		||||
></a>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import ImageViewer from '../../../common/views/components/image-viewer.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('desktop/views/components/media-image.vue'),
 | 
			
		||||
	props: {
 | 
			
		||||
		image: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		raw: {
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			hide: true
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		style(): any {
 | 
			
		||||
			return {
 | 
			
		||||
				'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 | 
			
		||||
				'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.thumbnailUrl})`
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		onClick() {
 | 
			
		||||
			this.$root.new(ImageViewer, {
 | 
			
		||||
				image: this.image
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.lcjomzwbohoelkxsnuqjiaccdbdfiazy
 | 
			
		||||
	display block
 | 
			
		||||
	cursor zoom-in
 | 
			
		||||
	overflow hidden
 | 
			
		||||
	width 100%
 | 
			
		||||
	height 100%
 | 
			
		||||
	background-position center
 | 
			
		||||
	background-size contain
 | 
			
		||||
	background-repeat no-repeat
 | 
			
		||||
 | 
			
		||||
.ldwbgwstjsdgcjruamauqdrffetqudry
 | 
			
		||||
	display flex
 | 
			
		||||
	justify-content center
 | 
			
		||||
	align-items center
 | 
			
		||||
	background #111
 | 
			
		||||
	color #fff
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
		display table-cell
 | 
			
		||||
		text-align center
 | 
			
		||||
		font-size 12px
 | 
			
		||||
 | 
			
		||||
		> *
 | 
			
		||||
			display block
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,6 @@ import Vue from 'vue';
 | 
			
		|||
import ui from './ui.vue';
 | 
			
		||||
import note from './note.vue';
 | 
			
		||||
import notes from './notes.vue';
 | 
			
		||||
import mediaImage from './media-image.vue';
 | 
			
		||||
import mediaVideo from './media-video.vue';
 | 
			
		||||
import notePreview from './note-preview.vue';
 | 
			
		||||
import subNoteContent from './sub-note-content.vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +23,6 @@ import postForm from './post-form.vue';
 | 
			
		|||
Vue.component('mk-ui', ui);
 | 
			
		||||
Vue.component('mk-note', note);
 | 
			
		||||
Vue.component('mk-notes', notes);
 | 
			
		||||
Vue.component('mk-media-image', mediaImage);
 | 
			
		||||
Vue.component('mk-media-video', mediaVideo);
 | 
			
		||||
Vue.component('mk-note-preview', notePreview);
 | 
			
		||||
Vue.component('mk-sub-note-content', subNoteContent);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,8 @@
 | 
			
		|||
 | 
			
		||||
html
 | 
			
		||||
	--primary #fb4e4e
 | 
			
		||||
	--link #fb4e4e
 | 
			
		||||
	--linkTapHighlight #fb4e4eb3
 | 
			
		||||
 | 
			
		||||
body
 | 
			
		||||
	margin 0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -100,20 +100,6 @@ export default class Reversi {
 | 
			
		|||
		return count(WHITE, this.board);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 黒石の比率
 | 
			
		||||
	 */
 | 
			
		||||
	public get blackP() {
 | 
			
		||||
		return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.blackCount / (this.blackCount + this.whiteCount);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 白石の比率
 | 
			
		||||
	 */
 | 
			
		||||
	public get whiteP() {
 | 
			
		||||
		return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.whiteCount / (this.blackCount + this.whiteCount);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public transformPosToXy(pos: number): number[] {
 | 
			
		||||
		const x = pos % this.mapWidth;
 | 
			
		||||
		const y = Math.floor(pos / this.mapWidth);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,6 +55,18 @@ export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteU
 | 
			
		|||
			return el;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		spin(token) {
 | 
			
		||||
			const el = doc.createElement('i');
 | 
			
		||||
			appendChildren(token.children, el);
 | 
			
		||||
			return el;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		flip(token) {
 | 
			
		||||
			const el = doc.createElement('span');
 | 
			
		||||
			appendChildren(token.children, el);
 | 
			
		||||
			return el;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		blockCode(token) {
 | 
			
		||||
			const pre = doc.createElement('pre');
 | 
			
		||||
			const inner = doc.createElement('code');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,6 +91,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
	root: r => P.alt(
 | 
			
		||||
		r.big,
 | 
			
		||||
		r.small,
 | 
			
		||||
		r.spin,
 | 
			
		||||
		r.bold,
 | 
			
		||||
		r.strike,
 | 
			
		||||
		r.italic,
 | 
			
		||||
| 
						 | 
				
			
			@ -101,6 +102,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
		r.hashtag,
 | 
			
		||||
		r.emoji,
 | 
			
		||||
		r.blockCode,
 | 
			
		||||
		r.flip,
 | 
			
		||||
		r.inlineCode,
 | 
			
		||||
		r.quote,
 | 
			
		||||
		r.mathInline,
 | 
			
		||||
| 
						 | 
				
			
			@ -123,6 +125,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
			r.hashtag,
 | 
			
		||||
			r.emoji,
 | 
			
		||||
			r.mathInline,
 | 
			
		||||
			r.spin,
 | 
			
		||||
			r.text
 | 
			
		||||
		).atLeast(1).tryParse(x), {})),
 | 
			
		||||
	//#endregion
 | 
			
		||||
| 
						 | 
				
			
			@ -141,6 +144,15 @@ const mfm = P.createLanguage({
 | 
			
		|||
		).atLeast(1).tryParse(x), {})),
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region Spin
 | 
			
		||||
	spin: r =>
 | 
			
		||||
		P.regexp(/<spin>(.+?)<\/spin>/, 1)
 | 
			
		||||
		.map(x => createTree('spin', P.alt(
 | 
			
		||||
			r.emoji,
 | 
			
		||||
			r.text
 | 
			
		||||
		).atLeast(1).tryParse(x), {})),
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region Block code
 | 
			
		||||
	blockCode: r =>
 | 
			
		||||
		newline.then(
 | 
			
		||||
| 
						 | 
				
			
			@ -163,6 +175,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
			r.hashtag,
 | 
			
		||||
			r.url,
 | 
			
		||||
			r.link,
 | 
			
		||||
			r.flip,
 | 
			
		||||
			r.emoji,
 | 
			
		||||
			r.text
 | 
			
		||||
		).atLeast(1).tryParse(x), {})),
 | 
			
		||||
| 
						 | 
				
			
			@ -174,6 +187,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
		.map(x => createTree('center', P.alt(
 | 
			
		||||
			r.big,
 | 
			
		||||
			r.small,
 | 
			
		||||
			r.spin,
 | 
			
		||||
			r.bold,
 | 
			
		||||
			r.strike,
 | 
			
		||||
			r.italic,
 | 
			
		||||
| 
						 | 
				
			
			@ -184,6 +198,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
			r.mathInline,
 | 
			
		||||
			r.url,
 | 
			
		||||
			r.link,
 | 
			
		||||
			r.flip,
 | 
			
		||||
			r.text
 | 
			
		||||
		).atLeast(1).tryParse(x), {})),
 | 
			
		||||
	//#endregion
 | 
			
		||||
| 
						 | 
				
			
			@ -217,6 +232,23 @@ const mfm = P.createLanguage({
 | 
			
		|||
		}),
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region Flip
 | 
			
		||||
	flip: r =>
 | 
			
		||||
		P.regexp(/<flip>(.+?)<\/flip>/, 1)
 | 
			
		||||
		.map(x => createTree('flip', P.alt(
 | 
			
		||||
			r.big,
 | 
			
		||||
			r.small,
 | 
			
		||||
			r.spin,
 | 
			
		||||
			r.bold,
 | 
			
		||||
			r.strike,
 | 
			
		||||
			r.link,
 | 
			
		||||
			r.italic,
 | 
			
		||||
			r.motion,
 | 
			
		||||
			r.emoji,
 | 
			
		||||
			r.text
 | 
			
		||||
		).atLeast(1).tryParse(x), {})),
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region Inline code
 | 
			
		||||
	inlineCode: r =>
 | 
			
		||||
		P.regexp(/`([^´\n]+?)`/, 1)
 | 
			
		||||
| 
						 | 
				
			
			@ -242,6 +274,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
			r.hashtag,
 | 
			
		||||
			r.url,
 | 
			
		||||
			r.link,
 | 
			
		||||
			r.flip,
 | 
			
		||||
			r.emoji,
 | 
			
		||||
			r.text
 | 
			
		||||
		).atLeast(1).tryParse(x), {})),
 | 
			
		||||
| 
						 | 
				
			
			@ -262,6 +295,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
			return createTree('link', P.alt(
 | 
			
		||||
				r.big,
 | 
			
		||||
				r.small,
 | 
			
		||||
				r.spin,
 | 
			
		||||
				r.bold,
 | 
			
		||||
				r.strike,
 | 
			
		||||
				r.italic,
 | 
			
		||||
| 
						 | 
				
			
			@ -311,6 +345,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
		.map(x => createTree('motion', P.alt(
 | 
			
		||||
			r.bold,
 | 
			
		||||
			r.small,
 | 
			
		||||
			r.spin,
 | 
			
		||||
			r.strike,
 | 
			
		||||
			r.italic,
 | 
			
		||||
			r.mention,
 | 
			
		||||
| 
						 | 
				
			
			@ -318,6 +353,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
			r.emoji,
 | 
			
		||||
			r.url,
 | 
			
		||||
			r.link,
 | 
			
		||||
			r.flip,
 | 
			
		||||
			r.mathInline,
 | 
			
		||||
			r.text
 | 
			
		||||
		).atLeast(1).tryParse(x), {})),
 | 
			
		||||
| 
						 | 
				
			
			@ -356,6 +392,7 @@ const mfm = P.createLanguage({
 | 
			
		|||
			r.hashtag,
 | 
			
		||||
			r.url,
 | 
			
		||||
			r.link,
 | 
			
		||||
			r.flip,
 | 
			
		||||
			r.emoji,
 | 
			
		||||
			r.text
 | 
			
		||||
		).atLeast(1).tryParse(x), {})),
 | 
			
		||||
| 
						 | 
				
			
			@ -365,18 +402,20 @@ const mfm = P.createLanguage({
 | 
			
		|||
	title: r =>
 | 
			
		||||
		newline.then(P((input, i) => {
 | 
			
		||||
			const text = input.substr(i);
 | 
			
		||||
			const match = text.match(/^((【|\[)(.+?)(】|]))(\n|$)/);
 | 
			
		||||
			const match = text.match(/^([【\[]([^【\[】\]\n]+?)[】\]])(\n|$)/);
 | 
			
		||||
			if (!match) return P.makeFailure(i, 'not a title');
 | 
			
		||||
			const q = match[1].trim().substring(1, match[1].length - 1);
 | 
			
		||||
			const q = match[2].trim();
 | 
			
		||||
			const contents = P.alt(
 | 
			
		||||
				r.big,
 | 
			
		||||
				r.small,
 | 
			
		||||
				r.spin,
 | 
			
		||||
				r.bold,
 | 
			
		||||
				r.strike,
 | 
			
		||||
				r.italic,
 | 
			
		||||
				r.motion,
 | 
			
		||||
				r.url,
 | 
			
		||||
				r.link,
 | 
			
		||||
				r.flip,
 | 
			
		||||
				r.mention,
 | 
			
		||||
				r.hashtag,
 | 
			
		||||
				r.emoji,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,343 +0,0 @@
 | 
			
		|||
import { capitalize, toUpperCase } from '../prelude/string';
 | 
			
		||||
 | 
			
		||||
function escape(text: string) {
 | 
			
		||||
	return text
 | 
			
		||||
		.replace(/>/g, '>')
 | 
			
		||||
		.replace(/</g, '<');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 文字数が多い順にソートします
 | 
			
		||||
// そうしないと、「function」という文字列が与えられたときに「func」が先にマッチしてしまう可能性があるためです
 | 
			
		||||
const _keywords = [
 | 
			
		||||
	'true',
 | 
			
		||||
	'false',
 | 
			
		||||
	'null',
 | 
			
		||||
	'nil',
 | 
			
		||||
	'undefined',
 | 
			
		||||
	'void',
 | 
			
		||||
	'var',
 | 
			
		||||
	'const',
 | 
			
		||||
	'let',
 | 
			
		||||
	'mut',
 | 
			
		||||
	'dim',
 | 
			
		||||
	'if',
 | 
			
		||||
	'then',
 | 
			
		||||
	'else',
 | 
			
		||||
	'switch',
 | 
			
		||||
	'match',
 | 
			
		||||
	'case',
 | 
			
		||||
	'default',
 | 
			
		||||
	'for',
 | 
			
		||||
	'each',
 | 
			
		||||
	'in',
 | 
			
		||||
	'while',
 | 
			
		||||
	'loop',
 | 
			
		||||
	'continue',
 | 
			
		||||
	'break',
 | 
			
		||||
	'do',
 | 
			
		||||
	'goto',
 | 
			
		||||
	'next',
 | 
			
		||||
	'end',
 | 
			
		||||
	'sub',
 | 
			
		||||
	'throw',
 | 
			
		||||
	'try',
 | 
			
		||||
	'catch',
 | 
			
		||||
	'finally',
 | 
			
		||||
	'enum',
 | 
			
		||||
	'delegate',
 | 
			
		||||
	'function',
 | 
			
		||||
	'func',
 | 
			
		||||
	'fun',
 | 
			
		||||
	'fn',
 | 
			
		||||
	'return',
 | 
			
		||||
	'yield',
 | 
			
		||||
	'async',
 | 
			
		||||
	'await',
 | 
			
		||||
	'require',
 | 
			
		||||
	'include',
 | 
			
		||||
	'import',
 | 
			
		||||
	'imports',
 | 
			
		||||
	'export',
 | 
			
		||||
	'exports',
 | 
			
		||||
	'from',
 | 
			
		||||
	'as',
 | 
			
		||||
	'using',
 | 
			
		||||
	'use',
 | 
			
		||||
	'internal',
 | 
			
		||||
	'module',
 | 
			
		||||
	'namespace',
 | 
			
		||||
	'where',
 | 
			
		||||
	'select',
 | 
			
		||||
	'struct',
 | 
			
		||||
	'union',
 | 
			
		||||
	'new',
 | 
			
		||||
	'delete',
 | 
			
		||||
	'this',
 | 
			
		||||
	'super',
 | 
			
		||||
	'base',
 | 
			
		||||
	'class',
 | 
			
		||||
	'interface',
 | 
			
		||||
	'abstract',
 | 
			
		||||
	'static',
 | 
			
		||||
	'public',
 | 
			
		||||
	'private',
 | 
			
		||||
	'protected',
 | 
			
		||||
	'virtual',
 | 
			
		||||
	'partial',
 | 
			
		||||
	'override',
 | 
			
		||||
	'extends',
 | 
			
		||||
	'implements',
 | 
			
		||||
	'constructor'
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const keywords = _keywords
 | 
			
		||||
	.concat(_keywords.map(capitalize))
 | 
			
		||||
	.concat(_keywords.map(toUpperCase))
 | 
			
		||||
	.sort((a, b) => b.length - a.length);
 | 
			
		||||
 | 
			
		||||
const symbols = [
 | 
			
		||||
	'=',
 | 
			
		||||
	'+',
 | 
			
		||||
	'-',
 | 
			
		||||
	'*',
 | 
			
		||||
	'/',
 | 
			
		||||
	'%',
 | 
			
		||||
	'~',
 | 
			
		||||
	'^',
 | 
			
		||||
	'&',
 | 
			
		||||
	'|',
 | 
			
		||||
	'>',
 | 
			
		||||
	'<',
 | 
			
		||||
	'!',
 | 
			
		||||
	'?'
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
type Token = {
 | 
			
		||||
	html: string
 | 
			
		||||
	next: number
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Element = (code: string, i: number, source: string) => (Token | null);
 | 
			
		||||
 | 
			
		||||
const elements: Element[] = [
 | 
			
		||||
	// comment
 | 
			
		||||
	code => {
 | 
			
		||||
		if (code.substr(0, 2) != '//') return null;
 | 
			
		||||
		const match = code.match(/^\/\/(.+?)(\n|$)/);
 | 
			
		||||
		if (!match) return null;
 | 
			
		||||
		const comment = match[0];
 | 
			
		||||
		return {
 | 
			
		||||
			html: `<span class="comment">${escape(comment)}</span>`,
 | 
			
		||||
			next: comment.length
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// block comment
 | 
			
		||||
	code => {
 | 
			
		||||
		const match = code.match(/^\/\*([\s\S]+?)\*\//);
 | 
			
		||||
		if (!match) return null;
 | 
			
		||||
		return {
 | 
			
		||||
			html: `<span class="comment">${escape(match[0])}</span>`,
 | 
			
		||||
			next: match[0].length
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// string
 | 
			
		||||
	code => {
 | 
			
		||||
		if (!/^['"`]/.test(code)) return null;
 | 
			
		||||
		const begin = code[0];
 | 
			
		||||
		let str = begin;
 | 
			
		||||
		let thisIsNotAString = false;
 | 
			
		||||
		for (let i = 1; i < code.length; i++) {
 | 
			
		||||
			const char = code[i];
 | 
			
		||||
			if (char == '\\') {
 | 
			
		||||
				str += char;
 | 
			
		||||
				str += code[i + 1] || '';
 | 
			
		||||
				i++;
 | 
			
		||||
				continue;
 | 
			
		||||
			} else if (char == begin) {
 | 
			
		||||
				str += char;
 | 
			
		||||
				break;
 | 
			
		||||
			} else if (char == '\n' || i == (code.length - 1)) {
 | 
			
		||||
				thisIsNotAString = true;
 | 
			
		||||
				break;
 | 
			
		||||
			} else {
 | 
			
		||||
				str += char;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (thisIsNotAString) {
 | 
			
		||||
			return null;
 | 
			
		||||
		} else {
 | 
			
		||||
			return {
 | 
			
		||||
				html: `<span class="string">${escape(str)}</span>`,
 | 
			
		||||
				next: str.length
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// regexp
 | 
			
		||||
	code => {
 | 
			
		||||
		if (code[0] != '/') return null;
 | 
			
		||||
		let regexp = '';
 | 
			
		||||
		let thisIsNotARegexp = false;
 | 
			
		||||
		for (let i = 1; i < code.length; i++) {
 | 
			
		||||
			const char = code[i];
 | 
			
		||||
			if (char == '\\') {
 | 
			
		||||
				regexp += char;
 | 
			
		||||
				regexp += code[i + 1] || '';
 | 
			
		||||
				i++;
 | 
			
		||||
				continue;
 | 
			
		||||
			} else if (char == '/') {
 | 
			
		||||
				break;
 | 
			
		||||
			} else if (char == '\n' || i == (code.length - 1)) {
 | 
			
		||||
				thisIsNotARegexp = true;
 | 
			
		||||
				break;
 | 
			
		||||
			} else {
 | 
			
		||||
				regexp += char;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (thisIsNotARegexp) return null;
 | 
			
		||||
		if (regexp == '') return null;
 | 
			
		||||
		if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null;
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			html: `<span class="regexp">/${escape(regexp)}/</span>`,
 | 
			
		||||
			next: regexp.length + 2
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// label
 | 
			
		||||
	code => {
 | 
			
		||||
		if (code[0] != '@') return null;
 | 
			
		||||
		const match = code.match(/^@([a-zA-Z_-]+?)\n/);
 | 
			
		||||
		if (!match) return null;
 | 
			
		||||
		const label = match[0];
 | 
			
		||||
		return {
 | 
			
		||||
			html: `<span class="label">${label}</span>`,
 | 
			
		||||
			next: label.length
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// number
 | 
			
		||||
	(code, i, source) => {
 | 
			
		||||
		const prev = source[i - 1];
 | 
			
		||||
		if (prev && /[a-zA-Z]/.test(prev)) return null;
 | 
			
		||||
		if (!/^[\-\+]?[0-9\.]+/.test(code)) return null;
 | 
			
		||||
		const match = code.match(/^[\-\+]?[0-9\.]+/)[0];
 | 
			
		||||
		if (match) {
 | 
			
		||||
			return {
 | 
			
		||||
				html: `<span class="number">${match}</span>`,
 | 
			
		||||
				next: match.length
 | 
			
		||||
			};
 | 
			
		||||
		} else {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// nan
 | 
			
		||||
	(code, i, source) => {
 | 
			
		||||
		const prev = source[i - 1];
 | 
			
		||||
		if (prev && /[a-zA-Z]/.test(prev)) return null;
 | 
			
		||||
		if (code.substr(0, 3) == 'NaN') {
 | 
			
		||||
			return {
 | 
			
		||||
				html: `<span class="nan">NaN</span>`,
 | 
			
		||||
				next: 3
 | 
			
		||||
			};
 | 
			
		||||
		} else {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// method
 | 
			
		||||
	code => {
 | 
			
		||||
		const match = code.match(/^([a-zA-Z_-]+?)\(/);
 | 
			
		||||
		if (!match) return null;
 | 
			
		||||
 | 
			
		||||
		if (match[1] == '-') return null;
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			html: `<span class="method">${match[1]}</span>`,
 | 
			
		||||
			next: match[1].length
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// property
 | 
			
		||||
	(code, i, source) => {
 | 
			
		||||
		const prev = source[i - 1];
 | 
			
		||||
		if (prev != '.') return null;
 | 
			
		||||
 | 
			
		||||
		const match = code.match(/^[a-zA-Z0-9_-]+/);
 | 
			
		||||
		if (!match) return null;
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			html: `<span class="property">${match[0]}</span>`,
 | 
			
		||||
			next: match[0].length
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// keyword
 | 
			
		||||
	(code, i, source) => {
 | 
			
		||||
		const prev = source[i - 1];
 | 
			
		||||
		if (prev && /[a-zA-Z]/.test(prev)) return null;
 | 
			
		||||
 | 
			
		||||
		const match = keywords.filter(k => code.substr(0, k.length) == k)[0];
 | 
			
		||||
		if (match) {
 | 
			
		||||
			if (/^[a-zA-Z]/.test(code.substr(match.length))) return null;
 | 
			
		||||
			return {
 | 
			
		||||
				html: `<span class="keyword ${match}">${match}</span>`,
 | 
			
		||||
				next: match.length
 | 
			
		||||
			};
 | 
			
		||||
		} else {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// symbol
 | 
			
		||||
	code => {
 | 
			
		||||
		const match = symbols.filter(s => code[0] == s)[0];
 | 
			
		||||
		if (match) {
 | 
			
		||||
			return {
 | 
			
		||||
				html: `<span class="symbol">${match}</span>`,
 | 
			
		||||
				next: 1
 | 
			
		||||
			};
 | 
			
		||||
		} else {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// TODO: specify lang
 | 
			
		||||
export default (source: string, lang?: string): string => {
 | 
			
		||||
	let code = source;
 | 
			
		||||
	let html = '';
 | 
			
		||||
 | 
			
		||||
	let i = 0;
 | 
			
		||||
 | 
			
		||||
	function push(token: Token) {
 | 
			
		||||
		html += token.html;
 | 
			
		||||
		code = code.substr(token.next);
 | 
			
		||||
		i += token.next;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	while (code != '') {
 | 
			
		||||
		const parsed = elements.some(el => {
 | 
			
		||||
			const e = el(code, i, source);
 | 
			
		||||
			if (e) {
 | 
			
		||||
				push(e);
 | 
			
		||||
				return true;
 | 
			
		||||
			} else {
 | 
			
		||||
				return false;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (!parsed) {
 | 
			
		||||
			push({
 | 
			
		||||
				html: escape(code[0]),
 | 
			
		||||
				next: 1
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return html;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import * as mongo from 'mongodb';
 | 
			
		||||
import Note from "../../../models/note";
 | 
			
		||||
import User, { isRemoteUser, isLocalUser } from "../../../models/user";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get valied note for API processing
 | 
			
		||||
| 
						 | 
				
			
			@ -16,3 +17,44 @@ export async function getValiedNote(noteId: mongo.ObjectID) {
 | 
			
		|||
 | 
			
		||||
	return note;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get user for API processing
 | 
			
		||||
 */
 | 
			
		||||
export async function getUser(userId: mongo.ObjectID) {
 | 
			
		||||
	const user = await User.findOne({
 | 
			
		||||
		_id: userId
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (user == null) {
 | 
			
		||||
		throw 'user not found';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return user;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get remote user for API processing
 | 
			
		||||
 */
 | 
			
		||||
export async function getRemoteUser(userId: mongo.ObjectID) {
 | 
			
		||||
	const user = await getUser(userId);
 | 
			
		||||
 | 
			
		||||
	if (!isRemoteUser(user)) {
 | 
			
		||||
		throw 'user is not a remote user';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return user;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get local user for API processing
 | 
			
		||||
 */
 | 
			
		||||
export async function getLocalUser(userId: mongo.ObjectID) {
 | 
			
		||||
	const user = await getUser(userId);
 | 
			
		||||
 | 
			
		||||
	if (!isLocalUser(user)) {
 | 
			
		||||
		throw 'user is not a local user';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return user;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										36
									
								
								src/server/api/endpoints/admin/update-remote-user.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/server/api/endpoints/admin/update-remote-user.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
import * as mongo from 'mongodb';
 | 
			
		||||
import $ from 'cafy';
 | 
			
		||||
import ID, { transform } from '../../../../misc/cafy-id';
 | 
			
		||||
import define from '../../define';
 | 
			
		||||
import { getRemoteUser } from '../../common/getters';
 | 
			
		||||
import { updatePerson } from '../../../../remote/activitypub/models/person';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	desc: {
 | 
			
		||||
		'ja-JP': '指定されたリモートユーザーの情報を更新します。',
 | 
			
		||||
		'en-US': 'Update specified remote user information.'
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requireModerator: true,
 | 
			
		||||
 | 
			
		||||
	params: {
 | 
			
		||||
		userId: {
 | 
			
		||||
			validator: $.type(ID),
 | 
			
		||||
			transform: transform,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': '対象のユーザーID',
 | 
			
		||||
				'en-US': 'The user ID which you want to update'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default define(meta, (ps) => new Promise((res, rej) => {
 | 
			
		||||
	updatePersonById(ps.userId).then(() => res(), e => rej(e));
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
async function updatePersonById(userId: mongo.ObjectID) {
 | 
			
		||||
	const user = await getRemoteUser(userId);
 | 
			
		||||
	await updatePerson(user.uri);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
 | 
			
		|||
import define from '../../define';
 | 
			
		||||
import User from '../../../../models/user';
 | 
			
		||||
import AbuseUserReport from '../../../../models/abuse-user-report';
 | 
			
		||||
import { publishAdminStream } from '../../../../stream';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	desc: {
 | 
			
		||||
| 
						 | 
				
			
			@ -47,12 +48,31 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 | 
			
		|||
		return rej('cannot report admin');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	await AbuseUserReport.insert({
 | 
			
		||||
	const report = await AbuseUserReport.insert({
 | 
			
		||||
		createdAt: new Date(),
 | 
			
		||||
		userId: user._id,
 | 
			
		||||
		reporterId: me._id,
 | 
			
		||||
		comment: ps.comment
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// Publish event to moderators
 | 
			
		||||
	setTimeout(async () => {
 | 
			
		||||
		const moderators = await User.find({
 | 
			
		||||
			$or: [{
 | 
			
		||||
				isAdmin: true
 | 
			
		||||
			}, {
 | 
			
		||||
				isModerator: true
 | 
			
		||||
			}]
 | 
			
		||||
		});
 | 
			
		||||
		for (const moderator of moderators) {
 | 
			
		||||
			publishAdminStream(moderator._id, 'newAbuseUserReport', {
 | 
			
		||||
				id: report._id,
 | 
			
		||||
				userId: report.userId,
 | 
			
		||||
				reporterId: report.reporterId,
 | 
			
		||||
				comment: report.comment
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}, 1);
 | 
			
		||||
 | 
			
		||||
	res();
 | 
			
		||||
}));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										16
									
								
								src/server/api/stream/channels/admin.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/server/api/stream/channels/admin.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
import autobind from 'autobind-decorator';
 | 
			
		||||
import Channel from '../channel';
 | 
			
		||||
 | 
			
		||||
export default class extends Channel {
 | 
			
		||||
	public readonly chName = 'admin';
 | 
			
		||||
	public static shouldShare = true;
 | 
			
		||||
	public static requireCredential = true;
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public async init(params: any) {
 | 
			
		||||
		// Subscribe admin stream
 | 
			
		||||
		this.subscriber.on(`adminStream:${this.user._id}`, data => {
 | 
			
		||||
			this.send(data);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import messagingIndex from './messaging-index';
 | 
			
		|||
import drive from './drive';
 | 
			
		||||
import hashtag from './hashtag';
 | 
			
		||||
import apLog from './ap-log';
 | 
			
		||||
import admin from './admin';
 | 
			
		||||
import gamesReversi from './games/reversi';
 | 
			
		||||
import gamesReversiGame from './games/reversi-game';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +29,7 @@ export default {
 | 
			
		|||
	drive,
 | 
			
		||||
	hashtag,
 | 
			
		||||
	apLog,
 | 
			
		||||
	admin,
 | 
			
		||||
	gamesReversi,
 | 
			
		||||
	gamesReversiGame
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -377,8 +377,10 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
 | 
			
		|||
 | 
			
		||||
			if (note.visibility == 'specified') {
 | 
			
		||||
				for (const u of visibleUsers) {
 | 
			
		||||
					publishHomeTimelineStream(u._id, detailPackedNote);
 | 
			
		||||
					publishHybridTimelineStream(u._id, detailPackedNote);
 | 
			
		||||
					if (!u._id.equals(user._id)) {
 | 
			
		||||
						publishHomeTimelineStream(u._id, detailPackedNote);
 | 
			
		||||
						publishHybridTimelineStream(u._id, detailPackedNote);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,12 +30,25 @@ export default async function(user: IUser, note: INote) {
 | 
			
		|||
			text: null,
 | 
			
		||||
			tags: [],
 | 
			
		||||
			fileIds: [],
 | 
			
		||||
			renoteId: null,
 | 
			
		||||
			poll: null,
 | 
			
		||||
			geo: null,
 | 
			
		||||
			cw: null
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (note.renoteId) {
 | 
			
		||||
		Note.update({ _id: note.renoteId }, {
 | 
			
		||||
			$inc: {
 | 
			
		||||
				renoteCount: -1,
 | 
			
		||||
				score: -1
 | 
			
		||||
			},
 | 
			
		||||
			$pull: {
 | 
			
		||||
				_quoteIds: note._id
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	publishNoteStream(note._id, 'deleted', {
 | 
			
		||||
		deletedAt: deletedAt
 | 
			
		||||
	});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,6 +87,10 @@ class Publisher {
 | 
			
		|||
	public publishApLogStream = (log: any): void => {
 | 
			
		||||
		this.publish('apLog', null, log);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public publishAdminStream = (userId: ID, type: string, value?: any): void => {
 | 
			
		||||
		this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const publisher = new Publisher();
 | 
			
		||||
| 
						 | 
				
			
			@ -107,3 +111,4 @@ export const publishHybridTimelineStream = publisher.publishHybridTimelineStream
 | 
			
		|||
export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
 | 
			
		||||
export const publishHashtagStream = publisher.publishHashtagStream;
 | 
			
		||||
export const publishApLogStream = publisher.publishApLogStream;
 | 
			
		||||
export const publishAdminStream = publisher.publishAdminStream;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,9 +24,7 @@ if (!acct.match(/^\w+@\w/)) {
 | 
			
		|||
console.log(`resync ${acct}`);
 | 
			
		||||
 | 
			
		||||
main(acct).then(() => {
 | 
			
		||||
	console.log('success');
 | 
			
		||||
	process.exit(0);
 | 
			
		||||
	console.log('Done');
 | 
			
		||||
}).catch(e => {
 | 
			
		||||
	console.warn(e);
 | 
			
		||||
	process.exit(1);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue