Compare commits
	
		
			28 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 468ff7037f | ||
|   | df23504ccf | ||
|   | 66e3cb8eda | ||
|   | 6ddd2389dc | ||
|   | 402efb8c50 | ||
|   | 7b6eae0ce4 | ||
|   | 26ce9725ce | ||
|   | ebfaa18f12 | ||
|   | cc81d41a05 | ||
|   | 212176ee5c | ||
|   | a63ec05e41 | ||
|   | 0dcb527bf3 | ||
|   | 54710f17fc | ||
|   | e58a6593c0 | ||
|   | 62132570e1 | ||
|   | 9f0b8ba2f8 | ||
|   | adbe0fbcd1 | ||
|   | 7896242f57 | ||
|   | 4a6722b9e9 | ||
|   | 7c9fb5228b | ||
|   | 81805b01cc | ||
|   | 50824a7245 | ||
|   | 6f2953f3a7 | ||
|   | dd3f007582 | ||
|   | a4b2b093fc | ||
|   | 0fbf56219f | ||
|   | 0acacf7a8e | ||
|   | c84500d914 | 
							
								
								
									
										126
									
								
								docs/setup.fr.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								docs/setup.fr.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| Guide d'installation et de configuration de Misskey | ||||
| ================================================================ | ||||
|  | ||||
| Nous vous remerçions de l'intrêt que vous manifestez pour l'installation de votre propre instance Misskey ! | ||||
| Ce guide décrit les étapes à suivre afin d'installer et de configurer une instance Misskey. | ||||
|  | ||||
| [La version en japonnais est également disponible sur - 日本語版もあります](./setup.ja.md) | ||||
|  | ||||
| ---------------------------------------------------------------- | ||||
|  | ||||
| *1.* Création de l'utilisateur Misskey | ||||
| ---------------------------------------------------------------- | ||||
| Lancer misskey en tant qu'utilisateur est une mauvaise idée, nous avons besoin de créer un utilisateur dédié. | ||||
| Sur Debian, à titre d'exemple : | ||||
|  | ||||
| ``` | ||||
| adduser --disabled-password --disabled-login misskey | ||||
| ``` | ||||
|  | ||||
| *2.* Installation des dépendances | ||||
| ---------------------------------------------------------------- | ||||
| Installez les paquets suivants : | ||||
|  | ||||
| #### Dépendences :package: | ||||
| * **[Node.js](https://nodejs.org/en/)** >= 10.0.0 | ||||
| * **[MongoDB](https://www.mongodb.com/)** >= 3.6 | ||||
|  | ||||
| ##### Optionnels | ||||
| * [Redis](https://redis.io/) | ||||
|   * Redis est optionnel mais nous vous recommandons vivement de l'installer | ||||
| * [Elasticsearch](https://www.elastic.co/) - requis pour pouvoir activer la fonctionnalité de recherche | ||||
|  | ||||
| *3.* Paramètrage de MongoDB | ||||
| ---------------------------------------------------------------- | ||||
| En mode root : | ||||
| 1. `mongo` Accédez au shell de mango | ||||
| 2. `use misskey` Utilisez la base de données misskey | ||||
| 3. `db.users.save( {dummy:"dummy"} )` Write dummy data to initialize the db. | ||||
| 4. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Créez l'utilisateur misskey. | ||||
| 5. `exit` Vous avez terminé ! | ||||
|  | ||||
| *4.* Installation de Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. `su - misskey` Basculez vers l'utilisateur misskey. | ||||
| 2. `git clone -b master git://github.com/syuilo/misskey.git` Clonez la branche master du dépôt misskey. | ||||
| 3. `cd misskey` Accédez au dossier misskey. | ||||
| 4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Télécharge la [version la plus récente](https://github.com/syuilo/misskey/releases/latest) | ||||
| 5. `npm install` Installez les dépendances de misskey. | ||||
|  | ||||
| *(optionnel)* Génération des clés VAPID | ||||
| ---------------------------------------------------------------- | ||||
| Si vous désirez activer ServiceWorker, vous devez générer les clés VAPID : | ||||
| Unless you have set your global node_modules location elsewhere, vous devez lancer ceci en mode root. | ||||
|  | ||||
| ``` shell | ||||
| npm install web-push -g | ||||
| web-push generate-vapid-keys | ||||
| ``` | ||||
|  | ||||
| *5.* Création du fichier de configuration | ||||
| ---------------------------------------------------------------- | ||||
| 1. `cp .config/example.yml .config/default.yml` Copiez le fichier `.config/example.yml` et renommez-le `default.yml`. | ||||
| 2. Editez le fichier `default.yml` | ||||
|  | ||||
| *6.* Construction de Misskey | ||||
| ---------------------------------------------------------------- | ||||
|  | ||||
| Construisez Misskey comme ceci : | ||||
|  | ||||
| `npm run build` | ||||
|  | ||||
| Si vous êtes sous Debian, vous serez amené à installer les paquets `build-essential`, `python`. | ||||
|  | ||||
| Si vous rencontrez des erreurs concernant certains modules, utilisez node-gyp: | ||||
|  | ||||
| 1. `npm install -g node-gyp` | ||||
| 2. `node-gyp configure` | ||||
| 3. `node-gyp build` | ||||
| 4. `npm run build` | ||||
|  | ||||
| *7.* C'est tout. | ||||
| ---------------------------------------------------------------- | ||||
| Excellent ! Maintenant, vous avez un environnement prêt pour lancer Misskey | ||||
|  | ||||
| ### Lancement conventionnel | ||||
| Lancez tout simplement `npm start`. Bonne chance et amusez-vous bien ! | ||||
|  | ||||
| ### Démarrage avec systemd | ||||
|  | ||||
| 1. Créez une service systemd sur : `/etc/systemd/system/misskey.service` | ||||
| 2. Editez-le puis copiez et coller ceci dans le fichier : | ||||
|  | ||||
| ``` | ||||
| [Unit] | ||||
| Description=Misskey daemon | ||||
|  | ||||
| [Service] | ||||
| Type=simple | ||||
| User=misskey | ||||
| ExecStart=/usr/bin/npm start | ||||
| WorkingDirectory=/home/misskey/misskey | ||||
| TimeoutSec=60 | ||||
| StandardOutput=syslog | ||||
| StandardError=syslog | ||||
| SyslogIdentifier=misskey | ||||
| Restart=always | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
| ``` | ||||
|  | ||||
| 3. `systemctl daemon-reload ; systemctl enable misskey` Redémarre systemd et active le service misskey. | ||||
| 4. `systemctl start misskey` Démarre le service misskey. | ||||
|  | ||||
| Vous pouvez vérifier si le service a démarré en utilisant la commande `systemctl status misskey`. | ||||
|  | ||||
| ### Méthode de mise à jour vers la plus récente version de Misskey | ||||
| 1. `git fetch` | ||||
| 2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` | ||||
| 3. `npm install` | ||||
| 4. `npm run build` | ||||
| 5. Consultez [ChangeLog](../CHANGELOG.md) pour les information de migration. | ||||
|  | ||||
| ---------------------------------------------------------------- | ||||
|  | ||||
| Si vous rencontrez des difficultés ou avez d'autres questions, n'hésitez pas à nous contacter ! | ||||
| @@ -379,6 +379,17 @@ common/views/components/poll-editor.vue: | ||||
| common/views/components/reaction-picker.vue: | ||||
|   choose-reaction: "リアクションを選択" | ||||
|  | ||||
| common/views/components/emoji-picker.vue: | ||||
|   custom-emoji: "カスタム絵文字" | ||||
|   people: "人" | ||||
|   animals-and-nature: "動物&自然" | ||||
|   food-and-drink: "食べ物&飲み物" | ||||
|   activity: "アクティビティ" | ||||
|   travel-and-places: "場所" | ||||
|   objects: "物" | ||||
|   symbols: "記号" | ||||
|   flags: "旗" | ||||
|  | ||||
| common/views/components/signin.vue: | ||||
|   username: "ユーザー名" | ||||
|   password: "パスワード" | ||||
| @@ -657,13 +668,6 @@ desktop/views/components/media-video.vue: | ||||
|   sensitive: "閲覧注意" | ||||
|   click-to-show: "クリックして表示" | ||||
|  | ||||
| desktop/views/components/follow-button.vue: | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   follow-processing: "フォロー処理中" | ||||
|   follow-request: "フォロー申請" | ||||
|  | ||||
| desktop/views/components/followers-window.vue: | ||||
|   followers: "{} のフォロワー" | ||||
|  | ||||
| @@ -1325,7 +1329,7 @@ mobile/views/components/media-video.vue: | ||||
|   sensitive: "閲覧注意" | ||||
|   click-to-show: "クリックして表示" | ||||
|  | ||||
| mobile/views/components/follow-button.vue: | ||||
| common/views/components/follow-button.vue: | ||||
|   following: "フォロー中" | ||||
|   follow: "フォロー" | ||||
|   request-pending: "フォロー許可待ち" | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <i@syuilo.com>", | ||||
| 	"version": "10.48.0", | ||||
| 	"clientVersion": "2.0.11733", | ||||
| 	"version": "10.49.2", | ||||
| 	"clientVersion": "2.0.11761", | ||||
| 	"codename": "nighthike", | ||||
| 	"main": "./built/index.js", | ||||
| 	"private": true, | ||||
| @@ -198,6 +198,7 @@ | ||||
| 		"summaly": "2.2.0", | ||||
| 		"systeminformation": "3.47.0", | ||||
| 		"syuilo-password-strength": "0.0.1", | ||||
| 		"terser-webpack-plugin": "1.1.0", | ||||
| 		"textarea-caret": "3.1.0", | ||||
| 		"tinycolor2": "1.4.1", | ||||
| 		"tmp": "0.0.33", | ||||
|   | ||||
| @@ -66,19 +66,6 @@ export default abstract class Chart<T> { | ||||
| 		} else { | ||||
| 			this.collection.createIndex({ span: -1, date: -1 }, { unique: true }); | ||||
| 		} | ||||
|  | ||||
| 		//#region 後方互換性のため | ||||
| 		this.collection.find({ span: 'day' }, { fields: { _id: true, date: true } }).then(logs => { | ||||
| 			logs.forEach(log => { | ||||
| 				this.collection.update({ _id: log._id }, { $set: { date: utc(log.date).hour(0).toDate() } }); | ||||
| 			}); | ||||
| 		}); | ||||
| 		this.collection.find({ span: 'hour' }, { fields: { _id: true, date: true } }).then(logs => { | ||||
| 			logs.forEach(log => { | ||||
| 				this.collection.update({ _id: log._id }, { $set: { date: utc(log.date).toDate() } }); | ||||
| 			}); | ||||
| 		}); | ||||
| 		//#endregion | ||||
| 	} | ||||
|  | ||||
| 	@autobind | ||||
|   | ||||
| @@ -9,14 +9,11 @@ import './style.styl'; | ||||
|  | ||||
| import init from '../init'; | ||||
| import Index from './views/index.vue'; | ||||
| import * as config from '../config'; | ||||
|  | ||||
| /** | ||||
|  * init | ||||
|  */ | ||||
| init(launch => { | ||||
| 	document.title = `${config.name} | %i18n:common.application-authorization%`; | ||||
|  | ||||
| 	// Init router | ||||
| 	const router = new VueRouter({ | ||||
| 		mode: 'history', | ||||
|   | ||||
| @@ -146,6 +146,8 @@ | ||||
| 	function refresh() { | ||||
| 		localStorage.setItem('shouldFlush', 'false'); | ||||
|  | ||||
| 		localStorage.removeItem('locale'); | ||||
|  | ||||
| 		// Random | ||||
| 		localStorage.setItem('salt', Math.random().toString().substr(2, 8)); | ||||
|  | ||||
|   | ||||
| @@ -66,7 +66,7 @@ export default function<T extends object>(data: { | ||||
|  | ||||
| 				this.bakeProps(); | ||||
|  | ||||
| 				(this as any).api('i/update_widget', { | ||||
| 				this.$root.api('i/update_widget', { | ||||
| 					id: this.id, | ||||
| 					data: this.props | ||||
| 				}); | ||||
|   | ||||
							
								
								
									
										202
									
								
								src/client/app/common/views/components/emoji-picker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								src/client/app/common/views/components/emoji-picker.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | ||||
| <template> | ||||
| <div class="prlncendiewqqkrevzeruhndoakghvtx"> | ||||
| 	<header> | ||||
| 		<button v-for="category in categories" | ||||
| 			:title="category.text" | ||||
| 			@click="go(category.ref)" | ||||
| 			:class="{ active: category.isActive }" | ||||
| 		> | ||||
| 			<fa :icon="category.icon" fixed-width/> | ||||
| 		</button> | ||||
| 	</header> | ||||
| 	<div class="emojis" ref="emojis" @scroll.passive="onScroll"> | ||||
| 		<section v-for="category in categories" :ref="category.ref"> | ||||
| 			<header><fa :icon="category.icon" fixed-width/> {{ category.text }}</header> | ||||
| 			<div v-if="category.name"> | ||||
| 				<button v-for="emoji in Object.entries(lib).filter(([k, v]) => v.category === category.name)" | ||||
| 					:title="emoji[0]" | ||||
| 					@click="chosen(emoji[1].char)" | ||||
| 				> | ||||
| 					<mk-emoji :emoji="emoji[1].char"/> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 			<div v-else> | ||||
| 				<button v-for="emoji in customEmojis" | ||||
| 					:title="emoji.name" | ||||
| 					@click="chosen(`:${emoji.name}:`)" | ||||
| 				> | ||||
| 					<img :src="emoji.url" :alt="emoji.name"/> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</section> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import { lib } from 'emojilib'; | ||||
| import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faHeart, faFlag } from '@fortawesome/free-regular-svg-icons'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/emoji-picker.vue'), | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			lib, | ||||
| 			customEmojis: [], | ||||
| 			categories: [{ | ||||
| 				ref: 'customEmojiSection', | ||||
| 				text: this.$t('custom-emoji'), | ||||
| 				icon: faAsterisk, | ||||
| 				isActive: true | ||||
| 			}, { | ||||
| 				name: 'people', | ||||
| 				ref: 'peopleSection', | ||||
| 				text: this.$t('people'), | ||||
| 				icon: ['far', 'laugh'], | ||||
| 				isActive: false | ||||
| 			}, { | ||||
| 				name: 'animals_and_nature', | ||||
| 				ref: 'animalsAndNatureSection', | ||||
| 				text: this.$t('animals-and-nature'), | ||||
| 				icon: faLeaf, | ||||
| 				isActive: false | ||||
| 			}, { | ||||
| 				name: 'food_and_drink', | ||||
| 				ref: 'foodAndDrinkSection', | ||||
| 				text: this.$t('food-and-drink'), | ||||
| 				icon: faUtensils, | ||||
| 				isActive: false | ||||
| 			}, { | ||||
| 				name: 'activity', | ||||
| 				ref: 'activitySection', | ||||
| 				text: this.$t('activity'), | ||||
| 				icon: faFutbol, | ||||
| 				isActive: false | ||||
| 			}, { | ||||
| 				name: 'travel_and_places', | ||||
| 				ref: 'travelAndPlacesSection', | ||||
| 				text: this.$t('travel-and-places'), | ||||
| 				icon: faCity, | ||||
| 				isActive: false | ||||
| 			}, { | ||||
| 				name: 'objects', | ||||
| 				ref: 'objectsSection', | ||||
| 				text: this.$t('objects'), | ||||
| 				icon: faDice, | ||||
| 				isActive: false | ||||
| 			}, { | ||||
| 				name: 'symbols', | ||||
| 				ref: 'symbolsSection', | ||||
| 				text: this.$t('symbols'), | ||||
| 				icon: faHeart, | ||||
| 				isActive: false | ||||
| 			}, { | ||||
| 				name: 'flags', | ||||
| 				ref: 'flagsSection', | ||||
| 				text: this.$t('flags'), | ||||
| 				icon: faFlag, | ||||
| 				isActive: false | ||||
| 			}] | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || []; | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		go(ref) { | ||||
| 			this.$refs.emojis.scrollTop = this.$refs[ref][0].offsetTop; | ||||
| 		}, | ||||
|  | ||||
| 		onScroll(e) { | ||||
| 			const section = this.categories.forEach(x => { | ||||
| 				const top = e.target.scrollTop; | ||||
| 				const el = this.$refs[x.ref][0]; | ||||
| 				x.isActive = el.offsetTop <= top && el.offsetTop + el.offsetHeight > top; | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		chosen(emoji) { | ||||
| 			this.$emit('chosen', emoji); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .prlncendiewqqkrevzeruhndoakghvtx | ||||
| 	width 350px | ||||
| 	background var(--face) | ||||
|  | ||||
| 	> header | ||||
| 		display flex | ||||
|  | ||||
| 		> button | ||||
| 			flex 1 | ||||
| 			padding 10px 0 | ||||
| 			font-size 16px | ||||
| 			color var(--text) | ||||
| 			transition color 0.2s ease | ||||
|  | ||||
| 			&:hover | ||||
| 				color var(--textHighlighted) | ||||
| 				transition color 0s | ||||
|  | ||||
| 			&.active | ||||
| 				color var(--primary) | ||||
| 				transition color 0s | ||||
|  | ||||
| 	> .emojis | ||||
| 		height 300px | ||||
| 		overflow-y auto | ||||
| 		overflow-x hidden | ||||
|  | ||||
| 		> section | ||||
| 			> header | ||||
| 				position sticky | ||||
| 				top 0 | ||||
| 				left 0 | ||||
| 				z-index 1 | ||||
| 				padding 8px | ||||
| 				background var(--faceHeader) | ||||
| 				color var(--text) | ||||
| 				font-size 12px | ||||
|  | ||||
| 			> div | ||||
| 				display grid | ||||
| 				grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr | ||||
| 				gap 4px | ||||
| 				padding 8px | ||||
|  | ||||
| 				> button | ||||
| 					padding 0 | ||||
| 					width 100% | ||||
|  | ||||
| 					&:before | ||||
| 						content '' | ||||
| 						display block | ||||
| 						width 1px | ||||
| 						height 0 | ||||
| 						padding-bottom 100% | ||||
|  | ||||
| 					&:hover | ||||
| 						> * | ||||
| 							transform scale(1.2) | ||||
| 							transition transform 0s | ||||
|  | ||||
| 					> * | ||||
| 						position absolute | ||||
| 						top 0 | ||||
| 						left 0 | ||||
| 						width 100% | ||||
| 						height 100% | ||||
| 						font-size 28px | ||||
| 						transition transform 0.2s ease | ||||
| 						pointer-events none | ||||
|  | ||||
| </style> | ||||
| @@ -22,7 +22,7 @@ export default Vue.extend({ | ||||
| 		}, | ||||
| 		customEmojis: { | ||||
| 			required: false, | ||||
| 			default: [] | ||||
| 			default: () => [] | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
|   | ||||
							
								
								
									
										184
									
								
								src/client/app/common/views/components/follow-button.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								src/client/app/common/views/components/follow-button.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | ||||
| <template> | ||||
| <button class="wfliddvnhxvyusikowhxozkyxyenqxqr" | ||||
| 	:class="{ wait, block, mini, active: isFollowing || hasPendingFollowRequestFromYou }" | ||||
| 	@click="onClick" | ||||
| 	:disabled="wait" | ||||
| > | ||||
| 	<template v-if="!wait"> | ||||
| 		<fa :icon="iconAndText[0]"/> <template v-if="!mini">{{ iconAndText[1] }}</template> | ||||
| 	</template> | ||||
| 	<template v-else><fa icon="spinner" pulse fixed-width/></template> | ||||
| </button> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/follow-button.vue'), | ||||
|  | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		block: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		mini: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			isFollowing: this.user.isFollowing, | ||||
| 			hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou, | ||||
| 			wait: false, | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		iconAndText(): any[] { | ||||
| 			return ( | ||||
| 				(this.hasPendingFollowRequestFromYou && this.user.isLocked) ? ['hourglass-half', this.$t('request-pending')] : | ||||
| 				(this.hasPendingFollowRequestFromYou && !this.user.isLocked) ? ['hourglass-start', this.$t('follow-processing')] : | ||||
| 				(this.isFollowing) ? ['minus', this.$t('following')] : | ||||
| 				(!this.isFollowing && this.user.isLocked) ? ['plus', this.$t('follow-request')] : | ||||
| 				(!this.isFollowing && !this.user.isLocked) ? ['plus', this.$t('follow')] : | ||||
| 				[] | ||||
| 			); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.connection = this.$root.stream.useSharedConnection('main'); | ||||
|  | ||||
| 		this.connection.on('follow', this.onFollowChange); | ||||
| 		this.connection.on('unfollow', this.onFollowChange); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		onFollowChange(user) { | ||||
| 			if (user.id == this.user.id) { | ||||
| 				this.isFollowing = user.isFollowing; | ||||
| 				this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		async onClick() { | ||||
| 			this.wait = true; | ||||
|  | ||||
| 			try { | ||||
| 				if (this.isFollowing) { | ||||
| 					await this.$root.api('following/delete', { | ||||
| 						userId: this.user.id | ||||
| 					}); | ||||
| 				} else { | ||||
| 					if (this.hasPendingFollowRequestFromYou) { | ||||
| 						await this.$root.api('following/requests/cancel', { | ||||
| 							userId: this.user.id | ||||
| 						}); | ||||
| 					} else if (this.user.isLocked) { | ||||
| 						await this.$root.api('following/create', { | ||||
| 							userId: this.user.id | ||||
| 						}); | ||||
| 						this.hasPendingFollowRequestFromYou = true; | ||||
| 					} else { | ||||
| 						await this.$root.api('following/create', { | ||||
| 							userId: this.user.id | ||||
| 						}); | ||||
| 						this.hasPendingFollowRequestFromYou = true; | ||||
| 					} | ||||
| 				} | ||||
| 			} catch (e) { | ||||
| 				console.error(e); | ||||
| 			} finally { | ||||
| 				this.wait = false; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .wfliddvnhxvyusikowhxozkyxyenqxqr | ||||
| 	display block | ||||
| 	user-select none | ||||
| 	cursor pointer | ||||
| 	padding 0 16px | ||||
| 	margin 0 | ||||
| 	min-width 100px | ||||
| 	line-height 36px | ||||
| 	font-size 14px | ||||
| 	font-weight bold | ||||
| 	color var(--primary) | ||||
| 	background transparent | ||||
| 	outline none | ||||
| 	border solid 1px var(--primary) | ||||
| 	border-radius 36px | ||||
|  | ||||
| 	&.mini | ||||
| 		padding 0 | ||||
| 		min-width 0 | ||||
| 		width 32px | ||||
| 		height 32px | ||||
| 		font-size 16px | ||||
| 		border-radius 4px | ||||
| 		line-height 32px | ||||
|  | ||||
| 		&:focus | ||||
| 			&:after | ||||
| 				border-radius 8px | ||||
|  | ||||
| 	&.block | ||||
| 		width 100% | ||||
|  | ||||
| 	&:focus | ||||
| 		&:after | ||||
| 			content "" | ||||
| 			pointer-events none | ||||
| 			position absolute | ||||
| 			top -5px | ||||
| 			right -5px | ||||
| 			bottom -5px | ||||
| 			left -5px | ||||
| 			border 2px solid var(--primaryAlpha03) | ||||
| 			border-radius 36px | ||||
|  | ||||
| 	&:hover | ||||
| 		background var(--primaryAlpha01) | ||||
|  | ||||
| 	&:active | ||||
| 		background var(--primaryAlpha02) | ||||
|  | ||||
| 	&.active | ||||
| 		color var(--primaryForeground) | ||||
| 		background var(--primary) | ||||
|  | ||||
| 		&:hover | ||||
| 			background var(--primaryLighten10) | ||||
| 			border-color var(--primaryLighten10) | ||||
|  | ||||
| 		&:active | ||||
| 			background var(--primaryDarken10) | ||||
| 			border-color var(--primaryDarken10) | ||||
|  | ||||
| 	&.wait | ||||
| 		cursor wait !important | ||||
| 		opacity 0.7 | ||||
|  | ||||
| 	* | ||||
| 		pointer-events none | ||||
|  | ||||
| </style> | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="mk-github-setting"> | ||||
| 	<p>{{ $t('description') }}<a :href="`${docsUrl}/link-to-github`" target="_blank">{{ $t('detail') }}</a></p> | ||||
| 	<p>{{ $t('description') }}</p> | ||||
| 	<p class="account" v-if="$store.state.i.github" :title="`GitHub ID: ${$store.state.i.github.id}`">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p> | ||||
| 	<p> | ||||
| 		<a :href="`${apiUrl}/connect/github`" target="_blank" @click.prevent="connect">{{ $store.state.i.github ? this.$t('reconnect') : this.$t('connect') }}</a> | ||||
| @@ -14,15 +14,14 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import { apiUrl, docsUrl } from '../../../config'; | ||||
| import { apiUrl } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/github-setting.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			form: null, | ||||
| 			apiUrl, | ||||
| 			docsUrl | ||||
| 			apiUrl | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
|   | ||||
| @@ -1,13 +1,8 @@ | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| import muteAndBlock from './mute-and-block.vue'; | ||||
| import followButton from './follow-button.vue'; | ||||
| import error from './error.vue'; | ||||
| import apiSettings from './api-settings.vue'; | ||||
| import passwordSettings from './password-settings.vue'; | ||||
| import driveSettings from './drive-settings.vue'; | ||||
| import profileEditor from './profile-editor.vue'; | ||||
| import noteSkeleton from './note-skeleton.vue'; | ||||
| import theme from './theme.vue'; | ||||
| import instance from './instance.vue'; | ||||
| import cwButton from './cw-button.vue'; | ||||
| import tagCloud from './tag-cloud.vue'; | ||||
| @@ -27,7 +22,6 @@ import pollEditor from './poll-editor.vue'; | ||||
| import reactionIcon from './reaction-icon.vue'; | ||||
| import reactionsViewer from './reactions-viewer.vue'; | ||||
| import time from './time.vue'; | ||||
| import timer from './timer.vue'; | ||||
| import mediaList from './media-list.vue'; | ||||
| import uploader from './uploader.vue'; | ||||
| import streamIndicator from './stream-indicator.vue'; | ||||
| @@ -51,14 +45,9 @@ import uiInfo from './ui/info.vue'; | ||||
| import formButton from './ui/form/button.vue'; | ||||
| import formRadio from './ui/form/radio.vue'; | ||||
|  | ||||
| Vue.component('mk-mute-and-block', muteAndBlock); | ||||
| Vue.component('mk-follow-button', followButton); | ||||
| Vue.component('mk-error', error); | ||||
| Vue.component('mk-api-settings', apiSettings); | ||||
| Vue.component('mk-password-settings', passwordSettings); | ||||
| Vue.component('mk-drive-settings', driveSettings); | ||||
| Vue.component('mk-profile-editor', profileEditor); | ||||
| Vue.component('mk-note-skeleton', noteSkeleton); | ||||
| Vue.component('mk-theme', theme); | ||||
| Vue.component('mk-instance', instance); | ||||
| Vue.component('mk-cw-button', cwButton); | ||||
| Vue.component('mk-tag-cloud', tagCloud); | ||||
| @@ -78,7 +67,6 @@ Vue.component('mk-poll-editor', pollEditor); | ||||
| Vue.component('mk-reaction-icon', reactionIcon); | ||||
| Vue.component('mk-reactions-viewer', reactionsViewer); | ||||
| Vue.component('mk-time', time); | ||||
| Vue.component('mk-timer', timer); | ||||
| Vue.component('mk-media-list', mediaList); | ||||
| Vue.component('mk-uploader', uploader); | ||||
| Vue.component('mk-stream-indicator', streamIndicator); | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| 		<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ $t('empty') }}</p> | ||||
| 		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa icon="flag"/>{{ $t('no-history') }}</p> | ||||
| 		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> | ||||
| 			<template v-if="fetchingMoreMessages"><fa icon="spinner .pulse" fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }} | ||||
| 			<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }} | ||||
| 		</button> | ||||
| 		<template v-for="(message, i) in _messages"> | ||||
| 			<x-message :message="message" :key="message.id"/> | ||||
|   | ||||
| @@ -45,7 +45,7 @@ | ||||
| 		</template> | ||||
| 	</div> | ||||
| 	<p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
| 			<span>{{ $t('username') }}</span> | ||||
| 			<span slot="prefix">@</span> | ||||
| 			<span slot="suffix">@{{ host }}</span> | ||||
| 			<p slot="desc" v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner .pulse" fixed-width/> {{ $t('checking') }}</p> | ||||
| 			<p slot="desc" v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner" pulse fixed-width/> {{ $t('checking') }}</p> | ||||
| 			<p slot="desc" v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('available') }}</p> | ||||
| 			<p slot="desc" v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('unavailable') }}</p> | ||||
| 			<p slot="desc" v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('error') }}</p> | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| <template> | ||||
| <div class="mk-stream-indicator"> | ||||
| 	<p v-if="stream.state == 'initializing'"> | ||||
| 		<fa icon="spinner .pulse"/> | ||||
| 		<fa icon="spinner" pulse/> | ||||
| 		<span>{{ $t('connecting') }}<mk-ellipsis/></span> | ||||
| 	</p> | ||||
| 	<p v-if="stream.state == 'reconnecting'"> | ||||
| 		<fa icon="spinner .pulse"/> | ||||
| 		<fa icon="spinner" pulse/> | ||||
| 		<span>{{ $t('reconnecting') }}<mk-ellipsis/></span> | ||||
| 	</p> | ||||
| 	<p v-if="stream.state == 'connected'"> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="jtivnzhfwquxpsfidertopbmwmchmnmo"> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="empty" v-else-if="tags.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p> | ||||
| 	<div v-else> | ||||
| 		<vue-word-cloud | ||||
|   | ||||
| @@ -1,49 +0,0 @@ | ||||
| <template> | ||||
| <time class="mk-time"> | ||||
| 	{{ hh }}:{{ mm }}:{{ ss }} | ||||
| </time> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		time: { | ||||
| 			type: [Date, String], | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			tickId: null, | ||||
| 			hh: null, | ||||
| 			mm: null, | ||||
| 			ss: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		_time(): Date { | ||||
| 			return typeof this.time == 'string' ? new Date(this.time) : this.time; | ||||
| 		} | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.tick(); | ||||
| 		this.tickId = setInterval(this.tick, 1000); | ||||
| 	}, | ||||
| 	destroyed() { | ||||
| 		clearInterval(this.tickId); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		tick() { | ||||
| 			const now = new Date().getTime(); | ||||
| 			const start = this._time.getTime(); | ||||
| 			const ago = Math.floor((now - start) / 1000); | ||||
|  | ||||
| 			this.hh = Math.floor(ago / (60 * 60)).toString().padStart(2, '0'); | ||||
| 			this.mm = Math.floor(ago / 60).toString().padStart(2, '0'); | ||||
| 			this.ss = (ago % 60).toString().padStart(2, '0'); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc"> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p> | ||||
| 	<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||
| 	<transition-group v-else tag="div" name="chart"> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="mk-twitter-setting"> | ||||
| 	<p>{{ $t('description') }}<a :href="`${docsUrl}/link-to-twitter`" target="_blank">{{ $t('detail') }}</a></p> | ||||
| 	<p>{{ $t('description') }}</p> | ||||
| 	<p class="account" v-if="$store.state.i.twitter" :title="`Twitter ID: ${$store.state.i.twitter.userId}`">{{ $t('connected-to') }}: <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" @click.prevent="connect">{{ $store.state.i.twitter ? this.$t('reconnect') : this.$t('connect') }}</a> | ||||
| @@ -14,15 +14,14 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import { apiUrl, docsUrl } from '../../../config'; | ||||
| import { apiUrl } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/twitter-setting.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			form: null, | ||||
| 			apiUrl, | ||||
| 			docsUrl | ||||
| 			apiUrl | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| 	<ol v-if="uploads.length > 0"> | ||||
| 		<li v-for="ctx in uploads" :key="ctx.id"> | ||||
| 			<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div> | ||||
| 			<p class="name"><fa icon="spinner .pulse"/>{{ ctx.name }}</p> | ||||
| 			<p class="name"><fa icon="spinner" pulse/>{{ ctx.name }}</p> | ||||
| 			<p class="status"> | ||||
| 				<span class="initing" v-if="ctx.progress == undefined">{{ $t('waiting') }}<mk-ellipsis/></span> | ||||
| 				<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span> | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
| 			<template v-else-if="!user.isFollowing && user.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template> | ||||
| 			<template v-else-if="!user.isFollowing && !user.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template> | ||||
| 		</template> | ||||
| 		<template v-else><fa icon="spinner .pulse" fixed-width/></template> | ||||
| 		<template v-else><fa icon="spinner" pulse fixed-width/></template> | ||||
| 	</button> | ||||
| </div> | ||||
| </template> | ||||
|   | ||||
| @@ -3,9 +3,15 @@ | ||||
| 	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2"> | ||||
| 		<template slot="header"><fa icon="camera"/>{{ $t('title') }}</template> | ||||
|  | ||||
| 		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 		<div :class="$style.stream" v-if="!fetching && images.length > 0"> | ||||
| 			<div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.thumbnailUrl || image.url})`"></div> | ||||
| 			<div v-for="image in images" | ||||
| 				:class="$style.img" | ||||
| 				:style="`background-image: url(${image.thumbnailUrl || image.url})`" | ||||
| 				draggable="true" | ||||
| 				@dragstart="onDragstart(image, $event)" | ||||
| 				@dragend="onDragend" | ||||
| 			></div> | ||||
| 		</div> | ||||
| 		<p :class="$style.empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p> | ||||
| 	</mk-widget-container> | ||||
| @@ -31,6 +37,7 @@ export default define({ | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.connection = this.$root.stream.useSharedConnection('main'); | ||||
|  | ||||
| @@ -44,9 +51,11 @@ export default define({ | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		onDriveFileCreated(file) { | ||||
| 			if (/^image\/.+$/.test(file.type)) { | ||||
| @@ -54,6 +63,7 @@ export default define({ | ||||
| 				if (this.images.length > 9) this.images.pop(); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		func() { | ||||
| 			if (this.props.design == 2) { | ||||
| 				this.props.design = 0; | ||||
| @@ -62,7 +72,16 @@ export default define({ | ||||
| 			} | ||||
|  | ||||
| 			this.save(); | ||||
| 		} | ||||
| 		}, | ||||
|  | ||||
| 		onDragstart(file, e) { | ||||
| 			e.dataTransfer.effectAllowed = 'move'; | ||||
| 			e.dataTransfer.setData('mk_drive_file', JSON.stringify(file)); | ||||
| 		}, | ||||
|  | ||||
| 		onDragend(e) { | ||||
| 			this.browser.isDragSource = false; | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| 		<button slot="func" title="設定" @click="setting"><fa icon="cog"/></button> | ||||
|  | ||||
| 		<div class="mkw-rss--body" :data-mobile="platform == 'mobile'"> | ||||
| 			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 			<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 			<div class="feed" v-else> | ||||
| 				<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a> | ||||
| 			</div> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <div class="memory"> | ||||
| 	<x-pie class="pie" :value="usage"/> | ||||
| 	<div> | ||||
| 		<p><fa icon="flask"/>Memory</p> | ||||
| 		<p><fa icon="memory"/>Memory</p> | ||||
| 		<p>Total: {{ total | bytes(1) }}</p> | ||||
| 		<p>Used: {{ used | bytes(1) }}</p> | ||||
| 		<p>Free: {{ free | bytes(1) }}</p> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 		<template slot="header"><fa icon="server"/>{{ $t('title') }}</template> | ||||
| 		<button slot="func" @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button> | ||||
|  | ||||
| 		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 		<template v-if="!fetching"> | ||||
| 			<x-cpu-memory v-show="props.view == 0" :connection="connection"/> | ||||
| 			<x-cpu v-show="props.view == 1" :connection="connection" :meta="meta"/> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 		<template slot="header"><fa icon="chart-bar"/>{{ $t('title') }}</template> | ||||
| 		<button slot="func" :title="$t('toggle')" @click="toggle"><fa icon="sort"/></button> | ||||
|  | ||||
| 		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 		<template v-else> | ||||
| 			<x-calendar v-show="view == 0" :data="[].concat(activity)"/> | ||||
| 			<x-chart v-show="view == 1" :data="[].concat(activity)"/> | ||||
|   | ||||
| @@ -0,0 +1,84 @@ | ||||
| <template> | ||||
| <div class="gcafiosrssbtbnbzqupfmglvzgiaipyv"> | ||||
| 	<x-picker @chosen="chosen"/> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import contains from '../../../common/scripts/contains'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XPicker: () => import('../../../common/views/components/emoji-picker.vue').then(m => m.default) | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		x: { | ||||
| 			type: Number, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		y: { | ||||
| 			type: Number, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.$nextTick(() => { | ||||
| 			const width = this.$el.offsetWidth; | ||||
| 			const height = this.$el.offsetHeight; | ||||
|  | ||||
| 			let x = this.x; | ||||
| 			let y = this.y; | ||||
|  | ||||
| 			if (x + width - window.pageXOffset > window.innerWidth) { | ||||
| 				x = window.innerWidth - width + window.pageXOffset; | ||||
| 			} | ||||
|  | ||||
| 			if (y + height - window.pageYOffset > window.innerHeight) { | ||||
| 				y = window.innerHeight - height + window.pageYOffset; | ||||
| 			} | ||||
|  | ||||
| 			this.$el.style.left = x + 'px'; | ||||
| 			this.$el.style.top = y + 'px'; | ||||
|  | ||||
| 			Array.from(document.querySelectorAll('body *')).forEach(el => { | ||||
| 				el.addEventListener('mousedown', this.onMousedown); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		onMousedown(e) { | ||||
| 			e.preventDefault(); | ||||
| 			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); | ||||
| 			return false; | ||||
| 		}, | ||||
|  | ||||
| 		chosen(emoji) { | ||||
| 			this.$emit('chosen', emoji); | ||||
| 			this.close(); | ||||
| 		}, | ||||
|  | ||||
| 		close() { | ||||
| 			Array.from(document.querySelectorAll('body *')).forEach(el => { | ||||
| 				el.removeEventListener('mousedown', this.onMousedown); | ||||
| 			}); | ||||
|  | ||||
| 			this.$emit('closed'); | ||||
| 			this.destroyDom(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .gcafiosrssbtbnbzqupfmglvzgiaipyv | ||||
| 	position fixed | ||||
| 	top 0 | ||||
| 	left 0 | ||||
| 	z-index 3000 | ||||
| 	box-shadow 0 2px 12px 0 rgba(0, 0, 0, 0.3) | ||||
|  | ||||
| </style> | ||||
| @@ -1,157 +0,0 @@ | ||||
| <template> | ||||
| <button class="mk-follow-button" | ||||
| 	:class="{ wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou, big: size == 'big' }" | ||||
| 	@click="onClick" | ||||
| 	:disabled="wait" | ||||
| > | ||||
| 	<template v-if="!wait"> | ||||
| 		<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked"><fa icon="hourglass-half"/><template v-if="size == 'big'"> {{ $t('request-pending') }}</template></template> | ||||
| 		<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked"><fa icon="hourglass-start"/><template v-if="size == 'big'"> {{ $t('follow-processing') }}</template></template> | ||||
| 		<template v-else-if="u.isFollowing"><fa icon="minus"/><template v-if="size == 'big'"> {{ $t('following') }}</template></template> | ||||
| 		<template v-else-if="!u.isFollowing && u.isLocked"><fa icon="plus"/><template v-if="size == 'big'"> {{ $t('follow-request') }}</template></template> | ||||
| 		<template v-else-if="!u.isFollowing && !u.isLocked"><fa icon="plus"/><template v-if="size == 'big'"> {{ $t('follow') }}</template></template> | ||||
| 	</template> | ||||
| 	<template v-else><fa icon="spinner .pulse" fixed-width/></template> | ||||
| </button> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/follow-button.vue'), | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		size: { | ||||
| 			type: String, | ||||
| 			default: 'compact' | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			u: this.user, | ||||
| 			wait: false, | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.connection = this.$root.stream.useSharedConnection('main'); | ||||
| 		this.connection.on('follow', this.onFollowChange); | ||||
| 		this.connection.on('unfollow', this.onFollowChange); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		onFollowChange(user) { | ||||
| 			if (user.id == this.u.id) { | ||||
| 				this.u.isFollowing = user.isFollowing; | ||||
| 				this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; | ||||
| 				this.$forceUpdate(); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		async onClick() { | ||||
| 			this.wait = true; | ||||
|  | ||||
| 			try { | ||||
| 				if (this.u.isFollowing) { | ||||
| 					this.u = await this.$root.api('following/delete', { | ||||
| 						userId: this.u.id | ||||
| 					}); | ||||
| 				} else { | ||||
| 					if (this.u.hasPendingFollowRequestFromYou) { | ||||
| 						this.u = await this.$root.api('following/requests/cancel', { | ||||
| 							userId: this.u.id | ||||
| 						}); | ||||
| 					} else if (this.u.isLocked) { | ||||
| 						this.u = await this.$root.api('following/create', { | ||||
| 							userId: this.u.id | ||||
| 						}); | ||||
| 					} else { | ||||
| 						this.u = await this.$root.api('following/create', { | ||||
| 							userId: this.user.id | ||||
| 						}); | ||||
| 					} | ||||
| 				} | ||||
| 			} catch (e) { | ||||
| 				console.error(e); | ||||
| 			} finally { | ||||
| 				this.wait = false; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-follow-button | ||||
| 	display block | ||||
| 	cursor pointer | ||||
| 	padding 0 | ||||
| 	margin 0 | ||||
| 	width 32px | ||||
| 	height 32px | ||||
| 	font-size 1em | ||||
| 	outline none | ||||
| 	border-radius 4px | ||||
|  | ||||
| 	* | ||||
| 		pointer-events none | ||||
|  | ||||
| 	&:focus | ||||
| 		&:after | ||||
| 			content "" | ||||
| 			pointer-events none | ||||
| 			position absolute | ||||
| 			top -5px | ||||
| 			right -5px | ||||
| 			bottom -5px | ||||
| 			left -5px | ||||
| 			border 2px solid var(--primaryAlpha03) | ||||
| 			border-radius 8px | ||||
|  | ||||
| 	&:not(.active) | ||||
| 		color var(--primary) | ||||
| 		border solid 1px var(--primary) | ||||
|  | ||||
| 		&:hover | ||||
| 			background var(--primaryAlpha03) | ||||
|  | ||||
| 		&:active | ||||
| 			background var(--primaryAlpha05) | ||||
|  | ||||
| 	&.active | ||||
| 		color var(--primaryForeground) | ||||
| 		background var(--primary) | ||||
| 		border solid 1px var(--primary) | ||||
|  | ||||
| 		&:not(:disabled) | ||||
| 			font-weight bold | ||||
|  | ||||
| 		&:hover:not(:disabled) | ||||
| 			background var(--primaryLighten5) | ||||
| 			border-color var(--primaryLighten5) | ||||
|  | ||||
| 		&:active:not(:disabled) | ||||
| 			background var(--primaryDarken5) | ||||
| 			border-color var(--primaryDarken5) | ||||
|  | ||||
| 	&.wait | ||||
| 		cursor wait !important | ||||
| 		opacity 0.7 | ||||
|  | ||||
| 	&.big | ||||
| 		width 100% | ||||
| 		height 38px | ||||
| 		line-height 38px | ||||
|  | ||||
| </style> | ||||
| @@ -11,7 +11,7 @@ | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<p class="empty" v-if="!fetching && users.length == 0">{{ $t('empty') }}</p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('fetching') }}<mk-ellipsis/></p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('fetching') }}<mk-ellipsis/></p> | ||||
| 	<a class="refresh" @click="refresh">{{ $t('refresh') }}</a> | ||||
| 	<button class="close" @click="destroyDom()" :title="$t('title')"><fa icon="times"/></button> | ||||
| </div> | ||||
|   | ||||
| @@ -14,10 +14,8 @@ import mediaVideo from './media-video.vue'; | ||||
| import notifications from './notifications.vue'; | ||||
| import noteForm from './post-form.vue'; | ||||
| import renoteForm from './renote-form.vue'; | ||||
| import followButton from './follow-button.vue'; | ||||
| import notePreview from './note-preview.vue'; | ||||
| import noteDetail from './note-detail.vue'; | ||||
| import settings from './settings.vue'; | ||||
| import calendar from './calendar.vue'; | ||||
| import activity from './activity.vue'; | ||||
| import friendsMaker from './friends-maker.vue'; | ||||
| @@ -39,10 +37,8 @@ Vue.component('mk-media-video', mediaVideo); | ||||
| Vue.component('mk-notifications', notifications); | ||||
| Vue.component('mk-post-form', noteForm); | ||||
| Vue.component('mk-renote-form', renoteForm); | ||||
| Vue.component('mk-follow-button', followButton); | ||||
| Vue.component('mk-note-preview', notePreview); | ||||
| Vue.component('mk-note-detail', noteDetail); | ||||
| Vue.component('mk-settings', settings); | ||||
| Vue.component('mk-calendar', calendar); | ||||
| Vue.component('mk-activity', activity); | ||||
| Vue.component('mk-friends-maker', friendsMaker); | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| 		:disabled="conversationFetching" | ||||
| 	> | ||||
| 		<template v-if="!conversationFetching"><fa icon="ellipsis-v"/></template> | ||||
| 		<template v-if="conversationFetching"><fa icon="spinner .pulse"/></template> | ||||
| 		<template v-if="conversationFetching"><fa icon="spinner" pulse/></template> | ||||
| 	</button> | ||||
| 	<div class="conversation"> | ||||
| 		<x-sub v-for="note in conversation" :key="note.id" :note="note"/> | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
| 	<footer v-if="more"> | ||||
| 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| 			<template v-if="!moreFetching">{{ $t('@.load-more') }}</template> | ||||
| 			<template v-if="moreFetching"><fa icon="spinner .pulse" fixed-width/></template> | ||||
| 			<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> | ||||
| 		</button> | ||||
| 	</footer> | ||||
| </div> | ||||
|   | ||||
| @@ -105,7 +105,7 @@ | ||||
| 		</component> | ||||
| 	</div> | ||||
| 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> | ||||
| 		<template v-if="fetchingMoreNotifications"><fa icon="spinner .pulse" fixed-width/></template>{{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }} | ||||
| 		<template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }} | ||||
| 	</button> | ||||
| 	<p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p> | ||||
| </div> | ||||
|   | ||||
| @@ -15,11 +15,15 @@ | ||||
| 			<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a> | ||||
| 		</div> | ||||
| 		<input v-show="useCw" v-model="cw" :placeholder="$t('annotations')"> | ||||
| 		<div class="textarea"> | ||||
| 			<textarea :class="{ with: (files.length != 0 || poll) }" | ||||
| 				ref="text" v-model="text" :disabled="posting" | ||||
| 				@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" | ||||
| 				v-autocomplete="'text'" | ||||
| 			></textarea> | ||||
| 			<button class="emoji" @click="emoji" ref="emoji"> | ||||
| 				<fa :icon="['far', 'laugh']"/> | ||||
| 			</button> | ||||
| 			<div class="files" :class="{ with: poll }" v-show="files.length != 0"> | ||||
| 				<x-draggable :list="files" :options="{ animation: 150 }"> | ||||
| 					<div v-for="file in files" :key="file.id"> | ||||
| @@ -31,12 +35,13 @@ | ||||
| 			</div> | ||||
| 			<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="saveDraft()"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> | ||||
| 	<button class="upload" :title="$t('attach-media-from-local')" @click="chooseFile"><fa icon="upload"/></button> | ||||
| 	<button class="drive" :title="$t('attach-media-from-drive')" @click="chooseFileFromDrive"><fa icon="cloud"/></button> | ||||
| 	<button class="kao" :title="$t('insert-a-kao')" @click="kao"><fa :icon="['far', 'smile']"/></button> | ||||
| 	<button class="poll" :title="$t('create-poll')" @click="poll = !poll"><fa icon="chart-pie"/></button> | ||||
| 	<button class="cw" :title="$t('hide-contents')" @click="useCw = !useCw"><fa icon="eye-slash"/></button> | ||||
| 	<button class="cw" :title="$t('hide-contents')" @click="useCw = !useCw"><fa :icon="['far', 'eye-slash']"/></button> | ||||
| 	<button class="geo" :title="$t('attach-location-information')" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button> | ||||
| 	<button class="visibility" :title="$t('visibility')" @click="setVisibility" ref="visibilityButton"> | ||||
| 		<span v-if="visibility === 'public'"><fa icon="globe"/></span> | ||||
| @@ -377,6 +382,19 @@ export default Vue.extend({ | ||||
| 			this.visibleUsers = erase(user, this.visibleUsers); | ||||
| 		}, | ||||
|  | ||||
| 		async emoji() { | ||||
| 			const Picker = await import('./emoji-picker-dialog.vue').then(m => m.default); | ||||
| 			const button = this.$refs.emoji; | ||||
| 			const rect = button.getBoundingClientRect(); | ||||
| 			const vm = this.$root.new(Picker, { | ||||
| 				x: button.offsetWidth + rect.left + window.pageXOffset, | ||||
| 				y: rect.top + window.pageYOffset | ||||
| 			}); | ||||
| 			vm.$once('chosen', emoji => { | ||||
| 				insertTextAtCursor(this.$refs.text, emoji); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		post() { | ||||
| 			this.posting = true; | ||||
|  | ||||
| @@ -469,7 +487,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	> .content | ||||
| 		> input | ||||
| 		> textarea | ||||
| 		> .textarea > textarea | ||||
| 			display block | ||||
| 			width 100% | ||||
| 			padding 12px | ||||
| @@ -498,6 +516,24 @@ export default Vue.extend({ | ||||
| 		> input | ||||
| 			margin-bottom 8px | ||||
|  | ||||
| 		> .textarea | ||||
| 			> .emoji | ||||
| 				position absolute | ||||
| 				top 0 | ||||
| 				right 0 | ||||
| 				padding 10px | ||||
| 				font-size 18px | ||||
| 				color var(--text) | ||||
| 				opacity 0.5 | ||||
|  | ||||
| 				&:hover | ||||
| 					color var(--textHighlighted) | ||||
| 					opacity 1 | ||||
|  | ||||
| 				&:active | ||||
| 					color var(--primary) | ||||
| 					opacity 1 | ||||
|  | ||||
| 			> textarea | ||||
| 				margin 0 | ||||
| 				max-width 100% | ||||
| @@ -505,42 +541,24 @@ export default Vue.extend({ | ||||
| 				min-height 84px | ||||
|  | ||||
| 				&:hover | ||||
| 				& + * | ||||
| 					& + * + * | ||||
| 					& + * + * + * | ||||
| 						border-color var(--primaryAlpha02) | ||||
| 						transition border-color .1s ease | ||||
|  | ||||
| 				&:focus | ||||
| 				& + * | ||||
| 					& + * + * | ||||
| 					& + * + * + * | ||||
| 						border-color var(--primaryAlpha05) | ||||
| 						transition border-color 0s ease | ||||
|  | ||||
| 					& + .emoji | ||||
| 						opacity 0.7 | ||||
|  | ||||
| 				&.with | ||||
| 					border-bottom solid 1px var(--primaryAlpha01) !important | ||||
| 					border-radius 4px 4px 0 0 | ||||
|  | ||||
| 		> .visibleUsers | ||||
| 			margin-bottom 8px | ||||
| 			font-size 14px | ||||
|  | ||||
| 			> span | ||||
| 				margin-right 16px | ||||
| 				color var(--primary) | ||||
|  | ||||
| 		> .hashtags | ||||
| 			margin 0 0 8px 0 | ||||
| 			overflow hidden | ||||
| 			white-space nowrap | ||||
| 			font-size 14px | ||||
|  | ||||
| 			> b | ||||
| 				color var(--primary) | ||||
|  | ||||
| 			> * | ||||
| 				margin-right 8px | ||||
| 				white-space nowrap | ||||
|  | ||||
| 			> .files | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| @@ -601,6 +619,27 @@ export default Vue.extend({ | ||||
| 				border-radius 0 0 4px 4px | ||||
| 				transition border-color .3s ease | ||||
|  | ||||
| 		> .visibleUsers | ||||
| 			margin-bottom 8px | ||||
| 			font-size 14px | ||||
|  | ||||
| 			> span | ||||
| 				margin-right 16px | ||||
| 				color var(--primary) | ||||
|  | ||||
| 		> .hashtags | ||||
| 			margin 0 0 8px 0 | ||||
| 			overflow hidden | ||||
| 			white-space nowrap | ||||
| 			font-size 14px | ||||
|  | ||||
| 			> b | ||||
| 				color var(--primary) | ||||
|  | ||||
| 			> * | ||||
| 				margin-right 8px | ||||
| 				white-space nowrap | ||||
|  | ||||
| 	> .mk-uploader | ||||
| 		margin 8px 0 0 0 | ||||
| 		padding 8px | ||||
|   | ||||
| @@ -1,15 +1,21 @@ | ||||
| <template> | ||||
| <mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom"> | ||||
| 	<span slot="header" :class="$style.header"><fa icon="cog"/>{{ $t('settings') }}</span> | ||||
| 	<mk-settings :initial-page="initialPage" @done="close"/> | ||||
| 	<x-settings :initial-page="initialPage" @done="close"/> | ||||
| </mk-window> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/settings-window.vue'), | ||||
|  | ||||
| 	components: { | ||||
| 		XSettings: () => import('./settings.vue').then(m => m.default) | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		initialPage: { | ||||
| 			type: String, | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
| 	</div> | ||||
| 	<div class="pages"> | ||||
| 		<div class="profile" v-show="page == 'profile'"> | ||||
| 			<mk-profile-editor/> | ||||
| 			<x-profile-editor/> | ||||
|  | ||||
| 			<ui-card> | ||||
| 				<div slot="title"><fa :icon="['fab', 'twitter']"/> {{ $t('twitter') }}</div> | ||||
| @@ -36,7 +36,7 @@ | ||||
| 			<div slot="title"><fa icon="palette"/> {{ $t('theme') }}</div> | ||||
|  | ||||
| 			<section> | ||||
| 				<mk-theme/> | ||||
| 				<x-theme/> | ||||
| 			</section> | ||||
| 		</ui-card> | ||||
|  | ||||
| @@ -194,7 +194,7 @@ | ||||
| 		</ui-card> | ||||
|  | ||||
| 		<div class="drive" v-if="page == 'drive'"> | ||||
| 			<mk-drive-settings/> | ||||
| 			<x-drive-settings/> | ||||
| 		</div> | ||||
|  | ||||
| 		<ui-card class="hashtags" v-show="page == 'hashtags'"> | ||||
| @@ -205,7 +205,7 @@ | ||||
| 		</ui-card> | ||||
|  | ||||
| 		<div class="muteAndBlock" v-show="page == 'muteAndBlock'"> | ||||
| 			<mk-mute-and-block/> | ||||
| 			<x-mute-and-block/> | ||||
| 		</div> | ||||
|  | ||||
| 		<ui-card class="apps" v-show="page == 'apps'"> | ||||
| @@ -218,7 +218,7 @@ | ||||
| 		<ui-card class="password" v-show="page == 'security'"> | ||||
| 			<div slot="title"><fa icon="unlock-alt"/> {{ $t('password') }}</div> | ||||
| 			<section> | ||||
| 				<mk-password-settings/> | ||||
| 				<x-password-settings/> | ||||
| 			</section> | ||||
| 		</ui-card> | ||||
|  | ||||
| @@ -237,7 +237,7 @@ | ||||
| 		</ui-card> | ||||
|  | ||||
| 		<div class="api" v-show="page == 'api'"> | ||||
| 			<mk-api-settings/> | ||||
| 			<x-api-settings/> | ||||
| 		</div> | ||||
|  | ||||
| 		<ui-card class="other" v-show="page == 'other'"> | ||||
| @@ -301,7 +301,13 @@ export default Vue.extend({ | ||||
| 		X2fa, | ||||
| 		XApps, | ||||
| 		XSignins, | ||||
| 		XTags | ||||
| 		XTags, | ||||
| 		XTheme: () => import('../../../common/views/components/theme.vue').then(m => m.default), | ||||
| 		XDriveSettings: () => import('../../../common/views/components/drive-settings.vue').then(m => m.default), | ||||
| 		XMuteAndBlock: () => import('../../../common/views/components/mute-and-block.vue').then(m => m.default), | ||||
| 		XPasswordSettings: () => import('../../../common/views/components/password-settings.vue').then(m => m.default), | ||||
| 		XProfileEditor: () => import('../../../common/views/components/profile-editor.vue').then(m => m.default), | ||||
| 		XApiSettings: () => import('../../../common/views/components/api-settings.vue').then(m => m.default), | ||||
| 	}, | ||||
| 	props: { | ||||
| 		initialPage: { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <div class="zvdbznxvfixtmujpsigoccczftvpiwqh"> | ||||
| 	<div class="banner" :style="bannerStyle"></div> | ||||
| 	<mk-avatar class="avatar" :user="user" :disable-preview="true"/> | ||||
| 	<mk-follow-button :user="user" class="follow"/> | ||||
| 	<mk-follow-button :user="user" class="follow" mini/> | ||||
| 	<div class="body"> | ||||
| 		<router-link :to="user | userPage" class="name">{{ user | userName }}</router-link> | ||||
| 		<span class="username">@{{ user | acct }}</span> | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
| 				<p>{{ $t('followers') }}</p><span>{{ u.followersCount }}</span> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<mk-follow-button v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u"/> | ||||
| 		<mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u" mini/> | ||||
| 	</template> | ||||
| </div> | ||||
| </template> | ||||
| @@ -154,7 +154,7 @@ export default Vue.extend({ | ||||
| 				font-size 1em | ||||
| 				color var(--primary) | ||||
|  | ||||
| 	> .mk-follow-button | ||||
| 	> .follow-button | ||||
| 		position absolute | ||||
| 		top 92px | ||||
| 		right 8px | ||||
|   | ||||
| @@ -31,7 +31,7 @@ | ||||
| 	<footer v-if="more"> | ||||
| 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| 			<template v-if="!moreFetching">{{ $t('@.load-more') }}</template> | ||||
| 			<template v-if="moreFetching"><fa icon="spinner .pulse" fixed-width/></template> | ||||
| 			<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> | ||||
| 		</button> | ||||
| 	</footer> | ||||
| </div> | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
| 		</template> | ||||
| 	</component> | ||||
| 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> | ||||
| 		<template v-if="fetchingMoreNotifications"><fa icon="spinner .pulse" fixed-width/></template>{{ fetchingMoreNotifications ? this.$t('@.loading') : this.$t('@.load-more') }} | ||||
| 		<template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreNotifications ? this.$t('@.loading') : this.$t('@.load-more') }} | ||||
| 	</button> | ||||
| 	<p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p> | ||||
| </div> | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| 		<header :style="bannerStyle"> | ||||
| 			<div> | ||||
| 				<button class="menu" @click="menu" ref="menu"><fa icon="ellipsis-h"/></button> | ||||
| 				<mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow"/> | ||||
| 				<mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow" mini/> | ||||
| 				<mk-avatar class="avatar" :user="user" :disable-preview="true"/> | ||||
| 				<span class="name">{{ user | userName }}</span> | ||||
| 				<span class="acct">@{{ user | acct }}</span> | ||||
| @@ -155,7 +155,8 @@ export default Vue.extend({ | ||||
| 			this.$root.api('users/notes', { | ||||
| 				userId: this.user.id, | ||||
| 				fileType: image, | ||||
| 				limit: 9 | ||||
| 				limit: 9, | ||||
| 				untilDate: new Date().getTime() + 1000 * 86400 * 365 | ||||
| 			}).then(notes => { | ||||
| 				notes.forEach(note => { | ||||
| 					note.files.forEach(file => { | ||||
| @@ -254,6 +255,7 @@ export default Vue.extend({ | ||||
| 				this.$root.api('users/notes', { | ||||
| 					userId: this.user.id, | ||||
| 					limit: fetchLimit + 1, | ||||
| 					untilDate: new Date().getTime() + 1000 * 86400 * 365, | ||||
| 					withFiles: this.withFiles, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| @@ -274,7 +276,7 @@ export default Vue.extend({ | ||||
| 			const promise = this.$root.api('users/notes', { | ||||
| 				userId: this.user.id, | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime(), | ||||
| 				withFiles: this.withFiles, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div class="vahgrswmbzfdlmomxnqftuueyvwaafth"> | ||||
| 	<p class="title"><fa icon="users"/>{{ $t('title') }}</p> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p> | ||||
| 	<div v-if="!fetching && users.length > 0"> | ||||
| 	<router-link v-for="user in users" :to="user | userPage" :key="user.id"> | ||||
| 		<img :src="user.avatarUrl" :alt="user | userName" v-user-preview="user.id"/> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div class="hozptpaliadatkehcmcayizwzwwctpbc"> | ||||
| 	<p class="title"><fa icon="users"/>{{ $t('title') }}</p> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p> | ||||
| 	<template v-if="!fetching && users.length != 0"> | ||||
| 		<div class="user" v-for="friend in users"> | ||||
| 			<mk-avatar class="avatar" :user="friend"/> | ||||
| @@ -9,7 +9,7 @@ | ||||
| 				<router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link> | ||||
| 				<p class="username">@{{ friend | acct }}</p> | ||||
| 			</div> | ||||
| 			<mk-follow-button :user="friend"/> | ||||
| 			<mk-follow-button class="follow-button" :user="friend"/> | ||||
| 		</div> | ||||
| 	</template> | ||||
| 	<p class="empty" v-if="!fetching && users.length == 0">{{ $t('no-users') }}</p> | ||||
| @@ -110,7 +110,7 @@ export default Vue.extend({ | ||||
| 				color var(--text) | ||||
| 				opacity 0.7 | ||||
|  | ||||
| 		> .mk-follow-button | ||||
| 		> .follow-button | ||||
| 			position absolute | ||||
| 			top 16px | ||||
| 			right 16px | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div class="dzsuvbsrrrwobdxifudxuefculdfiaxd"> | ||||
| 	<p class="title"><fa icon="camera"/>{{ $t('title') }}</p> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p> | ||||
| 	<div class="stream" v-if="!fetching && images.length > 0"> | ||||
| 		<div v-for="image in images" class="img" | ||||
| 			:style="`background-image: url(${image.thumbnailUrl})`" | ||||
| @@ -27,7 +27,8 @@ export default Vue.extend({ | ||||
| 		this.$root.api('users/notes', { | ||||
| 			userId: this.user.id, | ||||
| 			withFiles: true, | ||||
| 			limit: 9 | ||||
| 			limit: 9, | ||||
| 			untilDate: new Date().getTime() + 1000 * 86400 * 365 | ||||
| 		}).then(notes => { | ||||
| 			notes.forEach(note => { | ||||
| 				note.files.forEach(file => { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div class="profile" v-if="$store.getters.isSignedIn"> | ||||
| 	<div class="friend-form" v-if="$store.state.i.id != user.id"> | ||||
| 		<mk-follow-button :user="user" size="big"/> | ||||
| 		<mk-follow-button :user="user" block/> | ||||
| 		<p class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</p> | ||||
| 		<p class="stalk" v-if="user.isFollowing"> | ||||
| 			<span v-if="user.isStalking">{{ $t('stalking') }} <a @click="unstalk"><fa icon="meh"/> {{ $t('unstalk') }}</a></span> | ||||
| @@ -11,7 +11,7 @@ | ||||
| 	<div class="action-form"> | ||||
| 		<ui-button @click="user.isMuted ? unmute() : mute()" v-if="$store.state.i.id != user.id"> | ||||
| 			<span v-if="user.isMuted"><fa icon="eye"/> {{ $t('unmute') }}</span> | ||||
| 			<span v-else><fa icon="eye-slash"/> {{ $t('mute') }}</span> | ||||
| 			<span v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mute') }}</span> | ||||
| 		</ui-button> | ||||
| 		<ui-button @click="user.isBlocking ? unblock() : block()" v-if="$store.state.i.id != user.id"> | ||||
| 			<span v-if="user.isBlocking"><fa icon="user"/> {{ $t('unblock') }}</span> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| 	<header> | ||||
| 		<span :data-active="mode == 'default'" @click="mode = 'default'"><fa :icon="['far', 'comment-alt']"/> {{ $t('default') }}</span> | ||||
| 		<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> {{ $t('with-replies') }}</span> | ||||
| 		<span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa icon="images"/> {{ $t('with-media') }}</span> | ||||
| 		<span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa :icon="['far', 'images']"/> {{ $t('with-media') }}</span> | ||||
| 	</header> | ||||
| 	<mk-notes ref="timeline" :more="existMore ? more : null"> | ||||
| 		<p class="empty" slot="empty"><fa :icon="['far', 'comments']"/>{{ $t('empty') }}</p> | ||||
| @@ -63,7 +63,7 @@ export default Vue.extend({ | ||||
| 				this.$root.api('users/notes', { | ||||
| 					userId: this.user.id, | ||||
| 					limit: fetchLimit + 1, | ||||
| 					untilDate: this.date ? this.date.getTime() : undefined, | ||||
| 					untilDate: this.date ? this.date.getTime() : new Date().getTime() + 1000 * 86400 * 365, | ||||
| 					includeReplies: this.mode == 'with-replies', | ||||
| 					withFiles: this.mode == 'with-media' | ||||
| 				}).then(notes => { | ||||
| @@ -86,7 +86,7 @@ export default Vue.extend({ | ||||
| 				limit: fetchLimit + 1, | ||||
| 				includeReplies: this.mode == 'with-replies', | ||||
| 				withFiles: this.mode == 'with-media', | ||||
| 				untilId: (this.$refs.timeline as any).tail().id | ||||
| 				untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime() | ||||
| 			}); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
|   | ||||
| @@ -50,7 +50,7 @@ | ||||
| 			</div> | ||||
|  | ||||
| 			<div class="photos block"> | ||||
| 				<header><fa icon="images"/> {{ $t('photos') }}</header> | ||||
| 				<header><fa :icon="['far', 'images']"/> {{ $t('photos') }}</header> | ||||
| 				<div> | ||||
| 					<div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div> | ||||
| 				</div> | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
| 				<mk-poll :note="poll"/> | ||||
| 			</div> | ||||
| 			<p class="empty" v-if="!fetching && poll == null">{{ $t('nothing') }}</p> | ||||
| 			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 			<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| </div> | ||||
|   | ||||
| @@ -1,16 +1,51 @@ | ||||
| <template> | ||||
| <div class="mkw-post-form"> | ||||
| 	<template v-if="props.design == 0"> | ||||
| 		<p class="title"><fa icon="pencil-alt"/>{{ $t('title') }}</p> | ||||
| 	</template> | ||||
| 	<textarea :disabled="posting" v-model="text" @keydown="onKeydown" :placeholder="placeholder"></textarea> | ||||
| 	<button @click="post" :disabled="posting">{{ $t('note') }}</button> | ||||
| <div> | ||||
| 	<mk-widget-container :show-header="props.design == 0"> | ||||
| 		<template slot="header"><fa icon="pencil-alt"/>{{ $t('title') }}</template> | ||||
|  | ||||
| 		<div class="lhcuptdmcdkfwmipgazeawoiuxpzaclc-body" | ||||
| 			@dragover.stop="onDragover" | ||||
| 			@drop.stop="onDrop" | ||||
| 		> | ||||
| 			<div class="textarea"> | ||||
| 				<textarea | ||||
| 					:disabled="posting" | ||||
| 					v-model="text" | ||||
| 					@keydown="onKeydown" | ||||
| 					@paste="onPaste" | ||||
| 					:placeholder="placeholder" | ||||
| 					ref="text" | ||||
| 					v-autocomplete="'text'" | ||||
| 				></textarea> | ||||
| 				<button class="emoji" @click="emoji" ref="emoji"> | ||||
| 					<fa :icon="['far', 'laugh']"/> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 			<div class="files" v-show="files.length != 0"> | ||||
| 				<x-draggable :list="files" :options="{ animation: 150 }"> | ||||
| 					<div v-for="file in files" :key="file.id"> | ||||
| 						<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div> | ||||
| 						<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/> | ||||
| 					</div> | ||||
| 				</x-draggable> | ||||
| 			</div> | ||||
| 			<input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/> | ||||
| 			<mk-uploader ref="uploader" @uploaded="attachMedia"/> | ||||
| 			<footer> | ||||
| 				<button @click="chooseFile"><fa icon="upload"/></button> | ||||
| 				<button @click="chooseFileFromDrive"><fa icon="cloud"/></button> | ||||
| 				<button @click="post" :disabled="posting" class="post">{{ $t('note') }}</button> | ||||
| 			</footer> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import define from '../../../common/define-widget'; | ||||
| import i18n from '../../../i18n'; | ||||
| import insertTextAtCursor from 'insert-text-at-cursor'; | ||||
| import * as XDraggable from 'vuedraggable'; | ||||
|  | ||||
| export default define({ | ||||
| 	name: 'post-form', | ||||
| @@ -19,12 +54,19 @@ export default define({ | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	i18n: i18n('desktop/views/widgets/post-form.vue'), | ||||
|  | ||||
| 	components: { | ||||
| 		XDraggable | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			posting: false, | ||||
| 			text: '' | ||||
| 			text: '', | ||||
| 			files: [], | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		placeholder(): string { | ||||
| 			const xs = [ | ||||
| @@ -38,6 +80,7 @@ export default define({ | ||||
| 			return xs[Math.floor(Math.random() * xs.length)]; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			if (this.props.design == 1) { | ||||
| @@ -47,14 +90,95 @@ export default define({ | ||||
| 			} | ||||
| 			this.save(); | ||||
| 		}, | ||||
|  | ||||
| 		chooseFile() { | ||||
| 			(this.$refs.file as any).click(); | ||||
| 		}, | ||||
|  | ||||
| 		chooseFileFromDrive() { | ||||
| 			this.$chooseDriveFile({ | ||||
| 				multiple: true | ||||
| 			}).then(files => { | ||||
| 				files.forEach(this.attachMedia); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		attachMedia(driveFile) { | ||||
| 			this.files.push(driveFile); | ||||
| 			this.$emit('change-attached-files', this.files); | ||||
| 		}, | ||||
|  | ||||
| 		detachMedia(id) { | ||||
| 			this.files = this.files.filter(x => x.id != id); | ||||
| 			this.$emit('change-attached-files', this.files); | ||||
| 		}, | ||||
|  | ||||
| 		onKeydown(e) { | ||||
| 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post(); | ||||
| 		}, | ||||
|  | ||||
| 		onPaste(e) { | ||||
| 			Array.from(e.clipboardData.items).forEach((item: any) => { | ||||
| 				if (item.kind == 'file') { | ||||
| 					this.upload(item.getAsFile()); | ||||
| 				} | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeFile() { | ||||
| 			Array.from((this.$refs.file as any).files).forEach(this.upload); | ||||
| 		}, | ||||
|  | ||||
| 		upload(file) { | ||||
| 			(this.$refs.uploader as any).upload(file); | ||||
| 		}, | ||||
|  | ||||
| 		onDragover(e) { | ||||
| 			const isFile = e.dataTransfer.items[0].kind == 'file'; | ||||
| 			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; | ||||
| 			if (isFile || isDriveFile) { | ||||
| 				e.preventDefault(); | ||||
| 				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		onDrop(e): void { | ||||
| 			// ファイルだったら | ||||
| 			if (e.dataTransfer.files.length > 0) { | ||||
| 				e.preventDefault(); | ||||
| 				Array.from(e.dataTransfer.files).forEach(this.upload); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			//#region ドライブのファイル | ||||
| 			const driveFile = e.dataTransfer.getData('mk_drive_file'); | ||||
| 			if (driveFile != null && driveFile != '') { | ||||
| 				const file = JSON.parse(driveFile); | ||||
| 				this.files.push(file); | ||||
| 				e.preventDefault(); | ||||
| 			} | ||||
| 			//#endregion | ||||
| 		}, | ||||
|  | ||||
| 		async emoji() { | ||||
| 			const Picker = await import('../components/emoji-picker-dialog.vue').then(m => m.default); | ||||
| 			const button = this.$refs.emoji; | ||||
| 			const rect = button.getBoundingClientRect(); | ||||
| 			const vm = this.$root.new(Picker, { | ||||
| 				x: button.offsetWidth + rect.left + window.pageXOffset, | ||||
| 				y: rect.top + window.pageYOffset | ||||
| 			}); | ||||
| 			vm.$once('chosen', emoji => { | ||||
| 				insertTextAtCursor(this.$refs.text, emoji); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		post() { | ||||
| 			this.posting = true; | ||||
|  | ||||
| 			this.$root.api('notes/create', { | ||||
| 				text: this.text | ||||
| 				text: this.text == '' ? undefined : this.text, | ||||
| 				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, | ||||
| 			}).then(data => { | ||||
| 				this.clear(); | ||||
| 			}).catch(err => { | ||||
| @@ -63,34 +187,34 @@ export default define({ | ||||
| 				this.posting = false; | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		clear() { | ||||
| 			this.text = ''; | ||||
| 			this.files = []; | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .lhcuptdmcdkfwmipgazeawoiuxpzaclc-body | ||||
| 	> .textarea | ||||
| 		> .emoji | ||||
| 			position absolute | ||||
| 			top 0 | ||||
| 			right 0 | ||||
| 			padding 10px | ||||
| 			font-size 18px | ||||
| 			color var(--text) | ||||
| 			opacity 0.5 | ||||
|  | ||||
| 			&:hover | ||||
| 				color var(--textHighlighted) | ||||
| 				opacity 1 | ||||
|  | ||||
| .mkw-post-form | ||||
| 	background #fff | ||||
| 	overflow hidden | ||||
| 	border solid 1px rgba(#000, 0.075) | ||||
| 	border-radius 6px | ||||
|  | ||||
| 	> .title | ||||
| 		z-index 1 | ||||
| 		margin 0 | ||||
| 		padding 0 16px | ||||
| 		line-height 42px | ||||
| 		font-size 0.9em | ||||
| 		font-weight bold | ||||
| 		color #888 | ||||
| 		box-shadow 0 1px rgba(#000, 0.07) | ||||
|  | ||||
| 		> [data-icon] | ||||
| 			margin-right 4px | ||||
| 			&:active | ||||
| 				color var(--primary) | ||||
| 				opacity 1 | ||||
|  | ||||
| 		> textarea | ||||
| 			display block | ||||
| @@ -98,16 +222,64 @@ export default define({ | ||||
| 			max-width 100% | ||||
| 			min-width 100% | ||||
| 			padding 16px | ||||
| 		margin-bottom 28px + 16px | ||||
| 			color var(--desktopPostFormTextareaFg) | ||||
| 			outline none | ||||
| 			background var(--desktopPostFormTextareaBg) | ||||
| 			border none | ||||
| 		border-bottom solid 1px #eee | ||||
| 			border-bottom solid 1px var(--faceDivider) | ||||
|  | ||||
| 	> button | ||||
| 			&:focus | ||||
| 				& + .emoji | ||||
| 					opacity 0.7 | ||||
|  | ||||
| 	> .files | ||||
| 		> div | ||||
| 			padding 4px | ||||
|  | ||||
| 			&:after | ||||
| 				content "" | ||||
| 				display block | ||||
| 				clear both | ||||
|  | ||||
| 			> div | ||||
| 				float left | ||||
| 				border solid 4px transparent | ||||
| 				cursor move | ||||
|  | ||||
| 				&:hover > .remove | ||||
| 					display block | ||||
|  | ||||
| 				> .img | ||||
| 					width 64px | ||||
| 					height 64px | ||||
| 					background-size cover | ||||
| 					background-position center center | ||||
|  | ||||
| 				> .remove | ||||
| 					display none | ||||
| 					position absolute | ||||
| 		bottom 8px | ||||
| 		right 8px | ||||
| 		margin 0 | ||||
| 					top -6px | ||||
| 					right -6px | ||||
| 					width 16px | ||||
| 					height 16px | ||||
| 					cursor pointer | ||||
|  | ||||
| 	> input[type=file] | ||||
| 		display none | ||||
|  | ||||
| 	> footer | ||||
| 		display flex | ||||
| 		padding 8px | ||||
|  | ||||
| 		> button:not(.post) | ||||
| 			color var(--text) | ||||
|  | ||||
| 			&:hover | ||||
| 				color var(--textHighlighted) | ||||
|  | ||||
| 		> .post | ||||
| 			display block | ||||
| 			margin 0 0 0 auto | ||||
| 			padding 0 10px | ||||
| 			height 28px | ||||
| 			color var(--primaryForeground) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| 		<button slot="func" :title="$t('title')" @click="fetch"><fa icon="sync"/></button> | ||||
|  | ||||
| 		<div class="mkw-trends--body"> | ||||
| 			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 			<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 			<div class="note" v-else-if="note != null"> | ||||
| 				<p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p> | ||||
| 				<p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p> | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| 		<button slot="func" :title="$t('title')" @click="refresh"><fa icon="sync"/></button> | ||||
|  | ||||
| 		<div class="mkw-users--body"> | ||||
| 			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 			<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 			<template v-else-if="users.length != 0"> | ||||
| 				<div class="user" v-for="_user in users"> | ||||
| 					<mk-avatar class="avatar" :user="_user"/> | ||||
| @@ -114,11 +114,6 @@ export default define({ | ||||
| 					color var(--text) | ||||
| 					opacity 0.7 | ||||
|  | ||||
| 			> .mk-follow-button | ||||
| 				position absolute | ||||
| 				top 16px | ||||
| 				right 16px | ||||
|  | ||||
| 		> .empty | ||||
| 			margin 0 | ||||
| 			padding 16px | ||||
|   | ||||
| @@ -25,47 +25,120 @@ if (localStorage.getItem('theme') == null) { | ||||
| import { library } from '@fortawesome/fontawesome-svg-core'; | ||||
| import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; | ||||
|  | ||||
| /* なぜか動かない | ||||
| import faRetweet from '@fortawesome/free-solid-svg-icons/faRetweet'; | ||||
| import faPlus from '@fortawesome/free-solid-svg-icons/faPlus'; | ||||
| import faUser from '@fortawesome/free-solid-svg-icons/faUser'; | ||||
| import faCog from '@fortawesome/free-solid-svg-icons/faCog'; | ||||
| import faCheck from '@fortawesome/free-solid-svg-icons/faCheck'; | ||||
| import faStar from '@fortawesome/free-solid-svg-icons/faStar'; | ||||
| import faReply from '@fortawesome/free-solid-svg-icons/faReply'; | ||||
| import faEllipsisH from '@fortawesome/free-solid-svg-icons/faEllipsisH'; | ||||
| import faQuoteLeft from '@fortawesome/free-solid-svg-icons/faQuoteLeft'; | ||||
| import faQuoteRight from '@fortawesome/free-solid-svg-icons/faQuoteRight'; | ||||
| import faAngleUp from '@fortawesome/free-solid-svg-icons/faAngleUp'; | ||||
| import faAngleDown from '@fortawesome/free-solid-svg-icons/faAngleDown'; | ||||
| import faAt from '@fortawesome/free-solid-svg-icons/faAt'; | ||||
| import faHashtag from '@fortawesome/free-solid-svg-icons/faHashtag'; | ||||
| import faHome from '@fortawesome/free-solid-svg-icons/faHome'; | ||||
| import faGlobe from '@fortawesome/free-solid-svg-icons/faGlobe'; | ||||
| import faCircle from '@fortawesome/free-solid-svg-icons/faCircle'; | ||||
| import faList from '@fortawesome/free-solid-svg-icons/faList'; | ||||
| import faHeart from '@fortawesome/free-solid-svg-icons/faHeart'; | ||||
| import faUnlock from '@fortawesome/free-solid-svg-icons/faUnlock'; | ||||
| import faRssSquare from '@fortawesome/free-solid-svg-icons/faRssSquare'; | ||||
| import faSort from '@fortawesome/free-solid-svg-icons/faSort'; | ||||
| import faChartPie from '@fortawesome/free-solid-svg-icons/faChartPie'; | ||||
| import faChartBar from '@fortawesome/free-solid-svg-icons/faChartBar'; | ||||
| import faPencilAlt from '@fortawesome/free-solid-svg-icons/faPencilAlt'; | ||||
| import faColumns from '@fortawesome/free-solid-svg-icons/faColumns'; | ||||
| import faComments from '@fortawesome/free-solid-svg-icons/faComments'; | ||||
| import faGamepad from '@fortawesome/free-solid-svg-icons/faGamepad'; | ||||
| import faCloud from '@fortawesome/free-solid-svg-icons/faCloud'; | ||||
| import faPowerOff from '@fortawesome/free-solid-svg-icons/faPowerOff'; | ||||
| import faChevronCircleLeft from '@fortawesome/free-solid-svg-icons/faChevronCircleLeft'; | ||||
| import faChevronCircleRight from '@fortawesome/free-solid-svg-icons/faChevronCircleRight'; | ||||
| import faShareAlt from '@fortawesome/free-solid-svg-icons/faShareAlt'; | ||||
| import faTimes from '@fortawesome/free-solid-svg-icons/faTimes'; | ||||
| import faThumbtack from '@fortawesome/free-solid-svg-icons/faThumbtack'; | ||||
| import faSearch from '@fortawesome/free-solid-svg-icons/faSearch'; | ||||
| import { | ||||
| 	faRetweet, | ||||
| 	faPlus, | ||||
| 	faUser, | ||||
| 	faCog, | ||||
| 	faCheck, | ||||
| 	faStar, | ||||
| 	faReply, | ||||
| 	faEllipsisH, | ||||
| 	faQuoteLeft, | ||||
| 	faQuoteRight, | ||||
| 	faAngleUp, | ||||
| 	faAngleDown, | ||||
| 	faAt, | ||||
| 	faHashtag, | ||||
| 	faHome, | ||||
| 	faGlobe, | ||||
| 	faCircle, | ||||
| 	faList, | ||||
| 	faHeart, | ||||
| 	faUnlock, | ||||
| 	faRssSquare, | ||||
| 	faSort, | ||||
| 	faChartPie, | ||||
| 	faChartBar, | ||||
| 	faPencilAlt, | ||||
| 	faColumns, | ||||
| 	faComments, | ||||
| 	faGamepad, | ||||
| 	faCloud, | ||||
| 	faPowerOff, | ||||
| 	faChevronCircleLeft, | ||||
| 	faChevronCircleRight, | ||||
| 	faShareAlt, | ||||
| 	faTimes, | ||||
| 	faThumbtack, | ||||
| 	faSearch, | ||||
| 	faAngleRight, | ||||
| 	faWrench, | ||||
| 	faTerminal, | ||||
| 	faMoon, | ||||
| 	faPalette, | ||||
| 	faSlidersH, | ||||
| 	faDesktop, | ||||
| 	faVolumeUp, | ||||
| 	faLanguage, | ||||
| 	faInfoCircle, | ||||
| 	faExclamationTriangle, | ||||
| 	faKey, | ||||
| 	faBan, | ||||
| 	faCogs, | ||||
| 	faUnlockAlt, | ||||
| 	faPuzzlePiece, | ||||
| 	faMobileAlt, | ||||
| 	faSignInAlt, | ||||
| 	faSyncAlt, | ||||
| 	faPaperPlane, | ||||
| 	faUpload, | ||||
| 	faMapMarkerAlt, | ||||
| 	faEnvelope, | ||||
| 	faLock, | ||||
| 	faFolderOpen, | ||||
| 	faBirthdayCake, | ||||
| 	faImage, | ||||
| 	faEye, | ||||
| 	faDownload, | ||||
| 	faFileImport, | ||||
| 	faLink, | ||||
| 	faArrowRight, | ||||
| 	faICursor, | ||||
| 	faCaretRight, | ||||
| 	faReplyAll, | ||||
| 	faCamera, | ||||
| 	faMinus, | ||||
| 	faCaretDown, | ||||
| 	faCalculator, | ||||
| 	faUsers, | ||||
| 	faBars, | ||||
| 	faFileImage, | ||||
| 	faPollH, | ||||
| 	faFolder, | ||||
| 	faMicrochip, | ||||
| 	faMemory, | ||||
| 	faServer, | ||||
| 	faExclamationCircle, | ||||
| 	faSpinner, | ||||
| 	faBroadcastTower | ||||
| } from '@fortawesome/free-solid-svg-icons'; | ||||
|  | ||||
| import farBell from '@fortawesome/free-regular-svg-icons/faBell'; | ||||
| import farEnvelope from '@fortawesome/free-regular-svg-icons/faEnvelope'; | ||||
| import farComments from '@fortawesome/free-regular-svg-icons/faComments'; | ||||
| import { | ||||
| 	faBell as farBell, | ||||
| 	faEnvelope as farEnvelope, | ||||
| 	faComments as farComments, | ||||
| 	faTrashAlt as farTrashAlt, | ||||
| 	faWindowRestore as farWindowRestore, | ||||
| 	faFolder as farFolder, | ||||
| 	faLaugh as farLaugh, | ||||
| 	faSmile as farSmile, | ||||
| 	faEyeSlash as farEyeSlash, | ||||
| 	faFolderOpen as farFolderOpen, | ||||
| 	faSave as farSave, | ||||
| 	faImages as farImages, | ||||
| 	faChartBar as farChartBar, | ||||
| 	faCommentAlt as farCommentAlt, | ||||
| 	faClock as farClock, | ||||
| 	faCalendarAlt as farCalendarAlt, | ||||
| 	faHdd as farHdd, | ||||
| } from '@fortawesome/free-regular-svg-icons'; | ||||
|  | ||||
| import { | ||||
| 	faTwitter as fabTwitter, | ||||
| 	faGithub as fabGithub, | ||||
| } from '@fortawesome/free-brands-svg-icons'; | ||||
| import i18n from './i18n'; | ||||
|  | ||||
| library.add( | ||||
| 	faRetweet, | ||||
| @@ -104,16 +177,78 @@ library.add( | ||||
| 	faTimes, | ||||
| 	faThumbtack, | ||||
| 	faSearch, | ||||
| 	faAngleRight, | ||||
| 	faWrench, | ||||
| 	faTerminal, | ||||
| 	faMoon, | ||||
| 	faPalette, | ||||
| 	faSlidersH, | ||||
| 	faDesktop, | ||||
| 	faVolumeUp, | ||||
| 	faLanguage, | ||||
| 	faInfoCircle, | ||||
| 	faExclamationTriangle, | ||||
| 	faKey, | ||||
| 	faBan, | ||||
| 	faCogs, | ||||
| 	faUnlockAlt, | ||||
| 	faPuzzlePiece, | ||||
| 	faMobileAlt, | ||||
| 	faSignInAlt, | ||||
| 	faSyncAlt, | ||||
| 	faPaperPlane, | ||||
| 	faUpload, | ||||
| 	faMapMarkerAlt, | ||||
| 	faEnvelope, | ||||
| 	faLock, | ||||
| 	faFolderOpen, | ||||
| 	faBirthdayCake, | ||||
| 	faImage, | ||||
| 	faEye, | ||||
| 	faDownload, | ||||
| 	faFileImport, | ||||
| 	faLink, | ||||
| 	faArrowRight, | ||||
| 	faICursor, | ||||
| 	faCaretRight, | ||||
| 	faReplyAll, | ||||
| 	faCamera, | ||||
| 	faMinus, | ||||
| 	faCaretDown, | ||||
| 	faCalculator, | ||||
| 	faUsers, | ||||
| 	faBars, | ||||
| 	faFileImage, | ||||
| 	faPollH, | ||||
| 	faFolder, | ||||
| 	faMicrochip, | ||||
| 	faMemory, | ||||
| 	faServer, | ||||
| 	faExclamationCircle, | ||||
| 	faSpinner, | ||||
| 	faBroadcastTower, | ||||
|  | ||||
| 	farBell, | ||||
| 	farEnvelope, | ||||
| 	farComments, | ||||
| 	farTrashAlt, | ||||
| 	farWindowRestore, | ||||
| 	farFolder, | ||||
| 	farLaugh, | ||||
| 	farSmile, | ||||
| 	farEyeSlash, | ||||
| 	farFolderOpen, | ||||
| 	farSave, | ||||
| 	farImages, | ||||
| 	farChartBar, | ||||
| 	farCommentAlt, | ||||
| 	farClock, | ||||
| 	farCalendarAlt, | ||||
| 	farHdd, | ||||
|  | ||||
| 	fabTwitter, | ||||
| 	fabGithub | ||||
| ); | ||||
| */ | ||||
|  | ||||
| import { fas } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { far } from '@fortawesome/free-regular-svg-icons'; | ||||
|  | ||||
| library.add(fas, far); | ||||
| //#endregion | ||||
|  | ||||
| Vue.use(Vuex); | ||||
| @@ -269,13 +404,7 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS]) => void, | ||||
| 			}, { passive: true }); | ||||
|  | ||||
| 			const app = new Vue({ | ||||
| 				i18n: new VueI18n({ | ||||
| 					sync: false, | ||||
| 					locale: lang, | ||||
| 					messages: { | ||||
| 						[lang]: {} | ||||
| 					} | ||||
| 				}), | ||||
| 				i18n: i18n(), | ||||
| 				store: os.store, | ||||
| 				data() { | ||||
| 					return { | ||||
|   | ||||
| @@ -27,7 +27,6 @@ import MkFollowing from './views/pages/following.vue'; | ||||
| import MkFavorites from './views/pages/favorites.vue'; | ||||
| import MkUserLists from './views/pages/user-lists.vue'; | ||||
| import MkUserList from './views/pages/user-list.vue'; | ||||
| import MkSettings from './views/pages/settings.vue'; | ||||
| import MkReversi from './views/pages/games/reversi.vue'; | ||||
| import MkTag from './views/pages/tag.vue'; | ||||
| import MkShare from './views/pages/share.vue'; | ||||
| @@ -137,7 +136,7 @@ init((launch) => { | ||||
| 		routes: [ | ||||
| 			{ path: '/', name: 'index', component: MkIndex }, | ||||
| 			{ path: '/signup', name: 'signup', component: MkSignup }, | ||||
| 			{ path: '/i/settings', name: 'settings', component: MkSettings }, | ||||
| 			{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, | ||||
| 			{ path: '/i/notifications', name: 'notifications', component: MkNotifications }, | ||||
| 			{ path: '/i/favorites', name: 'favorites', component: MkFavorites }, | ||||
| 			{ path: '/i/lists', name: 'user-lists', component: MkUserLists }, | ||||
| @@ -154,7 +153,7 @@ init((launch) => { | ||||
| 			{ path: '/tags/:tag', component: MkTag }, | ||||
| 			{ path: '/share', component: MkShare }, | ||||
| 			{ path: '/reversi/:game?', name: 'reversi', component: MkReversi }, | ||||
| 			{ path: '/@:user', component: MkUser }, | ||||
| 			{ path: '/@:user', component: () => import('./views/pages/user.vue').then(m => m.default) }, | ||||
| 			{ path: '/@:user/followers', component: MkFollowers }, | ||||
| 			{ path: '/@:user/following', component: MkFollowing }, | ||||
| 			{ path: '/notes/:note', component: MkNote }, | ||||
|   | ||||
| @@ -31,7 +31,7 @@ | ||||
| 			<span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> | ||||
| 			<template v-if="file.isSensitive"> | ||||
| 				<span class="separator"></span> | ||||
| 				<span class="nsfw"><fa icon="eye-slash"/> {{ $t('nsfw') }}</span> | ||||
| 				<span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span> | ||||
| 			</template> | ||||
| 		</div> | ||||
| 	</div> | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
| 				<span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> | ||||
| 				<template v-if="file.isSensitive"> | ||||
| 					<span class="separator"></span> | ||||
| 					<span class="nsfw"><fa icon="eye-slash"/> {{ $t('nsfw') }}</span> | ||||
| 					<span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span> | ||||
| 				</template> | ||||
| 			</footer> | ||||
| 		</div> | ||||
|   | ||||
| @@ -51,8 +51,6 @@ export default Vue.extend({ | ||||
| 			top 0 | ||||
| 			bottom 0 | ||||
| 			right 20px | ||||
|  | ||||
| 			> * | ||||
| 			height 100% | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -1,134 +0,0 @@ | ||||
| <template> | ||||
| <button class="mk-follow-button" | ||||
| 	:class="{ wait: wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou }" | ||||
| 	@click="onClick" | ||||
| 	:disabled="wait" | ||||
| > | ||||
| 	<template v-if="!wait"> | ||||
| 		<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template> | ||||
| 		<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked"><fa icon="hourglass-start"/> {{ $t('follow-processing') }}</template> | ||||
| 		<template v-else-if="u.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template> | ||||
| 		<template v-else-if="!u.isFollowing && u.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template> | ||||
| 		<template v-else-if="!u.isFollowing && !u.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template> | ||||
| 	</template> | ||||
| 	<template v-else><fa icon="spinner .pulse" fixed-width/></template> | ||||
| </button> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('mobile/views/components/follow-button.vue'), | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			u: this.user, | ||||
| 			wait: false, | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.connection = this.$root.stream.useSharedConnection('main'); | ||||
|  | ||||
| 		this.connection.on('follow', this.onFollowChange); | ||||
| 		this.connection.on('unfollow', this.onFollowChange); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		onFollowChange(user) { | ||||
| 			if (user.id == this.u.id) { | ||||
| 				this.u.isFollowing = user.isFollowing; | ||||
| 				this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; | ||||
| 				this.$forceUpdate(); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		async onClick() { | ||||
| 			this.wait = true; | ||||
|  | ||||
| 			try { | ||||
| 				if (this.u.isFollowing) { | ||||
| 					this.u = await this.$root.api('following/delete', { | ||||
| 						userId: this.u.id | ||||
| 					}); | ||||
| 				} else { | ||||
| 					if (this.u.hasPendingFollowRequestFromYou) { | ||||
| 						this.u = await this.$root.api('following/requests/cancel', { | ||||
| 							userId: this.u.id | ||||
| 						}); | ||||
| 					} else if (this.u.isLocked) { | ||||
| 						this.u = await this.$root.api('following/create', { | ||||
| 							userId: this.u.id | ||||
| 						}); | ||||
| 					} else { | ||||
| 						this.u = await this.$root.api('following/create', { | ||||
| 							userId: this.user.id | ||||
| 						}); | ||||
| 					} | ||||
| 				} | ||||
| 			} catch (e) { | ||||
| 				console.error(e); | ||||
| 			} finally { | ||||
| 				this.wait = false; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-follow-button | ||||
| 	display block | ||||
| 	user-select none | ||||
| 	cursor pointer | ||||
| 	padding 0 16px | ||||
| 	margin 0 | ||||
| 	min-width 100px | ||||
| 	line-height 36px | ||||
| 	font-size 14px | ||||
| 	font-weight bold | ||||
| 	color var(--primary) | ||||
| 	background transparent | ||||
| 	outline none | ||||
| 	border solid 1px var(--primary) | ||||
| 	border-radius 36px | ||||
|  | ||||
| 	&:hover | ||||
| 		background var(--primaryAlpha01) | ||||
|  | ||||
| 	&:active | ||||
| 		background var(--primaryAlpha02) | ||||
|  | ||||
| 	&.active | ||||
| 		color var(--primaryForeground) | ||||
| 		background var(--primary) | ||||
|  | ||||
| 		&:hover | ||||
| 			background var(--primaryLighten10) | ||||
| 			border-color var(--primaryLighten10) | ||||
|  | ||||
| 		&:active | ||||
| 			background var(--primaryDarken10) | ||||
| 			border-color var(--primaryDarken10) | ||||
|  | ||||
| 	&.wait | ||||
| 		cursor wait !important | ||||
| 		opacity 0.7 | ||||
|  | ||||
| 	* | ||||
| 		pointer-events none | ||||
|  | ||||
| </style> | ||||
| @@ -5,7 +5,7 @@ | ||||
| 		<mk-user-card v-for="user in users" :key="user.id" :user="user"/> | ||||
| 	</div> | ||||
| 	<p class="empty" v-if="!fetching && users.length == 0">{{ $t('empty') }}</p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('fetching') }}<mk-ellipsis/></p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('fetching') }}<mk-ellipsis/></p> | ||||
| 	<a class="refresh" @click="refresh">{{ $t('refresh') }}</a> | ||||
| 	<button class="close" @click="close" :title="$t('title')"><fa icon="times"/></button> | ||||
| </div> | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import subNoteContent from './sub-note-content.vue'; | ||||
| import noteCard from './note-card.vue'; | ||||
| import userCard from './user-card.vue'; | ||||
| import noteDetail from './note-detail.vue'; | ||||
| import followButton from './follow-button.vue'; | ||||
| import friendsMaker from './friends-maker.vue'; | ||||
| import notification from './notification.vue'; | ||||
| import notifications from './notifications.vue'; | ||||
| @@ -19,7 +18,6 @@ import usersList from './users-list.vue'; | ||||
| import userPreview from './user-preview.vue'; | ||||
| import userTimeline from './user-timeline.vue'; | ||||
| import userListTimeline from './user-list-timeline.vue'; | ||||
| import activity from './activity.vue'; | ||||
| import widgetContainer from './widget-container.vue'; | ||||
| import postForm from './post-form.vue'; | ||||
|  | ||||
| @@ -33,7 +31,6 @@ Vue.component('mk-sub-note-content', subNoteContent); | ||||
| Vue.component('mk-note-card', noteCard); | ||||
| Vue.component('mk-user-card', userCard); | ||||
| Vue.component('mk-note-detail', noteDetail); | ||||
| Vue.component('mk-follow-button', followButton); | ||||
| Vue.component('mk-friends-maker', friendsMaker); | ||||
| Vue.component('mk-notification', notification); | ||||
| Vue.component('mk-notifications', notifications); | ||||
| @@ -42,6 +39,5 @@ Vue.component('mk-users-list', usersList); | ||||
| Vue.component('mk-user-preview', userPreview); | ||||
| Vue.component('mk-user-timeline', userTimeline); | ||||
| Vue.component('mk-user-list-timeline', userListTimeline); | ||||
| Vue.component('mk-activity', activity); | ||||
| Vue.component('mk-widget-container', widgetContainer); | ||||
| Vue.component('mk-post-form', postForm); | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
| 		:disabled="conversationFetching" | ||||
| 	> | ||||
| 		<template v-if="!conversationFetching"><fa icon="ellipsis-v"/></template> | ||||
| 		<template v-if="conversationFetching"><fa icon="spinner .pulse"/></template> | ||||
| 		<template v-if="conversationFetching"><fa icon="spinner" pulse/></template> | ||||
| 	</button> | ||||
| 	<div class="conversation"> | ||||
| 		<x-sub v-for="note in conversation" :key="note.id" :note="note"/> | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
| 	<footer v-if="more"> | ||||
| 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| 			<template v-if="!moreFetching">{{ $t('@.load-more') }}</template> | ||||
| 			<template v-if="moreFetching"><fa icon="spinner .pulse" fixed-width/></template> | ||||
| 			<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> | ||||
| 		</button> | ||||
| 	</footer> | ||||
| </div> | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
| 	</component> | ||||
|  | ||||
| 	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> | ||||
| 		<template v-if="fetchingMoreNotifications"><fa icon="spinner .pulse" fixed-width/></template> | ||||
| 		<template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template> | ||||
| 		{{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }} | ||||
| 	</button> | ||||
|  | ||||
|   | ||||
| @@ -32,7 +32,7 @@ | ||||
| 				<button class="drive" @click="chooseFileFromDrive"><fa icon="cloud"/></button> | ||||
| 				<button class="kao" @click="kao"><fa :icon="['far', 'smile']"/></button> | ||||
| 				<button class="poll" @click="poll = true"><fa icon="chart-pie"/></button> | ||||
| 				<button class="poll" @click="useCw = !useCw"><fa icon="eye-slash"/></button> | ||||
| 				<button class="poll" @click="useCw = !useCw"><fa :icon="['far', 'eye-slash']"/></button> | ||||
| 				<button class="geo" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button> | ||||
| 				<button class="visibility" @click="setVisibility" ref="visibilityButton"> | ||||
| 					<span v-if="visibility === 'public'"><fa icon="globe"/></span> | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| 	</header> | ||||
| 	<a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a> | ||||
| 	<p class="username"><mk-acct :user="user"/></p> | ||||
| 	<mk-follow-button :user="user"/> | ||||
| 	<mk-follow-button class="follow-button" :user="user"/> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -53,7 +53,7 @@ export default Vue.extend({ | ||||
| 		font-size 15px | ||||
| 		color #ccc | ||||
|  | ||||
| 	> .mk-follow-button | ||||
| 	> .follow-button | ||||
| 		display inline-block | ||||
| 		margin 8px 0 16px 0 | ||||
|  | ||||
|   | ||||
| @@ -44,7 +44,8 @@ export default Vue.extend({ | ||||
| 				this.$root.api('users/notes', { | ||||
| 					userId: this.user.id, | ||||
| 					withFiles: this.withMedia, | ||||
| 					limit: fetchLimit + 1 | ||||
| 					limit: fetchLimit + 1, | ||||
| 					untilDate: new Date().getTime() + 1000 * 86400 * 365 | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| @@ -66,7 +67,7 @@ export default Vue.extend({ | ||||
| 				userId: this.user.id, | ||||
| 				withFiles: this.withMedia, | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id | ||||
| 				untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime() | ||||
| 			}); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| 	<p class="no" v-if="!fetching && users.length == 0"> | ||||
| 		<slot></slot> | ||||
| 	</p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -5,12 +5,12 @@ | ||||
| 		<div class="signin-as" v-html="this.$t('signed-in-as').replace('{}', `<b>${name}</b>`)"></div> | ||||
|  | ||||
| 		<div> | ||||
| 			<mk-profile-editor/> | ||||
| 			<x-profile-editor/> | ||||
|  | ||||
| 			<ui-card> | ||||
| 				<div slot="title"><fa icon="palette"/> {{ $t('theme') }}</div> | ||||
| 				<section> | ||||
| 					<mk-theme/> | ||||
| 					<x-theme/> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
|  | ||||
| @@ -85,9 +85,9 @@ | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
|  | ||||
| 			<mk-drive-settings/> | ||||
| 			<x-drive-settings/> | ||||
|  | ||||
| 			<mk-mute-and-block/> | ||||
| 			<x-mute-and-block/> | ||||
|  | ||||
| 			<ui-card> | ||||
| 				<div slot="title"><fa icon="volume-up"/> {{ $t('sound') }}</div> | ||||
| @@ -140,12 +140,12 @@ | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
|  | ||||
| 			<mk-api-settings /> | ||||
| 			<x-api-settings /> | ||||
|  | ||||
| 			<ui-card> | ||||
| 				<div slot="title"><fa icon="unlock-alt"/> {{ $t('password') }}</div> | ||||
| 				<section> | ||||
| 					<mk-password-settings/> | ||||
| 					<x-password-settings/> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
|  | ||||
| @@ -182,6 +182,16 @@ import checkForUpdate from '../../../common/scripts/check-for-update'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('mobile/views/pages/settings.vue'), | ||||
|  | ||||
| 	components: { | ||||
| 		XTheme: () => import('../../../common/views/components/theme.vue').then(m => m.default), | ||||
| 		XDriveSettings: () => import('../../../common/views/components/drive-settings.vue').then(m => m.default), | ||||
| 		XMuteAndBlock: () => import('../../../common/views/components/mute-and-block.vue').then(m => m.default), | ||||
| 		XPasswordSettings: () => import('../../../common/views/components/password-settings.vue').then(m => m.default), | ||||
| 		XProfileEditor: () => import('../../../common/views/components/profile-editor.vue').then(m => m.default), | ||||
| 		XApiSettings: () => import('../../../common/views/components/api-settings.vue').then(m => m.default), | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			apiUrl, | ||||
|   | ||||
| @@ -116,7 +116,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		menu() { | ||||
| 			let menu = [{ | ||||
| 				icon: this.user.isMuted ? '<fa icon="eye"/>' : '<fa icon="eye-slash"/>', | ||||
| 				icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], | ||||
| 				text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'), | ||||
| 				action: () => { | ||||
| 					if (this.user.isMuted) { | ||||
| @@ -138,7 +138,7 @@ export default Vue.extend({ | ||||
| 					} | ||||
| 				} | ||||
| 			}, { | ||||
| 				icon: this.user.isBlocking ? '<fa icon="user"/>' : '<fa icon="user-slash"/>', | ||||
| 				icon: this.user.isBlocking ? ['fas', 'user'] : ['fas', 'user-slash'], | ||||
| 				text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'), | ||||
| 				action: () => { | ||||
| 					if (this.user.isBlocking) { | ||||
| @@ -243,9 +243,6 @@ main | ||||
| 					font-size 18px | ||||
| 					color var(--text) | ||||
|  | ||||
| 				> .mk-follow-button | ||||
| 					margin 0 | ||||
|  | ||||
| 			> .title | ||||
| 				margin 8px 0 | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="root followers-you-know"> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<div v-if="!fetching && users.length > 0"> | ||||
| 		<a v-for="user in users" :key="user.id" :href="user | userPage"> | ||||
| 			<img :src="user.avatarUrl" :alt="user | userName"/> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="root friends"> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<div v-if="!fetching && users.length > 0"> | ||||
| 		<mk-user-card v-for="user in users" :key="user.id" :user="user"/> | ||||
| 	</div> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="root notes"> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<div v-if="!fetching && notes.length > 0"> | ||||
| 		<mk-note-card v-for="note in notes" :key="note.id" :note="note"/> | ||||
| 	</div> | ||||
| @@ -22,7 +22,8 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.$root.api('users/notes', { | ||||
| 			userId: this.user.id | ||||
| 			userId: this.user.id, | ||||
| 			untilDate: new Date().getTime() + 1000 * 86400 * 365 | ||||
| 		}).then(notes => { | ||||
| 			this.notes = notes; | ||||
| 			this.fetching = false; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="root photos"> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| 	<div class="stream" v-if="!fetching && images.length > 0"> | ||||
| 		<a v-for="image in images" | ||||
| 			class="img" | ||||
| @@ -29,7 +29,8 @@ export default Vue.extend({ | ||||
| 		this.$root.api('users/notes', { | ||||
| 			userId: this.user.id, | ||||
| 			withFiles: true, | ||||
| 			limit: 6 | ||||
| 			limit: 6, | ||||
| 			untilDate: new Date().getTime() + 1000 * 86400 * 365 | ||||
| 		}).then(notes => { | ||||
| 			notes.forEach(note => { | ||||
| 				note.media.forEach(media => { | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
| 	<section class="activity"> | ||||
| 		<h2><fa icon="chart-bar"/>{{ $t('activity') }}</h2> | ||||
| 		<div> | ||||
| 			<mk-activity :user="user"/> | ||||
| 			<x-activity :user="user"/> | ||||
| 		</div> | ||||
| 	</section> | ||||
| 	<section class="frequently-replied-users"> | ||||
| @@ -49,7 +49,8 @@ export default Vue.extend({ | ||||
| 		XNotes, | ||||
| 		XPhotos, | ||||
| 		XFriends, | ||||
| 		XFollowersYouKnow | ||||
| 		XFollowersYouKnow, | ||||
| 		XActivity: () => import('../../components/activity.vue').then(m => m.default) | ||||
| 	}, | ||||
| 	props: ['user'] | ||||
| }); | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| 	<mk-widget-container :show-header="!props.compact"> | ||||
| 		<template slot="header"><fa icon="chart-bar"/>{{ $t('activity') }}</template> | ||||
| 		<div :class="$style.body"> | ||||
| 			<mk-activity :user="$store.state.i"/> | ||||
| 			<x-activity :user="$store.state.i"/> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| </div> | ||||
| @@ -20,6 +20,9 @@ export default define({ | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	i18n: i18n(), | ||||
| 	components: { | ||||
| 		XActivity: () => import('../components/activity.vue').then(m => m.default) | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			this.props.compact = !this.props.compact; | ||||
|   | ||||
| @@ -18,6 +18,7 @@ | ||||
| 		secondary: '$secondary', | ||||
| 		bg: ':darken<8<$secondary', | ||||
| 		text: '$text', | ||||
| 		textHighlighted: ':lighten<7<$text', | ||||
|  | ||||
| 		scrollbarTrack: ':darken<5<$secondary', | ||||
| 		scrollbarHandle: ':lighten<5<$secondary', | ||||
|   | ||||
| @@ -18,6 +18,7 @@ | ||||
| 		secondary: '$secondary', | ||||
| 		bg: ':darken<8<$secondary', | ||||
| 		text: '$text', | ||||
| 		textHighlighted: ':darken<7<$text', | ||||
|  | ||||
| 		scrollbarTrack: '#fff', | ||||
| 		scrollbarHandle: '#00000033', | ||||
|   | ||||
| @@ -56,7 +56,7 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any) | ||||
| 			console.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`); | ||||
| 		} | ||||
| 	} catch (e) { | ||||
| 		if (e.name == 'INVALID_PARAM') { | ||||
| 		if (e && e.name == 'INVALID_PARAM') { | ||||
| 			rej({ | ||||
| 				code: e.name, | ||||
| 				param: e.param, | ||||
|   | ||||
| @@ -216,7 +216,7 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => { | ||||
| 	} | ||||
|  | ||||
| 	// 投稿を作成 | ||||
| 	const note = await create(user, { | ||||
| 	create(user, { | ||||
| 		createdAt: new Date(), | ||||
| 		files: files, | ||||
| 		poll: ps.poll, | ||||
| @@ -229,12 +229,14 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => { | ||||
| 		visibility: ps.visibility, | ||||
| 		visibleUsers, | ||||
| 		geo: ps.geo | ||||
| 	}); | ||||
|  | ||||
| 	const noteObj = await pack(note, user); | ||||
|  | ||||
| 	// Reponse | ||||
| 	}) | ||||
| 	.then(note => pack(note, user)) | ||||
| 	.then(noteObj => { | ||||
| 		res({ | ||||
| 			createdNote: noteObj | ||||
| 		}); | ||||
| 	}) | ||||
| 	.catch(e => { | ||||
| 		rej(e); | ||||
| 	}); | ||||
| })); | ||||
|   | ||||
| @@ -153,9 +153,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||
| 	} | ||||
|  | ||||
| 	//#region Construct query | ||||
| 	const sort = { | ||||
| 		_id: -1 | ||||
| 	}; | ||||
| 	const sort = { } as any; | ||||
|  | ||||
| 	const query = { | ||||
| 		deletedAt: null, | ||||
| @@ -168,15 +166,17 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||
| 			$gt: ps.sinceId | ||||
| 		}; | ||||
| 	} else if (ps.untilId) { | ||||
| 		sort._id = -1; | ||||
| 		query._id = { | ||||
| 			$lt: ps.untilId | ||||
| 		}; | ||||
| 	} else if (ps.sinceDate) { | ||||
| 		sort._id = 1; | ||||
| 		sort.createdAt = 1; | ||||
| 		query.createdAt = { | ||||
| 			$gt: new Date(ps.sinceDate) | ||||
| 		}; | ||||
| 	} else if (ps.untilDate) { | ||||
| 		sort.createdAt = -1; | ||||
| 		query.createdAt = { | ||||
| 			$lt: new Date(ps.untilDate) | ||||
| 		}; | ||||
|   | ||||
| @@ -116,27 +116,27 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< | ||||
|  | ||||
| 	// リプライ対象が削除された投稿だったらreject | ||||
| 	if (data.reply && data.reply.deletedAt != null) { | ||||
| 		return rej(); | ||||
| 		return rej('Reply target has been deleted'); | ||||
| 	} | ||||
|  | ||||
| 	// Renote対象が削除された投稿だったらreject | ||||
| 	if (data.renote && data.renote.deletedAt != null) { | ||||
| 		return rej(); | ||||
| 		return rej('Renote target has been deleted'); | ||||
| 	} | ||||
|  | ||||
| 	// Renote対象が「ホームまたは全体」以外の公開範囲ならreject | ||||
| 	if (data.renote && data.renote.visibility != 'public' && data.renote.visibility != 'home') { | ||||
| 		return rej(); | ||||
| 		return rej('Renote target is not public or home'); | ||||
| 	} | ||||
|  | ||||
| 	// リプライ対象が自分以外の非公開の投稿なら禁止 | ||||
| 	if (data.reply && data.reply.visibility == 'private' && !data.reply.userId.equals(user._id)) { | ||||
| 		return rej(); | ||||
| 		return rej('Reply target is private of others'); | ||||
| 	} | ||||
|  | ||||
| 	// Renote対象が自分以外の非公開の投稿なら禁止 | ||||
| 	if (data.renote && data.renote.visibility == 'private' && !data.renote.userId.equals(user._id)) { | ||||
| 		return rej(); | ||||
| 		return rej('Renote target is private of others'); | ||||
| 	} | ||||
|  | ||||
| 	if (data.text) { | ||||
|   | ||||
| @@ -9,6 +9,7 @@ const { VueLoaderPlugin } = require('vue-loader'); | ||||
| const WebpackOnBuildPlugin = require('on-build-webpack'); | ||||
| //const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); | ||||
| const ProgressBarPlugin = require('progress-bar-webpack-plugin'); | ||||
| const TerserPlugin = require('terser-webpack-plugin'); | ||||
|  | ||||
| const constants = require('./src/const.json'); | ||||
|  | ||||
| @@ -146,6 +147,9 @@ module.exports = { | ||||
| 	resolveLoader: { | ||||
| 		modules: ['node_modules'] | ||||
| 	}, | ||||
| 	optimization: { | ||||
| 		minimizer: [new TerserPlugin()] | ||||
| 	}, | ||||
| 	cache: true, | ||||
| 	devtool: false, //'source-map', | ||||
| 	mode: isProduction ? 'production' : 'development' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user