Compare commits
	
		
			203 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9059c149dd | ||
| 
						 | 
					7d8e70b2ac | ||
| 
						 | 
					89105f5641 | ||
| 
						 | 
					1813d17b4c | ||
| 
						 | 
					ce27b36fd0 | ||
| 
						 | 
					e635a87628 | ||
| 
						 | 
					80c52433cc | ||
| 
						 | 
					1472f0b141 | ||
| 
						 | 
					4d914f5c0a | ||
| 
						 | 
					0318f7344f | ||
| 
						 | 
					413fbb3d0c | ||
| 
						 | 
					8bc47baf4f | ||
| 
						 | 
					e3f6d42a47 | ||
| 
						 | 
					8230935fd3 | ||
| 
						 | 
					f968d05ea0 | ||
| 
						 | 
					d6e5dc2167 | ||
| 
						 | 
					460147fea2 | ||
| 
						 | 
					cea44834bb | ||
| 
						 | 
					1af50fd7b8 | ||
| 
						 | 
					b18013025f | ||
| 
						 | 
					399eb60809 | ||
| 
						 | 
					ed67e3506b | ||
| 
						 | 
					d8ff37fc45 | ||
| 
						 | 
					2fcc3bb1ea | ||
| 
						 | 
					2e680c3d1e | ||
| 
						 | 
					af0a0ef41b | ||
| 
						 | 
					bbfccb0bbf | ||
| 
						 | 
					c89eb5d69f | ||
| 
						 | 
					ebde84214e | ||
| 
						 | 
					03fbae7b6d | ||
| 
						 | 
					f90e9596d4 | ||
| 
						 | 
					944f9524e2 | ||
| 
						 | 
					c61050244e | ||
| 
						 | 
					90337adbbc | ||
| 
						 | 
					7b67e41c5b | ||
| 
						 | 
					91db24fcfc | ||
| 
						 | 
					bb53db905f | ||
| 
						 | 
					0e9a1efe46 | ||
| 
						 | 
					289cd3e200 | ||
| 
						 | 
					e0f847e539 | ||
| 
						 | 
					c2842b486e | ||
| 
						 | 
					7235ade42f | ||
| 
						 | 
					850be2df1d | ||
| 
						 | 
					d504501440 | ||
| 
						 | 
					208392f12c | ||
| 
						 | 
					0fe036c640 | ||
| 
						 | 
					a40c41f0b0 | ||
| 
						 | 
					4affa5b710 | ||
| 
						 | 
					4eb574d991 | ||
| 
						 | 
					2c1577ea24 | ||
| 
						 | 
					b87e7e50b6 | ||
| 
						 | 
					36215d50bd | ||
| 
						 | 
					5ff1245d0c | ||
| 
						 | 
					ebd189fb27 | ||
| 
						 | 
					6f724827bd | ||
| 
						 | 
					b6a0982012 | ||
| 
						 | 
					c3e375e8a5 | ||
| 
						 | 
					302409fd83 | ||
| 
						 | 
					a2046461c1 | ||
| 
						 | 
					6660c34120 | ||
| 
						 | 
					b88ccf0ddd | ||
| 
						 | 
					b898bbf94c | ||
| 
						 | 
					787e89eb95 | ||
| 
						 | 
					1022c2c438 | ||
| 
						 | 
					ba21c62ed4 | ||
| 
						 | 
					bfe66c919b | ||
| 
						 | 
					3dacf7f661 | ||
| 
						 | 
					c0a3ae2612 | ||
| 
						 | 
					da612ef789 | ||
| 
						 | 
					df9cb7cf6e | ||
| 
						 | 
					9c1a26110e | ||
| 
						 | 
					0883d18a6c | ||
| 
						 | 
					c7246c61a5 | ||
| 
						 | 
					c5a1431fc0 | ||
| 
						 | 
					f0118a0dff | ||
| 
						 | 
					cffe96e46f | ||
| 
						 | 
					a9256578f0 | ||
| 
						 | 
					05ed202904 | ||
| 
						 | 
					963b63389a | ||
| 
						 | 
					e04706dc74 | ||
| 
						 | 
					04d4ce5ce1 | ||
| 
						 | 
					24cf3730fa | ||
| 
						 | 
					0700be86e2 | ||
| 
						 | 
					7cca509eb3 | ||
| 
						 | 
					7d7193cb63 | ||
| 
						 | 
					1cf10d05ff | ||
| 
						 | 
					2ec25a7729 | ||
| 
						 | 
					2a9065a61e | ||
| 
						 | 
					7518e30dcf | ||
| 
						 | 
					dc3c80e3ce | ||
| 
						 | 
					a25f61f6be | ||
| 
						 | 
					e70fb71a04 | ||
| 
						 | 
					f499630c2b | ||
| 
						 | 
					43319a8588 | ||
| 
						 | 
					d62b943c5d | ||
| 
						 | 
					8baddf2ea3 | ||
| 
						 | 
					600482660b | ||
| 
						 | 
					72ab5c143e | ||
| 
						 | 
					96ab0e7b4c | ||
| 
						 | 
					b60903e2b4 | ||
| 
						 | 
					b4f4d3f267 | ||
| 
						 | 
					6e017c86e8 | ||
| 
						 | 
					afcfc2dca5 | ||
| 
						 | 
					59e22a12a9 | ||
| 
						 | 
					b740ac3e01 | ||
| 
						 | 
					9719f0df03 | ||
| 
						 | 
					d4be599538 | ||
| 
						 | 
					f88195c90a | ||
| 
						 | 
					3b33f7e752 | ||
| 
						 | 
					67a37294f7 | ||
| 
						 | 
					fd88955696 | ||
| 
						 | 
					9d248dbb5a | ||
| 
						 | 
					20ec4104c6 | ||
| 
						 | 
					6c232d116d | ||
| 
						 | 
					2ef78bcd40 | ||
| 
						 | 
					94ce658ab9 | ||
| 
						 | 
					d8cf4cd341 | ||
| 
						 | 
					0360337df9 | ||
| 
						 | 
					119d38ea08 | ||
| 
						 | 
					bee77afb7f | ||
| 
						 | 
					16d4b16872 | ||
| 
						 | 
					951b2346ab | ||
| 
						 | 
					b29ff0e94b | ||
| 
						 | 
					c8dd8341ca | ||
| 
						 | 
					8bcf44bc16 | ||
| 
						 | 
					50b37a8420 | ||
| 
						 | 
					22df795733 | ||
| 
						 | 
					7e3bf06db1 | ||
| 
						 | 
					6630ca595c | ||
| 
						 | 
					5d01e19ce7 | ||
| 
						 | 
					56df89f8dd | ||
| 
						 | 
					13de984ce3 | ||
| 
						 | 
					15fc0e30d7 | ||
| 
						 | 
					4289c11185 | ||
| 
						 | 
					a3f564e702 | ||
| 
						 | 
					f6734a0c98 | ||
| 
						 | 
					72fb416239 | ||
| 
						 | 
					833f5b09d2 | ||
| 
						 | 
					b21b21f30a | ||
| 
						 | 
					2f77a3f6d2 | ||
| 
						 | 
					0bda655452 | ||
| 
						 | 
					4f80bb7031 | ||
| 
						 | 
					fbe7b3cc9b | ||
| 
						 | 
					8402f0abd7 | ||
| 
						 | 
					149b2ee5a7 | ||
| 
						 | 
					f9d5af0600 | ||
| 
						 | 
					72c4ccaee8 | ||
| 
						 | 
					92999dcaf2 | ||
| 
						 | 
					5bbd318518 | ||
| 
						 | 
					8807894890 | ||
| 
						 | 
					63b7820717 | ||
| 
						 | 
					9e7e2d6977 | ||
| 
						 | 
					89e4c280ae | ||
| 
						 | 
					b6c9f29be4 | ||
| 
						 | 
					74cbbc84ed | ||
| 
						 | 
					ead4197670 | ||
| 
						 | 
					4fc69ccdc8 | ||
| 
						 | 
					f556cb44b9 | ||
| 
						 | 
					45b540d375 | ||
| 
						 | 
					af2d36a3c9 | ||
| 
						 | 
					42a4f92cfa | ||
| 
						 | 
					ccb9ed3489 | ||
| 
						 | 
					773b2aa3d1 | ||
| 
						 | 
					30d5b8d65b | ||
| 
						 | 
					763676a18c | ||
| 
						 | 
					e166ad6780 | ||
| 
						 | 
					034c96d070 | ||
| 
						 | 
					f34f8d304c | ||
| 
						 | 
					944000c05c | ||
| 
						 | 
					e2503cdb47 | ||
| 
						 | 
					52db63bca2 | ||
| 
						 | 
					55dfd9e2a1 | ||
| 
						 | 
					d193cbf2b7 | ||
| 
						 | 
					bdec56a543 | ||
| 
						 | 
					e0a6d9740c | ||
| 
						 | 
					0ce9c057e1 | ||
| 
						 | 
					12a2fdbc20 | ||
| 
						 | 
					57c294bc89 | ||
| 
						 | 
					9758757805 | ||
| 
						 | 
					f9350fa35f | ||
| 
						 | 
					e120da4ecd | ||
| 
						 | 
					328a10b70c | ||
| 
						 | 
					1ed97c8deb | ||
| 
						 | 
					91b970e2aa | ||
| 
						 | 
					99af1bb479 | ||
| 
						 | 
					11ddcbdee3 | ||
| 
						 | 
					6e8a1086d8 | ||
| 
						 | 
					c78945436e | ||
| 
						 | 
					6eff8fde74 | ||
| 
						 | 
					726d5a177e | ||
| 
						 | 
					33495b5cb3 | ||
| 
						 | 
					fe159a13a9 | ||
| 
						 | 
					22a1dc0566 | ||
| 
						 | 
					02e6b732e9 | ||
| 
						 | 
					cc6fa135ac | ||
| 
						 | 
					5747732156 | ||
| 
						 | 
					581d1617d8 | ||
| 
						 | 
					6152fd20bf | ||
| 
						 | 
					19300ca65c | ||
| 
						 | 
					2f3d744e19 | ||
| 
						 | 
					724e812972 | ||
| 
						 | 
					9a6246fd4e | ||
| 
						 | 
					34f44de59c | 
@@ -55,3 +55,7 @@ twitter:
 | 
			
		||||
 | 
			
		||||
  # インテグレーション用アプリのコンシューマーシークレット
 | 
			
		||||
  consumer_secret:
 | 
			
		||||
 | 
			
		||||
# true にすると、リモートのファイルをキャッシュしなくなります(直リンクします)。
 | 
			
		||||
# ストレージ容量を節約することができますが、「リモートメディアを表示しない」設定をオンにしているユーザーは、リモートの画像などは見えなくなります。
 | 
			
		||||
preventCache: false
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -11,4 +11,4 @@ npm-debug.log
 | 
			
		||||
run.bat
 | 
			
		||||
api-docs.json
 | 
			
		||||
package-lock.json
 | 
			
		||||
yarn.lock
 | 
			
		||||
*.log
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
> Lead Maintainer: [syuilo][syuilo-link]
 | 
			
		||||
 | 
			
		||||
**[Misskey](https://misskey.xyz)** is a completely open source,
 | 
			
		||||
ultimately sophisticated new type of mini-blog based SNS.
 | 
			
		||||
ultimately sophisticated professional microblogging software.
 | 
			
		||||
 | 
			
		||||
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
 | 
			
		||||
 | 
			
		||||
@@ -23,6 +23,7 @@ ultimately sophisticated new type of mini-blog based SNS.
 | 
			
		||||
* Reactions
 | 
			
		||||
* User lists
 | 
			
		||||
* Customizable column view (known as MisskeyDeck)
 | 
			
		||||
  * and widgets!
 | 
			
		||||
* Private messages
 | 
			
		||||
* Mute
 | 
			
		||||
* Streaming
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								cli/update-remote-user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								cli/update-remote-user.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
const updatePerson = require('../built/remote/activitypub/models/person').updatePerson;
 | 
			
		||||
 | 
			
		||||
const args = process.argv.slice(2);
 | 
			
		||||
const user = args[0];
 | 
			
		||||
 | 
			
		||||
console.log(`Updating ${user}...`);
 | 
			
		||||
 | 
			
		||||
updatePerson(user).then(() => {
 | 
			
		||||
	console.log(`Updated ${user}`);
 | 
			
		||||
}, e => {
 | 
			
		||||
	console.error(e);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										123
									
								
								locales/de.yml
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								locales/de.yml
									
									
									
									
									
								
							@@ -57,18 +57,20 @@ common:
 | 
			
		||||
    memo: "Notizen"
 | 
			
		||||
    trends: "Trends"
 | 
			
		||||
    photo-stream: "Bilder"
 | 
			
		||||
    posts-monitor: "投稿チャート"
 | 
			
		||||
    slideshow: "Diashow"
 | 
			
		||||
    version: "Version"
 | 
			
		||||
    broadcast: "ブロードキャスト"
 | 
			
		||||
    notifications: "Benachrichtigungen"
 | 
			
		||||
    users: "Empfohlene Benutzer"
 | 
			
		||||
    polls: "Umfragen"
 | 
			
		||||
    post-form: "投稿フォーム"
 | 
			
		||||
    polls: "アンケート"
 | 
			
		||||
    post-form: "Beitragsform"
 | 
			
		||||
    messaging: "Nachrichten"
 | 
			
		||||
    server: "Server-Info"
 | 
			
		||||
    donation: "Spenden"
 | 
			
		||||
    nav: "Navigation"
 | 
			
		||||
    tips: "Tipps"
 | 
			
		||||
    hashtags: "ハッシュタグ"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "Widget hinzufügen:"
 | 
			
		||||
    home: "Startseite"
 | 
			
		||||
@@ -83,7 +85,7 @@ common:
 | 
			
		||||
    remove: "Spalte löschen"
 | 
			
		||||
    add-column: "Eine Spalte hinzufügen"
 | 
			
		||||
    rename: "Umbenennen"
 | 
			
		||||
    stack-left: "左に重ねる"
 | 
			
		||||
    stack-left: "Nach links schichten"
 | 
			
		||||
    pop-right: "右に出す"
 | 
			
		||||
common/views/components/connect-failed.vue:
 | 
			
		||||
  title: "Verbindung zum Server ist fehlgeschlagen"
 | 
			
		||||
@@ -220,6 +222,12 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "Fotostream"
 | 
			
		||||
  no-photos: "Keine Fotos"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "投稿チャート"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "ハッシュタグ"
 | 
			
		||||
  count: "{}人が投稿"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "Serverinformationen"
 | 
			
		||||
  toggle: "Sicht umschalten"
 | 
			
		||||
@@ -325,7 +333,7 @@ desktop/views/components/friends-maker.vue:
 | 
			
		||||
  refresh: "Mehr"
 | 
			
		||||
  close: "Schließen"
 | 
			
		||||
desktop/views/components/game-window.vue:
 | 
			
		||||
  game: "オセロ"
 | 
			
		||||
  game: "Othello"
 | 
			
		||||
desktop/views/components/home.vue:
 | 
			
		||||
  done: "Verbunden"
 | 
			
		||||
  add-widget: "Widget hinzufügen:"
 | 
			
		||||
@@ -362,7 +370,7 @@ desktop/views/components/notifications.vue:
 | 
			
		||||
desktop/views/components/post-form.vue:
 | 
			
		||||
  reply-placeholder: "Antworte auf diese Anmerkung..."
 | 
			
		||||
  quote-placeholder: "Zitiere diese Anmerkung..."
 | 
			
		||||
  submit: "投稿"
 | 
			
		||||
  submit: "Beitragsform"
 | 
			
		||||
  reply: "Antworten"
 | 
			
		||||
  renote: "Anmerkung"
 | 
			
		||||
  posted: "Gepostet!"
 | 
			
		||||
@@ -397,52 +405,52 @@ desktop/views/components/renote-form-window.vue:
 | 
			
		||||
desktop/views/components/settings-window.vue:
 | 
			
		||||
  settings: "Experimentelles"
 | 
			
		||||
desktop/views/components/settings.vue:
 | 
			
		||||
  profile: "プロフィール"
 | 
			
		||||
  profile: "Profil"
 | 
			
		||||
  notification: "Mitteilungen"
 | 
			
		||||
  apps: "In App öffnen"
 | 
			
		||||
  mute: "Stummschalten"
 | 
			
		||||
  drive: "Dateien vom Drive anfügen"
 | 
			
		||||
  security: "セキュリティ"
 | 
			
		||||
  security: "Sicherheit"
 | 
			
		||||
  signin: "サインイン履歴"
 | 
			
		||||
  password: "パスワード"
 | 
			
		||||
  2fa: "二段階認証"
 | 
			
		||||
  other: "その他"
 | 
			
		||||
  license: "ライセンス"
 | 
			
		||||
  password: "Passwort"
 | 
			
		||||
  2fa: "Zwei-Faktor-Authentifizierung"
 | 
			
		||||
  other: "Anderes"
 | 
			
		||||
  license: "Lizenz"
 | 
			
		||||
  behaviour: "Verhalten"
 | 
			
		||||
  fetch-on-scroll: "スクロールで自動読み込み"
 | 
			
		||||
  fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
 | 
			
		||||
  auto-popout: "ウィンドウの自動ポップアウト"
 | 
			
		||||
  auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
 | 
			
		||||
  fetch-on-scroll: "Aktualisieren beim scrollen"
 | 
			
		||||
  fetch-on-scroll-desc: "Wenn du runterscrollst empfängt die Seite automatisch zusätzliche Inhalte."
 | 
			
		||||
  auto-popout: "Automatische Pop-out Fenster"
 | 
			
		||||
  auto-popout-desc: "Pop-out ein offenes Fenster wenn möglich. Diese Einstellung wird im Browser gespeichert."
 | 
			
		||||
  advanced: "Erweiterte Einstellungen"
 | 
			
		||||
  api-via-stream: "API-Anfrage via stream"
 | 
			
		||||
  api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
 | 
			
		||||
  api-via-stream-desc: "API-Anfrage über WebSocket statt native Aktualisierungs-API (für bessere Leistung). Diese Einstellung wird im Browser gespeichert."
 | 
			
		||||
  display: "Erscheinungsbild und Anzeige"
 | 
			
		||||
  customize: "Startseite anpassen"
 | 
			
		||||
  dark-mode: "Nacht Modus"
 | 
			
		||||
  circle-icons: "Kreisförmige Icons"
 | 
			
		||||
  gradient-window-header: "Übergang in Fensterköpfen"
 | 
			
		||||
  post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
 | 
			
		||||
  show-reply-target: "リプライ先を表示する"
 | 
			
		||||
  show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
 | 
			
		||||
  show-renoted-my-notes: "Renoteされた自分の投稿をタイムラインに表示する"
 | 
			
		||||
  show-maps: "マップの自動展開"
 | 
			
		||||
  show-maps-desc: "位置情報が添付された投稿のマップを自動的に展開します。"
 | 
			
		||||
  sound: "サウンド"
 | 
			
		||||
  enable-sounds: "サウンドを有効にする"
 | 
			
		||||
  enable-sounds-desc: "投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。"
 | 
			
		||||
  volume: "ボリューム"
 | 
			
		||||
  test: "テスト"
 | 
			
		||||
  show-reply-target: "Zeige Antworten"
 | 
			
		||||
  show-my-renotes: "Zeige meine Reposts auf der Zeitleiste"
 | 
			
		||||
  show-renoted-my-notes: "Zeige meine Reposts, die geteilt wurden, auf der Zeitleiste"
 | 
			
		||||
  show-maps: "Karte anzeigen"
 | 
			
		||||
  show-maps-desc: "Zeige den Standort zu diesem Beitrag automatisch an."
 | 
			
		||||
  sound: "Ton"
 | 
			
		||||
  enable-sounds: "Ton aktivieren"
 | 
			
		||||
  enable-sounds-desc: "Spiel einen Ton ab beim Erhalten eines Beitrags bzw. einer Nachricht. Diese Einstellung wird im Browser gespeichert."
 | 
			
		||||
  volume: "Lautstärke"
 | 
			
		||||
  test: "Test"
 | 
			
		||||
  mobile: "Mobil"
 | 
			
		||||
  disable-via-mobile: "Diesen Beitrag nicht mit 'vom Handy' absenden"
 | 
			
		||||
  language: "Sprache"
 | 
			
		||||
  pick-language: "Sprache auswählen"
 | 
			
		||||
  recommended: "Empfohlen"
 | 
			
		||||
  auto: "Automatisch"
 | 
			
		||||
  specify-language: "言語を指定"
 | 
			
		||||
  specify-language: "Sprache auswählen"
 | 
			
		||||
  language-desc: "変更はページの再度読み込み後に反映されます。"
 | 
			
		||||
  cache: "キャッシュ"
 | 
			
		||||
  clean-cache: "クリーンアップ"
 | 
			
		||||
  cache-warn: "クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。"
 | 
			
		||||
  cache-warn: "Der Cache deines Benutzerkontos (Info, Beiträge, Antworten, Direktnachrichten, Einstellungen), die lokal im Browser gespeichert sind werden gelöscht.\nDu musst die Seite aktualisieren nachdem du aufgeräumt hast."
 | 
			
		||||
  cache-cleared: "キャッシュを削除しました"
 | 
			
		||||
  cache-cleared-desc: "ページを再度読み込みしてください。"
 | 
			
		||||
  auto-watch: "投稿の自動ウォッチ"
 | 
			
		||||
@@ -451,9 +459,9 @@ desktop/views/components/settings.vue:
 | 
			
		||||
  operator: "このサーバーの運営者"
 | 
			
		||||
  update: "Misskey Update"
 | 
			
		||||
  version: "バージョン:"
 | 
			
		||||
  latest-version: "最新のバージョン:"
 | 
			
		||||
  update-checking: "アップデートを確認中"
 | 
			
		||||
  do-update: "アップデートを確認"
 | 
			
		||||
  latest-version: "Neuste Version:"
 | 
			
		||||
  update-checking: "Suche nach Updates"
 | 
			
		||||
  do-update: "Suche nach Updates"
 | 
			
		||||
  update-settings: "詳細設定"
 | 
			
		||||
  prevent-update: "アップデートを延期する(非推奨)"
 | 
			
		||||
  prevent-update-desc: "この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効です。"
 | 
			
		||||
@@ -465,20 +473,20 @@ desktop/views/components/settings.vue:
 | 
			
		||||
  debug-mode: "デバッグモードを有効にする"
 | 
			
		||||
  debug-mode-desc: "この設定はブラウザに記憶されます。"
 | 
			
		||||
  experimental: "実験的機能を有効にする"
 | 
			
		||||
  experimental-desc: "実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。"
 | 
			
		||||
  tools: "ツール"
 | 
			
		||||
  task-manager: "タスクマネージャ"
 | 
			
		||||
  experimental-desc: "Experimentelle Funktionen können die Stabilität von Misskey beeinträchtigen. Diese Einstellung wird im Browser gespeichert."
 | 
			
		||||
  tools: "Werkzeuge"
 | 
			
		||||
  task-manager: "Taskmanager"
 | 
			
		||||
  third-parties: "サードパーティ"
 | 
			
		||||
desktop/views/components/settings.2fa.vue:
 | 
			
		||||
  intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。"
 | 
			
		||||
  detail: "詳細..."
 | 
			
		||||
  url: "https://www.google.co.jp/intl/ja/landing/2step/"
 | 
			
		||||
  url: "https://www.google.de/intl/de/landing/2step/"
 | 
			
		||||
  caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなりますのでご注意ください。"
 | 
			
		||||
  register: "デバイスを登録する"
 | 
			
		||||
  already-registered: "既に設定は完了しています。"
 | 
			
		||||
  unregister: "設定を解除"
 | 
			
		||||
  unregistered: "二段階認証が無効になりました。"
 | 
			
		||||
  enter-password: "パスワードを入力してください"
 | 
			
		||||
  register: "Ein Gerät registrieren"
 | 
			
		||||
  already-registered: "Das Gerät wurde bereits registriert"
 | 
			
		||||
  unregister: "Abschalten"
 | 
			
		||||
  unregistered: "Zwei-Faktor-Authentifizierung wurde deaktiviert."
 | 
			
		||||
  enter-password: "Bitte Passwort eingeben"
 | 
			
		||||
  authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:"
 | 
			
		||||
  howtoinstall: "インストール方法はこちら"
 | 
			
		||||
  scan: "次に、表示されているQRコードをスキャンします:"
 | 
			
		||||
@@ -493,16 +501,16 @@ desktop/views/components/settings.api.vue:
 | 
			
		||||
  regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
 | 
			
		||||
  regenerate-token: "トークンを再生成"
 | 
			
		||||
  token: "Token:"
 | 
			
		||||
  enter-password: "パスワードを入力してください"
 | 
			
		||||
  enter-password: "Bitte Passwort eingeben"
 | 
			
		||||
desktop/views/components/settings.apps.vue:
 | 
			
		||||
  no-apps: "連携しているアプリケーションはありません"
 | 
			
		||||
desktop/views/components/settings.mute.vue:
 | 
			
		||||
  no-users: "ミュートしているユーザーはいません"
 | 
			
		||||
desktop/views/components/settings.password.vue:
 | 
			
		||||
  reset: "パスワードを変更する"
 | 
			
		||||
  enter-current-password: "現在のパスワードを入力してください"
 | 
			
		||||
  enter-new-password: "新しいパスワードを入力してください"
 | 
			
		||||
  enter-new-password-again: "もう一度新しいパスワードを入力してください"
 | 
			
		||||
  enter-current-password: "Derzeitiges Passwort eingeben"
 | 
			
		||||
  enter-new-password: "Neues Passwort eingeben"
 | 
			
		||||
  enter-new-password-again: "Neues Passwort erneut eingeben"
 | 
			
		||||
  not-match: "新しいパスワードが一致しません"
 | 
			
		||||
  changed: "パスワードを変更しました"
 | 
			
		||||
desktop/views/components/settings.profile.vue:
 | 
			
		||||
@@ -519,9 +527,9 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "タスクマネージャ"
 | 
			
		||||
  title: "Taskmanager"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
  home: "Home"
 | 
			
		||||
  local: "Lokal"
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "ポップアウト"
 | 
			
		||||
  close: "閉じる"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
  is-media-view: "メディアビュー"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "{}がRenote"
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -632,7 +647,7 @@ desktop/views/widgets/notifications.vue:
 | 
			
		||||
  title: "通知"
 | 
			
		||||
  settings: "通知の設定"
 | 
			
		||||
desktop/views/widgets/polls.vue:
 | 
			
		||||
  title: "投票"
 | 
			
		||||
  title: "アンケート"
 | 
			
		||||
  refresh: "他を見る"
 | 
			
		||||
  nothing: "ありません!"
 | 
			
		||||
desktop/views/widgets/post-form.vue:
 | 
			
		||||
@@ -731,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
mobile/views/components/timeline.vue:
 | 
			
		||||
  empty: "投稿がありません"
 | 
			
		||||
  load-more: "もっと"
 | 
			
		||||
@@ -789,7 +804,7 @@ mobile/views/pages/notifications.vue:
 | 
			
		||||
  notifications: "通知"
 | 
			
		||||
  read-all: "すべての通知を既読にしますか?"
 | 
			
		||||
mobile/views/pages/settings/settings.profile.vue:
 | 
			
		||||
  title: "プロフィール"
 | 
			
		||||
  title: "Profil"
 | 
			
		||||
  name: "名前"
 | 
			
		||||
  account: "アカウント"
 | 
			
		||||
  location: "場所"
 | 
			
		||||
@@ -799,7 +814,7 @@ mobile/views/pages/settings/settings.profile.vue:
 | 
			
		||||
  banner: "バナー"
 | 
			
		||||
  is-cat: "このアカウントはCatです"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
  saved: "プロフィールを保存しました"
 | 
			
		||||
  saved: "Profil wurde aktualisiert"
 | 
			
		||||
  uploading: "アップロード中"
 | 
			
		||||
  upload-failed: "アップロードに失敗しました"
 | 
			
		||||
mobile/views/pages/search.vue:
 | 
			
		||||
@@ -813,7 +828,7 @@ mobile/views/pages/settings.vue:
 | 
			
		||||
  lang-tip: "変更はページの再読み込み後に反映されます。"
 | 
			
		||||
  recommended: "推奨"
 | 
			
		||||
  auto: "自動"
 | 
			
		||||
  specify-language: "言語を指定"
 | 
			
		||||
  specify-language: "Sprache auswählen"
 | 
			
		||||
  design: "デザインと表示"
 | 
			
		||||
  dark-mode: "ダークモード"
 | 
			
		||||
  i-am-under-limited-internet: "私は通信を制限されている"
 | 
			
		||||
@@ -836,9 +851,9 @@ mobile/views/pages/settings.vue:
 | 
			
		||||
  twitter-disconnect: "切断する"
 | 
			
		||||
  update: "Misskey Update"
 | 
			
		||||
  version: "バージョン:"
 | 
			
		||||
  latest-version: "最新のバージョン:"
 | 
			
		||||
  update-checking: "アップデートを確認中"
 | 
			
		||||
  check-for-updates: "アップデートを確認"
 | 
			
		||||
  latest-version: "Neuste Version:"
 | 
			
		||||
  update-checking: "Suche nach Updates"
 | 
			
		||||
  check-for-updates: "Suche nach Updates"
 | 
			
		||||
  no-updates: "利用可能な更新はありません"
 | 
			
		||||
  no-updates-desc: "お使いのMisskeyは最新です。"
 | 
			
		||||
  update-available: "新しいバージョンが利用可能です"
 | 
			
		||||
 
 | 
			
		||||
@@ -54,9 +54,10 @@ common:
 | 
			
		||||
    timemachine: "Calendar (Time Machine)"
 | 
			
		||||
    activity: "Activity"
 | 
			
		||||
    rss: "RSS reader"
 | 
			
		||||
    memo: "Memo"
 | 
			
		||||
    memo: "Sticky note"
 | 
			
		||||
    trends: "Trends"
 | 
			
		||||
    photo-stream: "Photo stream"
 | 
			
		||||
    posts-monitor: "Chart of posts"
 | 
			
		||||
    slideshow: "Slideshow"
 | 
			
		||||
    version: "Version"
 | 
			
		||||
    broadcast: "Broadcast"
 | 
			
		||||
@@ -69,6 +70,7 @@ common:
 | 
			
		||||
    donation: "Donation"
 | 
			
		||||
    nav: "Navigation"
 | 
			
		||||
    tips: "Tips"
 | 
			
		||||
    hashtags: "Hashtags"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "Widgets"
 | 
			
		||||
    home: "Home"
 | 
			
		||||
@@ -220,11 +222,17 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "Photostream"
 | 
			
		||||
  no-photos: "No photos"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "Chart of posts"
 | 
			
		||||
  toggle: "Toggle views"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "Hashtags"
 | 
			
		||||
  count: "{} users mentioned"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "Server info"
 | 
			
		||||
  toggle: "Toggle views"
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "Memo"
 | 
			
		||||
  title: "Sticky note"
 | 
			
		||||
  memo: "Write here!"
 | 
			
		||||
  save: "Save"
 | 
			
		||||
desktop/views/components/activity.chart.vue:
 | 
			
		||||
@@ -426,7 +434,7 @@ desktop/views/components/settings.vue:
 | 
			
		||||
  show-my-renotes: "Show my reposts in the timeline"
 | 
			
		||||
  show-renoted-my-notes: "Show my posts that have been shared in the timeline"
 | 
			
		||||
  show-maps: "Show the map"
 | 
			
		||||
  show-maps-desc: "Automatically show the map of the location attached to the post."
 | 
			
		||||
  show-maps-desc: "Automatically show the location on the map attached to this post."
 | 
			
		||||
  sound: "Sound"
 | 
			
		||||
  enable-sounds: "Enable sound"
 | 
			
		||||
  enable-sounds-desc: "Play a sound when you receive a post/message. This setting is stored in the browser."
 | 
			
		||||
@@ -519,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "this post is private"
 | 
			
		||||
  deleted: "this post has been deleted"
 | 
			
		||||
  media-count: "{} media attached"
 | 
			
		||||
  poll: "Polls"
 | 
			
		||||
  poll: "Poll"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "Task Manager"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "Popout"
 | 
			
		||||
  close: "Close"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "Only media posts"
 | 
			
		||||
  is-media-view: "Media view"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "Reposted by {}"
 | 
			
		||||
  private: "this post is private"
 | 
			
		||||
  deleted: "this post has been deleted"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "about"
 | 
			
		||||
  gotit: "Got it!"
 | 
			
		||||
 
 | 
			
		||||
@@ -54,21 +54,23 @@ common:
 | 
			
		||||
    timemachine: "カレンダー(タイムマシン)"
 | 
			
		||||
    activity: "アクティビティ"
 | 
			
		||||
    rss: "RSSリーダー"
 | 
			
		||||
    memo: "メモ"
 | 
			
		||||
    memo: "付箋"
 | 
			
		||||
    trends: "トレンド"
 | 
			
		||||
    photo-stream: "フォトストリーム"
 | 
			
		||||
    posts-monitor: "投稿チャート"
 | 
			
		||||
    slideshow: "スライドショー"
 | 
			
		||||
    version: "バージョン"
 | 
			
		||||
    broadcast: "ブロードキャスト"
 | 
			
		||||
    notifications: "通知"
 | 
			
		||||
    users: "おすすめユーザー"
 | 
			
		||||
    polls: "投票"
 | 
			
		||||
    polls: "アンケート"
 | 
			
		||||
    post-form: "投稿フォーム"
 | 
			
		||||
    messaging: "メッセージ"
 | 
			
		||||
    server: "サーバー情報"
 | 
			
		||||
    donation: "寄付のお願い"
 | 
			
		||||
    nav: "ナビゲーション"
 | 
			
		||||
    tips: "ヒント"
 | 
			
		||||
    hashtags: "ハッシュタグ"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "ウィジェット"
 | 
			
		||||
    home: "ホーム"
 | 
			
		||||
@@ -150,11 +152,11 @@ common/views/components/poll.vue:
 | 
			
		||||
  show-result: "結果を見る"
 | 
			
		||||
  voted: "投票済み"
 | 
			
		||||
common/views/components/poll-editor.vue:
 | 
			
		||||
  no-only-one-choice: "投票には、選択肢が最低2つ必要です"
 | 
			
		||||
  no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
 | 
			
		||||
  choice-n: "選択肢{}"
 | 
			
		||||
  remove: "この選択肢を削除"
 | 
			
		||||
  add: "+選択肢を追加"
 | 
			
		||||
  destroy: "投票を破棄"
 | 
			
		||||
  destroy: "アンケートを破棄"
 | 
			
		||||
common/views/components/reaction-picker.vue:
 | 
			
		||||
  choose-reaction: "リアクションを選択"
 | 
			
		||||
common/views/components/signin.vue:
 | 
			
		||||
@@ -220,11 +222,17 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "フォトストリーム"
 | 
			
		||||
  no-photos: "写真はありません"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "投稿チャート"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "ハッシュタグ"
 | 
			
		||||
  count: "{}人が投稿"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "サーバー情報"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "メモ"
 | 
			
		||||
  title: "付箋"
 | 
			
		||||
  memo: "ここに書いて!"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
desktop/views/components/activity.chart.vue:
 | 
			
		||||
@@ -376,7 +384,7 @@ desktop/views/components/post-form.vue:
 | 
			
		||||
  attach-media-from-drive: "ドライブからメディアを添付"
 | 
			
		||||
  attach-cancel: "添付取り消し"
 | 
			
		||||
  insert-a-kao: "v(‘ω’)v"
 | 
			
		||||
  create-poll: "投票を作成"
 | 
			
		||||
  create-poll: "アンケートを作成"
 | 
			
		||||
  text-remain: "残り{}文字"
 | 
			
		||||
desktop/views/components/post-form-window.vue:
 | 
			
		||||
  note: "新規投稿"
 | 
			
		||||
@@ -519,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "タスクマネージャ"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "ポップアウト"
 | 
			
		||||
  close: "閉じる"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
  is-media-view: "メディアビュー"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "{}がRenote"
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -632,7 +647,7 @@ desktop/views/widgets/notifications.vue:
 | 
			
		||||
  title: "通知"
 | 
			
		||||
  settings: "通知の設定"
 | 
			
		||||
desktop/views/widgets/polls.vue:
 | 
			
		||||
  title: "投票"
 | 
			
		||||
  title: "アンケート"
 | 
			
		||||
  refresh: "他を見る"
 | 
			
		||||
  nothing: "ありません!"
 | 
			
		||||
desktop/views/widgets/post-form.vue:
 | 
			
		||||
@@ -731,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
mobile/views/components/timeline.vue:
 | 
			
		||||
  empty: "投稿がありません"
 | 
			
		||||
  load-more: "もっと"
 | 
			
		||||
 
 | 
			
		||||
@@ -54,9 +54,10 @@ common:
 | 
			
		||||
    timemachine: "カレンダー(タイムマシン)"
 | 
			
		||||
    activity: "Activité"
 | 
			
		||||
    rss: "Lecteur de flux RSS"
 | 
			
		||||
    memo: "Note"
 | 
			
		||||
    memo: "Pense-bête"
 | 
			
		||||
    trends: "Tendances"
 | 
			
		||||
    photo-stream: "Flux de photos"
 | 
			
		||||
    posts-monitor: "Graph des publications"
 | 
			
		||||
    slideshow: "Diaporama"
 | 
			
		||||
    version: "Version"
 | 
			
		||||
    broadcast: "Diffusion"
 | 
			
		||||
@@ -69,6 +70,7 @@ common:
 | 
			
		||||
    donation: "Dons"
 | 
			
		||||
    nav: "Navigation"
 | 
			
		||||
    tips: "Conseils"
 | 
			
		||||
    hashtags: "ハッシュタグ"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "Widgets"
 | 
			
		||||
    home: "Accueil"
 | 
			
		||||
@@ -150,11 +152,11 @@ common/views/components/poll.vue:
 | 
			
		||||
  show-result: "Montrer les résultats"
 | 
			
		||||
  voted: "Voté"
 | 
			
		||||
common/views/components/poll-editor.vue:
 | 
			
		||||
  no-only-one-choice: "Vous devez entrer au moins deux choix"
 | 
			
		||||
  no-only-one-choice: "Vous devez saisir au moins deux choix."
 | 
			
		||||
  choice-n: "Choix {}"
 | 
			
		||||
  remove: "Supprimer ce choix"
 | 
			
		||||
  add: "+ Ajouter un choix"
 | 
			
		||||
  destroy: "Supprimer ce sondage"
 | 
			
		||||
  destroy: "Annuler ce sondage"
 | 
			
		||||
common/views/components/reaction-picker.vue:
 | 
			
		||||
  choose-reaction: "Choisissez votre réaction"
 | 
			
		||||
common/views/components/signin.vue:
 | 
			
		||||
@@ -220,11 +222,17 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "Flux de photo"
 | 
			
		||||
  no-photos: "Pas de photos"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "Graph des publications"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "ハッシュタグ"
 | 
			
		||||
  count: "{}人が投稿"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "Info sur le serveur"
 | 
			
		||||
  toggle: "Afficher les vues"
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "Note"
 | 
			
		||||
  title: "Pense-bête"
 | 
			
		||||
  memo: "Écrivez ici !"
 | 
			
		||||
  save: "Enregistrer"
 | 
			
		||||
desktop/views/components/activity.chart.vue:
 | 
			
		||||
@@ -410,7 +418,7 @@ desktop/views/components/settings.vue:
 | 
			
		||||
  license: "License"
 | 
			
		||||
  behaviour: "Comportement"
 | 
			
		||||
  fetch-on-scroll: "Chargement lors du défilement"
 | 
			
		||||
  fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
 | 
			
		||||
  fetch-on-scroll-desc: "Chargement automatique du contenu lors du défilement de la page."
 | 
			
		||||
  auto-popout: "Fenêtre contextuelle automatique"
 | 
			
		||||
  auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
 | 
			
		||||
  advanced: "Paramètres avancés"
 | 
			
		||||
@@ -519,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "cette publication est privée"
 | 
			
		||||
  deleted: "cette publication a été supprimée"
 | 
			
		||||
  media-count: "{} médias attachés"
 | 
			
		||||
  poll: "Sondages"
 | 
			
		||||
  poll: "Sondage"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "Gestionnaire de tâches"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "ポップアウト"
 | 
			
		||||
  close: "Fermer"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
  is-media-view: "メディアビュー"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "Reposté par {}"
 | 
			
		||||
  private: "cette publication est privée"
 | 
			
		||||
  deleted: "cette publication a été supprimée"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "à propos"
 | 
			
		||||
  gotit: "J'ai compris !"
 | 
			
		||||
@@ -659,7 +674,7 @@ mobile/views/components/drive.vue:
 | 
			
		||||
  nothing-in-drive: "Rien"
 | 
			
		||||
  folder-is-empty: "Ce dossier est vide"
 | 
			
		||||
  prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
 | 
			
		||||
  deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
 | 
			
		||||
  deletion-alert: "Désolé ! La suppression d’un dossier n’est pas encore implémentée."
 | 
			
		||||
  folder-name: "Nom du dossier"
 | 
			
		||||
  root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
 | 
			
		||||
  root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
 | 
			
		||||
@@ -731,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "cette publication est privée"
 | 
			
		||||
  deleted: "cette publication a été supprimée"
 | 
			
		||||
  media-count: "{} médias attachés"
 | 
			
		||||
  poll: "Sondage"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
mobile/views/components/timeline.vue:
 | 
			
		||||
  empty: "Pas de notes"
 | 
			
		||||
  load-more: "Afficher plus"
 | 
			
		||||
 
 | 
			
		||||
@@ -54,21 +54,23 @@ common:
 | 
			
		||||
    timemachine: "カレンダー(タイムマシン)"
 | 
			
		||||
    activity: "アクティビティ"
 | 
			
		||||
    rss: "RSSリーダー"
 | 
			
		||||
    memo: "メモ"
 | 
			
		||||
    memo: "付箋"
 | 
			
		||||
    trends: "トレンド"
 | 
			
		||||
    photo-stream: "フォトストリーム"
 | 
			
		||||
    posts-monitor: "投稿チャート"
 | 
			
		||||
    slideshow: "スライドショー"
 | 
			
		||||
    version: "バージョン"
 | 
			
		||||
    broadcast: "ブロードキャスト"
 | 
			
		||||
    notifications: "通知"
 | 
			
		||||
    users: "おすすめユーザー"
 | 
			
		||||
    polls: "投票"
 | 
			
		||||
    polls: "アンケート"
 | 
			
		||||
    post-form: "投稿フォーム"
 | 
			
		||||
    messaging: "メッセージ"
 | 
			
		||||
    server: "サーバー情報"
 | 
			
		||||
    donation: "寄付のお願い"
 | 
			
		||||
    nav: "ナビゲーション"
 | 
			
		||||
    tips: "ヒント"
 | 
			
		||||
    hashtags: "ハッシュタグ"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "ウィジェット"
 | 
			
		||||
    home: "ホーム"
 | 
			
		||||
@@ -150,11 +152,11 @@ common/views/components/poll.vue:
 | 
			
		||||
  show-result: "結果を見る"
 | 
			
		||||
  voted: "投票済み"
 | 
			
		||||
common/views/components/poll-editor.vue:
 | 
			
		||||
  no-only-one-choice: "投票には、選択肢が最低2つ必要です"
 | 
			
		||||
  no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
 | 
			
		||||
  choice-n: "選択肢{}"
 | 
			
		||||
  remove: "この選択肢を削除"
 | 
			
		||||
  add: "+選択肢を追加"
 | 
			
		||||
  destroy: "投票を破棄"
 | 
			
		||||
  destroy: "アンケートを破棄"
 | 
			
		||||
common/views/components/reaction-picker.vue:
 | 
			
		||||
  choose-reaction: "リアクションを選択"
 | 
			
		||||
common/views/components/signin.vue:
 | 
			
		||||
@@ -220,11 +222,17 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "フォトストリーム"
 | 
			
		||||
  no-photos: "写真はありません"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "投稿チャート"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "ハッシュタグ"
 | 
			
		||||
  count: "{}人が投稿"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "サーバー情報"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "メモ"
 | 
			
		||||
  title: "付箋"
 | 
			
		||||
  memo: "ここに書いて!"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
desktop/views/components/activity.chart.vue:
 | 
			
		||||
@@ -376,7 +384,7 @@ desktop/views/components/post-form.vue:
 | 
			
		||||
  attach-media-from-drive: "ドライブからメディアを添付"
 | 
			
		||||
  attach-cancel: "添付取り消し"
 | 
			
		||||
  insert-a-kao: "v(‘ω’)v"
 | 
			
		||||
  create-poll: "投票を作成"
 | 
			
		||||
  create-poll: "アンケートを作成"
 | 
			
		||||
  text-remain: "残り{}文字"
 | 
			
		||||
desktop/views/components/post-form-window.vue:
 | 
			
		||||
  note: "新規投稿"
 | 
			
		||||
@@ -519,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "タスクマネージャ"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "ポップアウト"
 | 
			
		||||
  close: "閉じる"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
  is-media-view: "メディアビュー"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "{}がRenote"
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -632,7 +647,7 @@ desktop/views/widgets/notifications.vue:
 | 
			
		||||
  title: "通知"
 | 
			
		||||
  settings: "通知の設定"
 | 
			
		||||
desktop/views/widgets/polls.vue:
 | 
			
		||||
  title: "投票"
 | 
			
		||||
  title: "アンケート"
 | 
			
		||||
  refresh: "他を見る"
 | 
			
		||||
  nothing: "ありません!"
 | 
			
		||||
desktop/views/widgets/post-form.vue:
 | 
			
		||||
@@ -731,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
mobile/views/components/timeline.vue:
 | 
			
		||||
  empty: "投稿がありません"
 | 
			
		||||
  load-more: "もっと"
 | 
			
		||||
 
 | 
			
		||||
@@ -60,21 +60,23 @@ common:
 | 
			
		||||
    timemachine: "カレンダー(タイムマシン)"
 | 
			
		||||
    activity: "アクティビティ"
 | 
			
		||||
    rss: "RSSリーダー"
 | 
			
		||||
    memo: "メモ"
 | 
			
		||||
    memo: "付箋"
 | 
			
		||||
    trends: "トレンド"
 | 
			
		||||
    photo-stream: "フォトストリーム"
 | 
			
		||||
    posts-monitor: "投稿チャート"
 | 
			
		||||
    slideshow: "スライドショー"
 | 
			
		||||
    version: "バージョン"
 | 
			
		||||
    broadcast: "ブロードキャスト"
 | 
			
		||||
    notifications: "通知"
 | 
			
		||||
    users: "おすすめユーザー"
 | 
			
		||||
    polls: "投票"
 | 
			
		||||
    polls: "アンケート"
 | 
			
		||||
    post-form: "投稿フォーム"
 | 
			
		||||
    messaging: "メッセージ"
 | 
			
		||||
    server: "サーバー情報"
 | 
			
		||||
    donation: "寄付のお願い"
 | 
			
		||||
    nav: "ナビゲーション"
 | 
			
		||||
    tips: "ヒント"
 | 
			
		||||
    hashtags: "ハッシュタグ"
 | 
			
		||||
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "ウィジェット"
 | 
			
		||||
@@ -167,11 +169,11 @@ common/views/components/poll.vue:
 | 
			
		||||
  voted: "投票済み"
 | 
			
		||||
 | 
			
		||||
common/views/components/poll-editor.vue:
 | 
			
		||||
  no-only-one-choice: "投票には、選択肢が最低2つ必要です"
 | 
			
		||||
  no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
 | 
			
		||||
  choice-n: "選択肢{}"
 | 
			
		||||
  remove: "この選択肢を削除"
 | 
			
		||||
  add: "+選択肢を追加"
 | 
			
		||||
  destroy: "投票を破棄"
 | 
			
		||||
  destroy: "アンケートを破棄"
 | 
			
		||||
 | 
			
		||||
common/views/components/reaction-picker.vue:
 | 
			
		||||
  choose-reaction: "リアクションを選択"
 | 
			
		||||
@@ -249,12 +251,21 @@ common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "フォトストリーム"
 | 
			
		||||
  no-photos: "写真はありません"
 | 
			
		||||
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "投稿チャート"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "ハッシュタグ"
 | 
			
		||||
  count: "{}人が投稿"
 | 
			
		||||
  empty: "トレンドなし"
 | 
			
		||||
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "サーバー情報"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "メモ"
 | 
			
		||||
  title: "付箋"
 | 
			
		||||
  memo: "ここに書いて!"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
 | 
			
		||||
@@ -433,7 +444,7 @@ desktop/views/components/post-form.vue:
 | 
			
		||||
  attach-media-from-drive: "ドライブからメディアを添付"
 | 
			
		||||
  attach-cancel: "添付取り消し"
 | 
			
		||||
  insert-a-kao: "v(‘ω’)v"
 | 
			
		||||
  create-poll: "投票を作成"
 | 
			
		||||
  create-poll: "アンケートを作成"
 | 
			
		||||
  text-remain: "残り{}文字"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/post-form-window.vue:
 | 
			
		||||
@@ -599,7 +610,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "タスクマネージャ"
 | 
			
		||||
@@ -663,6 +674,15 @@ desktop/views/components/window.vue:
 | 
			
		||||
  popout: "ポップアウト"
 | 
			
		||||
  close: "閉じる"
 | 
			
		||||
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
  is-media-view: "メディアビュー"
 | 
			
		||||
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "{}がRenote"
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -742,7 +762,7 @@ desktop/views/widgets/notifications.vue:
 | 
			
		||||
  settings: "通知の設定"
 | 
			
		||||
 | 
			
		||||
desktop/views/widgets/polls.vue:
 | 
			
		||||
  title: "投票"
 | 
			
		||||
  title: "アンケート"
 | 
			
		||||
  refresh: "他を見る"
 | 
			
		||||
  nothing: "ありません!"
 | 
			
		||||
 | 
			
		||||
@@ -860,7 +880,7 @@ mobile/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
 | 
			
		||||
mobile/views/components/timeline.vue:
 | 
			
		||||
  empty: "投稿がありません"
 | 
			
		||||
 
 | 
			
		||||
@@ -54,21 +54,23 @@ common:
 | 
			
		||||
    timemachine: "カレンダー(タイムマシン)"
 | 
			
		||||
    activity: "アクティビティ"
 | 
			
		||||
    rss: "RSSリーダー"
 | 
			
		||||
    memo: "メモ"
 | 
			
		||||
    memo: "付箋"
 | 
			
		||||
    trends: "トレンド"
 | 
			
		||||
    photo-stream: "フォトストリーム"
 | 
			
		||||
    posts-monitor: "投稿チャート"
 | 
			
		||||
    slideshow: "スライドショー"
 | 
			
		||||
    version: "バージョン"
 | 
			
		||||
    broadcast: "ブロードキャスト"
 | 
			
		||||
    notifications: "通知"
 | 
			
		||||
    users: "おすすめユーザー"
 | 
			
		||||
    polls: "投票"
 | 
			
		||||
    polls: "アンケート"
 | 
			
		||||
    post-form: "投稿フォーム"
 | 
			
		||||
    messaging: "メッセージ"
 | 
			
		||||
    server: "サーバー情報"
 | 
			
		||||
    donation: "寄付のお願い"
 | 
			
		||||
    nav: "ナビゲーション"
 | 
			
		||||
    tips: "ヒント"
 | 
			
		||||
    hashtags: "ハッシュタグ"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "ウィジェット"
 | 
			
		||||
    home: "ホーム"
 | 
			
		||||
@@ -150,11 +152,11 @@ common/views/components/poll.vue:
 | 
			
		||||
  show-result: "結果を見る"
 | 
			
		||||
  voted: "投票済み"
 | 
			
		||||
common/views/components/poll-editor.vue:
 | 
			
		||||
  no-only-one-choice: "投票には、選択肢が最低2つ必要です"
 | 
			
		||||
  no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
 | 
			
		||||
  choice-n: "選択肢{}"
 | 
			
		||||
  remove: "この選択肢を削除"
 | 
			
		||||
  add: "+選択肢を追加"
 | 
			
		||||
  destroy: "投票を破棄"
 | 
			
		||||
  destroy: "アンケートを破棄"
 | 
			
		||||
common/views/components/reaction-picker.vue:
 | 
			
		||||
  choose-reaction: "リアクションを選択"
 | 
			
		||||
common/views/components/signin.vue:
 | 
			
		||||
@@ -220,11 +222,17 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "フォトストリーム"
 | 
			
		||||
  no-photos: "写真はありません"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "投稿チャート"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "ハッシュタグ"
 | 
			
		||||
  count: "{}人が投稿"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "サーバー情報"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "メモ"
 | 
			
		||||
  title: "付箋"
 | 
			
		||||
  memo: "ここに書いて!"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
desktop/views/components/activity.chart.vue:
 | 
			
		||||
@@ -376,7 +384,7 @@ desktop/views/components/post-form.vue:
 | 
			
		||||
  attach-media-from-drive: "ドライブからメディアを添付"
 | 
			
		||||
  attach-cancel: "添付取り消し"
 | 
			
		||||
  insert-a-kao: "v(‘ω’)v"
 | 
			
		||||
  create-poll: "投票を作成"
 | 
			
		||||
  create-poll: "アンケートを作成"
 | 
			
		||||
  text-remain: "残り{}文字"
 | 
			
		||||
desktop/views/components/post-form-window.vue:
 | 
			
		||||
  note: "新規投稿"
 | 
			
		||||
@@ -519,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "タスクマネージャ"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "ポップアウト"
 | 
			
		||||
  close: "閉じる"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
  is-media-view: "メディアビュー"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "{}がRenote"
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -632,7 +647,7 @@ desktop/views/widgets/notifications.vue:
 | 
			
		||||
  title: "通知"
 | 
			
		||||
  settings: "通知の設定"
 | 
			
		||||
desktop/views/widgets/polls.vue:
 | 
			
		||||
  title: "投票"
 | 
			
		||||
  title: "アンケート"
 | 
			
		||||
  refresh: "他を見る"
 | 
			
		||||
  nothing: "ありません!"
 | 
			
		||||
desktop/views/widgets/post-form.vue:
 | 
			
		||||
@@ -731,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
mobile/views/components/timeline.vue:
 | 
			
		||||
  empty: "投稿がありません"
 | 
			
		||||
  load-more: "もっと"
 | 
			
		||||
 
 | 
			
		||||
@@ -54,9 +54,10 @@ common:
 | 
			
		||||
    timemachine: "Kalendarz (wehikuł czasu)"
 | 
			
		||||
    activity: "Aktywność"
 | 
			
		||||
    rss: "Czytnik RSS"
 | 
			
		||||
    memo: "Notatki"
 | 
			
		||||
    memo: "Notatka"
 | 
			
		||||
    trends: "Na czasie"
 | 
			
		||||
    photo-stream: "Photostream"
 | 
			
		||||
    posts-monitor: "Wykres wpisów"
 | 
			
		||||
    slideshow: "Pokaz slajdów"
 | 
			
		||||
    version: "Wersja"
 | 
			
		||||
    broadcast: "Transmisja"
 | 
			
		||||
@@ -69,6 +70,7 @@ common:
 | 
			
		||||
    donation: "Dotacje"
 | 
			
		||||
    nav: "Nawigacja"
 | 
			
		||||
    tips: "Wskazówki"
 | 
			
		||||
    hashtags: "Hashtagi"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "Widżety"
 | 
			
		||||
    home: "Strona główna"
 | 
			
		||||
@@ -150,11 +152,11 @@ common/views/components/poll.vue:
 | 
			
		||||
  show-result: "Pokaż wyniki"
 | 
			
		||||
  voted: "Zagłosowano"
 | 
			
		||||
common/views/components/poll-editor.vue:
 | 
			
		||||
  no-only-one-choice: "Musisz wprowadzić dwie lub więcej opcji."
 | 
			
		||||
  no-only-one-choice: "Musisz wprowadzić przynajmniej dwie opcje."
 | 
			
		||||
  choice-n: "Opcja {}"
 | 
			
		||||
  remove: "Usuń tą opcję"
 | 
			
		||||
  add: "+ Dodaj opcję"
 | 
			
		||||
  destroy: "Usuń ankietę"
 | 
			
		||||
  destroy: "Usuń tę ankietę"
 | 
			
		||||
common/views/components/reaction-picker.vue:
 | 
			
		||||
  choose-reaction: "Wybierz reakcję"
 | 
			
		||||
common/views/components/signin.vue:
 | 
			
		||||
@@ -220,11 +222,17 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "Photostream"
 | 
			
		||||
  no-photos: "Brak zdjęć"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "Wykres wpisów"
 | 
			
		||||
  toggle: "Przełącz widok"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "Hashtagi"
 | 
			
		||||
  count: "Wspomniany przez {} użytkowników"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "Informacje o serwerze"
 | 
			
		||||
  toggle: "Przełącz widok"
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "Notatki"
 | 
			
		||||
  title: "Notatka"
 | 
			
		||||
  memo: "Napisz tutaj!"
 | 
			
		||||
  save: "Zapisz"
 | 
			
		||||
desktop/views/components/activity.chart.vue:
 | 
			
		||||
@@ -519,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "ten wpis jest prywatny"
 | 
			
		||||
  deleted: "ten wpis został usunięty"
 | 
			
		||||
  media-count: "{}zawartości multimedialnej"
 | 
			
		||||
  poll: "Ankiety"
 | 
			
		||||
  poll: "Ankieta"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "Menedżer zadań"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "Pop-out"
 | 
			
		||||
  close: "Zamknij"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "Tylko wpisy z zawartością multimedialną"
 | 
			
		||||
  is-media-view: "Widok multimediów"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "Udostępniono przez {}"
 | 
			
		||||
  private: "ten wpis jest prywatny"
 | 
			
		||||
  deleted: "ten wpis został usunięty"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "O Misskey"
 | 
			
		||||
  gotit: "Rozumiem!"
 | 
			
		||||
 
 | 
			
		||||
@@ -54,21 +54,23 @@ common:
 | 
			
		||||
    timemachine: "カレンダー(タイムマシン)"
 | 
			
		||||
    activity: "アクティビティ"
 | 
			
		||||
    rss: "RSSリーダー"
 | 
			
		||||
    memo: "メモ"
 | 
			
		||||
    memo: "付箋"
 | 
			
		||||
    trends: "トレンド"
 | 
			
		||||
    photo-stream: "フォトストリーム"
 | 
			
		||||
    posts-monitor: "投稿チャート"
 | 
			
		||||
    slideshow: "スライドショー"
 | 
			
		||||
    version: "バージョン"
 | 
			
		||||
    broadcast: "ブロードキャスト"
 | 
			
		||||
    notifications: "通知"
 | 
			
		||||
    users: "おすすめユーザー"
 | 
			
		||||
    polls: "投票"
 | 
			
		||||
    polls: "アンケート"
 | 
			
		||||
    post-form: "投稿フォーム"
 | 
			
		||||
    messaging: "メッセージ"
 | 
			
		||||
    server: "サーバー情報"
 | 
			
		||||
    donation: "寄付のお願い"
 | 
			
		||||
    nav: "ナビゲーション"
 | 
			
		||||
    tips: "ヒント"
 | 
			
		||||
    hashtags: "ハッシュタグ"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "ウィジェット"
 | 
			
		||||
    home: "ホーム"
 | 
			
		||||
@@ -150,11 +152,11 @@ common/views/components/poll.vue:
 | 
			
		||||
  show-result: "結果を見る"
 | 
			
		||||
  voted: "投票済み"
 | 
			
		||||
common/views/components/poll-editor.vue:
 | 
			
		||||
  no-only-one-choice: "投票には、選択肢が最低2つ必要です"
 | 
			
		||||
  no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
 | 
			
		||||
  choice-n: "選択肢{}"
 | 
			
		||||
  remove: "この選択肢を削除"
 | 
			
		||||
  add: "+選択肢を追加"
 | 
			
		||||
  destroy: "投票を破棄"
 | 
			
		||||
  destroy: "アンケートを破棄"
 | 
			
		||||
common/views/components/reaction-picker.vue:
 | 
			
		||||
  choose-reaction: "リアクションを選択"
 | 
			
		||||
common/views/components/signin.vue:
 | 
			
		||||
@@ -220,11 +222,17 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "フォトストリーム"
 | 
			
		||||
  no-photos: "写真はありません"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "投稿チャート"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "ハッシュタグ"
 | 
			
		||||
  count: "{}人が投稿"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "サーバー情報"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "メモ"
 | 
			
		||||
  title: "付箋"
 | 
			
		||||
  memo: "ここに書いて!"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
desktop/views/components/activity.chart.vue:
 | 
			
		||||
@@ -376,7 +384,7 @@ desktop/views/components/post-form.vue:
 | 
			
		||||
  attach-media-from-drive: "ドライブからメディアを添付"
 | 
			
		||||
  attach-cancel: "添付取り消し"
 | 
			
		||||
  insert-a-kao: "v(‘ω’)v"
 | 
			
		||||
  create-poll: "投票を作成"
 | 
			
		||||
  create-poll: "アンケートを作成"
 | 
			
		||||
  text-remain: "残り{}文字"
 | 
			
		||||
desktop/views/components/post-form-window.vue:
 | 
			
		||||
  note: "新規投稿"
 | 
			
		||||
@@ -519,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "タスクマネージャ"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "ポップアウト"
 | 
			
		||||
  close: "閉じる"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
  is-media-view: "メディアビュー"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "{}がRenote"
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -632,7 +647,7 @@ desktop/views/widgets/notifications.vue:
 | 
			
		||||
  title: "通知"
 | 
			
		||||
  settings: "通知の設定"
 | 
			
		||||
desktop/views/widgets/polls.vue:
 | 
			
		||||
  title: "投票"
 | 
			
		||||
  title: "アンケート"
 | 
			
		||||
  refresh: "他を見る"
 | 
			
		||||
  nothing: "ありません!"
 | 
			
		||||
desktop/views/widgets/post-form.vue:
 | 
			
		||||
@@ -731,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
mobile/views/components/timeline.vue:
 | 
			
		||||
  empty: "投稿がありません"
 | 
			
		||||
  load-more: "もっと"
 | 
			
		||||
 
 | 
			
		||||
@@ -54,21 +54,23 @@ common:
 | 
			
		||||
    timemachine: "カレンダー(タイムマシン)"
 | 
			
		||||
    activity: "アクティビティ"
 | 
			
		||||
    rss: "RSSリーダー"
 | 
			
		||||
    memo: "メモ"
 | 
			
		||||
    memo: "付箋"
 | 
			
		||||
    trends: "トレンド"
 | 
			
		||||
    photo-stream: "フォトストリーム"
 | 
			
		||||
    posts-monitor: "投稿チャート"
 | 
			
		||||
    slideshow: "スライドショー"
 | 
			
		||||
    version: "バージョン"
 | 
			
		||||
    broadcast: "ブロードキャスト"
 | 
			
		||||
    notifications: "通知"
 | 
			
		||||
    users: "おすすめユーザー"
 | 
			
		||||
    polls: "投票"
 | 
			
		||||
    polls: "アンケート"
 | 
			
		||||
    post-form: "投稿フォーム"
 | 
			
		||||
    messaging: "メッセージ"
 | 
			
		||||
    server: "サーバー情報"
 | 
			
		||||
    donation: "寄付のお願い"
 | 
			
		||||
    nav: "ナビゲーション"
 | 
			
		||||
    tips: "ヒント"
 | 
			
		||||
    hashtags: "ハッシュタグ"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "ウィジェット"
 | 
			
		||||
    home: "ホーム"
 | 
			
		||||
@@ -150,11 +152,11 @@ common/views/components/poll.vue:
 | 
			
		||||
  show-result: "結果を見る"
 | 
			
		||||
  voted: "投票済み"
 | 
			
		||||
common/views/components/poll-editor.vue:
 | 
			
		||||
  no-only-one-choice: "投票には、選択肢が最低2つ必要です"
 | 
			
		||||
  no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
 | 
			
		||||
  choice-n: "選択肢{}"
 | 
			
		||||
  remove: "この選択肢を削除"
 | 
			
		||||
  add: "+選択肢を追加"
 | 
			
		||||
  destroy: "投票を破棄"
 | 
			
		||||
  destroy: "アンケートを破棄"
 | 
			
		||||
common/views/components/reaction-picker.vue:
 | 
			
		||||
  choose-reaction: "リアクションを選択"
 | 
			
		||||
common/views/components/signin.vue:
 | 
			
		||||
@@ -220,11 +222,17 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "フォトストリーム"
 | 
			
		||||
  no-photos: "写真はありません"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "投稿チャート"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "ハッシュタグ"
 | 
			
		||||
  count: "{}人が投稿"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "サーバー情報"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "メモ"
 | 
			
		||||
  title: "付箋"
 | 
			
		||||
  memo: "ここに書いて!"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
desktop/views/components/activity.chart.vue:
 | 
			
		||||
@@ -376,7 +384,7 @@ desktop/views/components/post-form.vue:
 | 
			
		||||
  attach-media-from-drive: "ドライブからメディアを添付"
 | 
			
		||||
  attach-cancel: "添付取り消し"
 | 
			
		||||
  insert-a-kao: "v(‘ω’)v"
 | 
			
		||||
  create-poll: "投票を作成"
 | 
			
		||||
  create-poll: "アンケートを作成"
 | 
			
		||||
  text-remain: "残り{}文字"
 | 
			
		||||
desktop/views/components/post-form-window.vue:
 | 
			
		||||
  note: "新規投稿"
 | 
			
		||||
@@ -519,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "タスクマネージャ"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "ポップアウト"
 | 
			
		||||
  close: "閉じる"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
  is-media-view: "メディアビュー"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "{}がRenote"
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -632,7 +647,7 @@ desktop/views/widgets/notifications.vue:
 | 
			
		||||
  title: "通知"
 | 
			
		||||
  settings: "通知の設定"
 | 
			
		||||
desktop/views/widgets/polls.vue:
 | 
			
		||||
  title: "投票"
 | 
			
		||||
  title: "アンケート"
 | 
			
		||||
  refresh: "他を見る"
 | 
			
		||||
  nothing: "ありません!"
 | 
			
		||||
desktop/views/widgets/post-form.vue:
 | 
			
		||||
@@ -731,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
mobile/views/components/timeline.vue:
 | 
			
		||||
  empty: "投稿がありません"
 | 
			
		||||
  load-more: "もっと"
 | 
			
		||||
 
 | 
			
		||||
@@ -54,21 +54,23 @@ common:
 | 
			
		||||
    timemachine: "カレンダー(タイムマシン)"
 | 
			
		||||
    activity: "アクティビティ"
 | 
			
		||||
    rss: "RSSリーダー"
 | 
			
		||||
    memo: "メモ"
 | 
			
		||||
    memo: "付箋"
 | 
			
		||||
    trends: "トレンド"
 | 
			
		||||
    photo-stream: "フォトストリーム"
 | 
			
		||||
    posts-monitor: "投稿チャート"
 | 
			
		||||
    slideshow: "スライドショー"
 | 
			
		||||
    version: "バージョン"
 | 
			
		||||
    broadcast: "ブロードキャスト"
 | 
			
		||||
    notifications: "通知"
 | 
			
		||||
    users: "おすすめユーザー"
 | 
			
		||||
    polls: "投票"
 | 
			
		||||
    polls: "アンケート"
 | 
			
		||||
    post-form: "投稿フォーム"
 | 
			
		||||
    messaging: "メッセージ"
 | 
			
		||||
    server: "サーバー情報"
 | 
			
		||||
    donation: "寄付のお願い"
 | 
			
		||||
    nav: "ナビゲーション"
 | 
			
		||||
    tips: "ヒント"
 | 
			
		||||
    hashtags: "ハッシュタグ"
 | 
			
		||||
  deck:
 | 
			
		||||
    widgets: "ウィジェット"
 | 
			
		||||
    home: "ホーム"
 | 
			
		||||
@@ -150,11 +152,11 @@ common/views/components/poll.vue:
 | 
			
		||||
  show-result: "結果を見る"
 | 
			
		||||
  voted: "投票済み"
 | 
			
		||||
common/views/components/poll-editor.vue:
 | 
			
		||||
  no-only-one-choice: "投票には、選択肢が最低2つ必要です"
 | 
			
		||||
  no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
 | 
			
		||||
  choice-n: "選択肢{}"
 | 
			
		||||
  remove: "この選択肢を削除"
 | 
			
		||||
  add: "+選択肢を追加"
 | 
			
		||||
  destroy: "投票を破棄"
 | 
			
		||||
  destroy: "アンケートを破棄"
 | 
			
		||||
common/views/components/reaction-picker.vue:
 | 
			
		||||
  choose-reaction: "リアクションを選択"
 | 
			
		||||
common/views/components/signin.vue:
 | 
			
		||||
@@ -220,11 +222,17 @@ common/views/widgets/donation.vue:
 | 
			
		||||
common/views/widgets/photo-stream.vue:
 | 
			
		||||
  title: "フォトストリーム"
 | 
			
		||||
  no-photos: "写真はありません"
 | 
			
		||||
common/views/widgets/posts-monitor.vue:
 | 
			
		||||
  title: "投稿チャート"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/hashtags.vue:
 | 
			
		||||
  title: "ハッシュタグ"
 | 
			
		||||
  count: "{}人が投稿"
 | 
			
		||||
common/views/widgets/server.vue:
 | 
			
		||||
  title: "サーバー情報"
 | 
			
		||||
  toggle: "表示を切り替え"
 | 
			
		||||
common/views/widgets/memo.vue:
 | 
			
		||||
  title: "メモ"
 | 
			
		||||
  title: "付箋"
 | 
			
		||||
  memo: "ここに書いて!"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
desktop/views/components/activity.chart.vue:
 | 
			
		||||
@@ -376,7 +384,7 @@ desktop/views/components/post-form.vue:
 | 
			
		||||
  attach-media-from-drive: "ドライブからメディアを添付"
 | 
			
		||||
  attach-cancel: "添付取り消し"
 | 
			
		||||
  insert-a-kao: "v(‘ω’)v"
 | 
			
		||||
  create-poll: "投票を作成"
 | 
			
		||||
  create-poll: "アンケートを作成"
 | 
			
		||||
  text-remain: "残り{}文字"
 | 
			
		||||
desktop/views/components/post-form-window.vue:
 | 
			
		||||
  note: "新規投稿"
 | 
			
		||||
@@ -519,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
desktop/views/components/taskmanager.vue:
 | 
			
		||||
  title: "タスクマネージャ"
 | 
			
		||||
desktop/views/components/timeline.vue:
 | 
			
		||||
@@ -569,6 +577,13 @@ desktop/views/components/users-list-item.vue:
 | 
			
		||||
desktop/views/components/window.vue:
 | 
			
		||||
  popout: "ポップアウト"
 | 
			
		||||
  close: "閉じる"
 | 
			
		||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
			
		||||
  is-media-only: "メディア投稿のみ"
 | 
			
		||||
  is-media-view: "メディアビュー"
 | 
			
		||||
desktop/views/pages/deck/deck.note.vue:
 | 
			
		||||
  reposted-by: "{}がRenote"
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -632,7 +647,7 @@ desktop/views/widgets/notifications.vue:
 | 
			
		||||
  title: "通知"
 | 
			
		||||
  settings: "通知の設定"
 | 
			
		||||
desktop/views/widgets/polls.vue:
 | 
			
		||||
  title: "投票"
 | 
			
		||||
  title: "アンケート"
 | 
			
		||||
  refresh: "他を見る"
 | 
			
		||||
  nothing: "ありません!"
 | 
			
		||||
desktop/views/widgets/post-form.vue:
 | 
			
		||||
@@ -731,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
  media-count: "{}つのメディア"
 | 
			
		||||
  poll: "投票"
 | 
			
		||||
  poll: "アンケート"
 | 
			
		||||
mobile/views/components/timeline.vue:
 | 
			
		||||
  empty: "投稿がありません"
 | 
			
		||||
  load-more: "もっと"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										126
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										126
									
								
								package.json
									
									
									
									
									
								
							@@ -1,8 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"author": "syuilo <i@syuilo.com>",
 | 
			
		||||
	"version": "2.32.0",
 | 
			
		||||
	"clientVersion": "1.0.6285",
 | 
			
		||||
	"version": "2.38.3",
 | 
			
		||||
	"clientVersion": "1.0.6486",
 | 
			
		||||
	"codename": "nighthike",
 | 
			
		||||
	"main": "./built/index.js",
 | 
			
		||||
	"private": true,
 | 
			
		||||
@@ -29,12 +29,71 @@
 | 
			
		||||
		"@fortawesome/fontawesome-free-solid": "5.0.2",
 | 
			
		||||
		"@koa/cors": "2.2.1",
 | 
			
		||||
		"@prezzemolo/rap": "0.1.2",
 | 
			
		||||
		"autwh": "0.1.0",
 | 
			
		||||
		"bcryptjs": "2.4.3",
 | 
			
		||||
		"cafy": "8.0.0",
 | 
			
		||||
		"chalk": "2.4.1",
 | 
			
		||||
		"crc-32": "1.2.0",
 | 
			
		||||
		"debug": "3.1.0",
 | 
			
		||||
		"deepcopy": "0.6.3",
 | 
			
		||||
		"diskusage": "0.2.4",
 | 
			
		||||
		"elasticsearch": "15.0.0",
 | 
			
		||||
		"emojilib": "2.2.12",
 | 
			
		||||
		"escape-regexp": "0.0.1",
 | 
			
		||||
		"file-type": "8.0.0",
 | 
			
		||||
		"gm": "1.23.1",
 | 
			
		||||
		"http-signature": "1.2.0",
 | 
			
		||||
		"is-root": "2.0.0",
 | 
			
		||||
		"is-url": "1.2.4",
 | 
			
		||||
		"js-yaml": "3.11.0",
 | 
			
		||||
		"jsdom": "11.11.0",
 | 
			
		||||
		"koa": "2.5.1",
 | 
			
		||||
		"koa-bodyparser": "4.2.1",
 | 
			
		||||
		"koa-compress": "3.0.0",
 | 
			
		||||
		"koa-favicon": "2.0.1",
 | 
			
		||||
		"koa-json-body": "5.3.0",
 | 
			
		||||
		"koa-logger": "3.2.0",
 | 
			
		||||
		"koa-mount": "3.0.0",
 | 
			
		||||
		"koa-multer": "1.0.2",
 | 
			
		||||
		"koa-router": "7.4.0",
 | 
			
		||||
		"koa-send": "4.1.3",
 | 
			
		||||
		"koa-slow": "2.1.0",
 | 
			
		||||
		"koa-views": "6.1.4",
 | 
			
		||||
		"kue": "0.11.6",
 | 
			
		||||
		"mongodb": "3.0.10",
 | 
			
		||||
		"monk": "6.0.6",
 | 
			
		||||
		"ms": "2.1.1",
 | 
			
		||||
		"nopt": "4.0.1",
 | 
			
		||||
		"os-utils": "0.0.14",
 | 
			
		||||
		"parse5": "5.0.0",
 | 
			
		||||
		"prominence": "0.2.0",
 | 
			
		||||
		"promise-sequential": "1.1.1",
 | 
			
		||||
		"punycode": "2.1.1",
 | 
			
		||||
		"qrcode": "1.2.0",
 | 
			
		||||
		"ratelimiter": "3.0.3",
 | 
			
		||||
		"recaptcha-promise": "0.1.3",
 | 
			
		||||
		"reconnecting-websocket": "3.2.2",
 | 
			
		||||
		"redis": "2.8.0",
 | 
			
		||||
		"request": "2.87.0",
 | 
			
		||||
		"request-promise-native": "1.0.5",
 | 
			
		||||
		"rndstr": "1.0.0",
 | 
			
		||||
		"speakeasy": "2.0.0",
 | 
			
		||||
		"summaly": "2.0.6",
 | 
			
		||||
		"tcp-port-used": "0.1.2",
 | 
			
		||||
		"tmp": "0.0.33",
 | 
			
		||||
		"uuid": "3.2.1",
 | 
			
		||||
		"web-push": "3.3.1",
 | 
			
		||||
		"webfinger.js": "2.6.6",
 | 
			
		||||
		"websocket": "1.0.26",
 | 
			
		||||
		"ws": "5.2.0",
 | 
			
		||||
		"xev": "2.0.1"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@prezzemolo/zip": "0.0.3",
 | 
			
		||||
		"@types/bcryptjs": "2.4.1",
 | 
			
		||||
		"@types/debug": "0.0.30",
 | 
			
		||||
		"@types/deep-equal": "1.0.1",
 | 
			
		||||
		"@types/elasticsearch": "5.0.23",
 | 
			
		||||
		"@types/eventemitter3": "2.0.2",
 | 
			
		||||
		"@types/gm": "1.18.0",
 | 
			
		||||
		"@types/gulp": "3.8.36",
 | 
			
		||||
		"@types/gulp-htmlmin": "1.3.32",
 | 
			
		||||
@@ -63,7 +122,6 @@
 | 
			
		||||
		"@types/mkdirp": "0.5.2",
 | 
			
		||||
		"@types/mocha": "5.2.0",
 | 
			
		||||
		"@types/mongodb": "3.0.18",
 | 
			
		||||
		"@types/monk": "6.0.0",
 | 
			
		||||
		"@types/ms": "0.7.30",
 | 
			
		||||
		"@types/node": "10.1.2",
 | 
			
		||||
		"@types/nopt": "3.0.29",
 | 
			
		||||
@@ -86,30 +144,17 @@
 | 
			
		||||
		"@types/ws": "5.1.1",
 | 
			
		||||
		"animejs": "2.2.0",
 | 
			
		||||
		"autosize": "4.0.2",
 | 
			
		||||
		"autwh": "0.1.0",
 | 
			
		||||
		"bcryptjs": "2.4.3",
 | 
			
		||||
		"bootstrap-vue": "2.0.0-rc.6",
 | 
			
		||||
		"cafy": "8.0.0",
 | 
			
		||||
		"chalk": "2.4.1",
 | 
			
		||||
		"crc-32": "1.2.0",
 | 
			
		||||
		"css-loader": "0.28.11",
 | 
			
		||||
		"debug": "3.1.0",
 | 
			
		||||
		"deep-equal": "1.0.1",
 | 
			
		||||
		"deepcopy": "0.6.3",
 | 
			
		||||
		"diskusage": "0.2.4",
 | 
			
		||||
		"dompurify": "1.0.4",
 | 
			
		||||
		"elasticsearch": "15.0.0",
 | 
			
		||||
		"element-ui": "2.3.9",
 | 
			
		||||
		"emojilib": "2.2.12",
 | 
			
		||||
		"escape-regexp": "0.0.1",
 | 
			
		||||
		"eslint": "4.19.1",
 | 
			
		||||
		"eslint-plugin-vue": "4.5.0",
 | 
			
		||||
		"eventemitter3": "3.1.0",
 | 
			
		||||
		"exif-js": "2.3.0",
 | 
			
		||||
		"file-loader": "1.1.11",
 | 
			
		||||
		"file-type": "8.0.0",
 | 
			
		||||
		"fuckadblock": "3.2.1",
 | 
			
		||||
		"gm": "1.23.1",
 | 
			
		||||
		"gulp": "3.9.1",
 | 
			
		||||
		"gulp-cssnano": "2.1.3",
 | 
			
		||||
		"gulp-htmlmin": "4.0.0",
 | 
			
		||||
@@ -127,71 +172,32 @@
 | 
			
		||||
		"hard-source-webpack-plugin": "0.6.10",
 | 
			
		||||
		"highlight.js": "9.12.0",
 | 
			
		||||
		"html-minifier": "3.5.16",
 | 
			
		||||
		"http-signature": "1.2.0",
 | 
			
		||||
		"inquirer": "5.2.0",
 | 
			
		||||
		"is-root": "2.0.0",
 | 
			
		||||
		"is-url": "1.2.4",
 | 
			
		||||
		"js-yaml": "3.11.0",
 | 
			
		||||
		"jsdom": "11.11.0",
 | 
			
		||||
		"koa": "2.5.1",
 | 
			
		||||
		"koa-bodyparser": "4.2.1",
 | 
			
		||||
		"koa-compress": "3.0.0",
 | 
			
		||||
		"koa-favicon": "2.0.1",
 | 
			
		||||
		"koa-json-body": "5.3.0",
 | 
			
		||||
		"koa-logger": "3.2.0",
 | 
			
		||||
		"koa-mount": "3.0.0",
 | 
			
		||||
		"koa-multer": "1.0.2",
 | 
			
		||||
		"koa-router": "7.4.0",
 | 
			
		||||
		"koa-send": "4.1.3",
 | 
			
		||||
		"koa-slow": "2.1.0",
 | 
			
		||||
		"koa-views": "6.1.4",
 | 
			
		||||
		"kue": "0.11.6",
 | 
			
		||||
		"license-checker": "20.0.0",
 | 
			
		||||
		"loader-utils": "1.1.0",
 | 
			
		||||
		"mecab-async": "0.1.2",
 | 
			
		||||
		"mkdirp": "0.5.1",
 | 
			
		||||
		"mocha": "5.2.0",
 | 
			
		||||
		"moji": "0.5.1",
 | 
			
		||||
		"mongodb": "3.0.8",
 | 
			
		||||
		"monk": "6.0.6",
 | 
			
		||||
		"ms": "2.1.1",
 | 
			
		||||
		"nan": "2.10.0",
 | 
			
		||||
		"node-sass": "4.9.0",
 | 
			
		||||
		"node-sass-json-importer": "3.2.0",
 | 
			
		||||
		"nopt": "4.0.1",
 | 
			
		||||
		"nprogress": "0.2.0",
 | 
			
		||||
		"object-assign-deep": "0.4.0",
 | 
			
		||||
		"on-build-webpack": "0.1.0",
 | 
			
		||||
		"os-utils": "0.0.14",
 | 
			
		||||
		"parse5": "5.0.0",
 | 
			
		||||
		"progress-bar-webpack-plugin": "1.11.0",
 | 
			
		||||
		"prominence": "0.2.0",
 | 
			
		||||
		"promise-sequential": "1.1.1",
 | 
			
		||||
		"pug": "2.0.3",
 | 
			
		||||
		"punycode": "2.1.1",
 | 
			
		||||
		"qrcode": "1.2.0",
 | 
			
		||||
		"ratelimiter": "3.0.3",
 | 
			
		||||
		"recaptcha-promise": "0.1.3",
 | 
			
		||||
		"reconnecting-websocket": "3.2.2",
 | 
			
		||||
		"redis": "2.8.0",
 | 
			
		||||
		"request": "2.87.0",
 | 
			
		||||
		"request-promise-native": "1.0.5",
 | 
			
		||||
		"rimraf": "2.6.2",
 | 
			
		||||
		"rndstr": "1.0.0",
 | 
			
		||||
		"s-age": "1.1.2",
 | 
			
		||||
		"sass-loader": "7.0.1",
 | 
			
		||||
		"seedrandom": "2.4.3",
 | 
			
		||||
		"single-line-log": "1.1.2",
 | 
			
		||||
		"speakeasy": "2.0.0",
 | 
			
		||||
		"style-loader": "0.21.0",
 | 
			
		||||
		"stylus": "0.54.5",
 | 
			
		||||
		"stylus-loader": "3.0.2",
 | 
			
		||||
		"summaly": "2.0.6",
 | 
			
		||||
		"swagger-jsdoc": "1.9.7",
 | 
			
		||||
		"syuilo-password-strength": "0.0.1",
 | 
			
		||||
		"tcp-port-used": "0.1.2",
 | 
			
		||||
		"textarea-caret": "3.1.0",
 | 
			
		||||
		"tmp": "0.0.33",
 | 
			
		||||
		"ts-loader": "4.3.0",
 | 
			
		||||
		"ts-node": "6.0.4",
 | 
			
		||||
		"tslint": "5.10.0",
 | 
			
		||||
@@ -199,7 +205,6 @@
 | 
			
		||||
		"typescript-eslint-parser": "15.0.0",
 | 
			
		||||
		"uglify-es": "3.3.9",
 | 
			
		||||
		"url-loader": "1.0.1",
 | 
			
		||||
		"uuid": "3.2.1",
 | 
			
		||||
		"v-animate-css": "0.0.2",
 | 
			
		||||
		"vue": "2.5.16",
 | 
			
		||||
		"vue-cropperjs": "2.2.0",
 | 
			
		||||
@@ -212,12 +217,7 @@
 | 
			
		||||
		"vuedraggable": "2.16.0",
 | 
			
		||||
		"vuex": "3.0.1",
 | 
			
		||||
		"vuex-persistedstate": "^2.5.4",
 | 
			
		||||
		"web-push": "3.3.1",
 | 
			
		||||
		"webfinger.js": "2.6.6",
 | 
			
		||||
		"webpack": "4.9.1",
 | 
			
		||||
		"webpack-cli": "2.1.4",
 | 
			
		||||
		"websocket": "1.0.26",
 | 
			
		||||
		"ws": "5.2.0",
 | 
			
		||||
		"xev": "2.0.0"
 | 
			
		||||
		"webpack-cli": "2.1.4"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ html
 | 
			
		||||
			| JavaScriptを有効にしてください
 | 
			
		||||
			br
 | 
			
		||||
			| Please turn on your JavaScript
 | 
			
		||||
		div#ini: p
 | 
			
		||||
			span .
 | 
			
		||||
			span .
 | 
			
		||||
			span .
 | 
			
		||||
		div#ini.
 | 
			
		||||
			<svg viewBox="0 0 50 50">
 | 
			
		||||
				<path fill=#{themeColor} d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z" />
 | 
			
		||||
			</svg>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,15 +3,15 @@ import StreamManager from './stream-manager';
 | 
			
		||||
import MiOS from '../../../mios';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Server stream connection
 | 
			
		||||
 * Notes stats stream connection
 | 
			
		||||
 */
 | 
			
		||||
export class ServerStream extends Stream {
 | 
			
		||||
export class NotesStatsStream extends Stream {
 | 
			
		||||
	constructor(os: MiOS) {
 | 
			
		||||
		super(os, 'server');
 | 
			
		||||
		super(os, 'notes-stats');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ServerStreamManager extends StreamManager<ServerStream> {
 | 
			
		||||
export class NotesStatsStreamManager extends StreamManager<NotesStatsStream> {
 | 
			
		||||
	private os: MiOS;
 | 
			
		||||
 | 
			
		||||
	constructor(os: MiOS) {
 | 
			
		||||
@@ -22,7 +22,7 @@ export class ServerStreamManager extends StreamManager<ServerStream> {
 | 
			
		||||
 | 
			
		||||
	public getConnection() {
 | 
			
		||||
		if (this.connection == null) {
 | 
			
		||||
			this.connection = new ServerStream(this.os);
 | 
			
		||||
			this.connection = new NotesStatsStream(this.os);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return this.connection;
 | 
			
		||||
							
								
								
									
										30
									
								
								src/client/app/common/scripts/streaming/server-stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/client/app/common/scripts/streaming/server-stats.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import Stream from './stream';
 | 
			
		||||
import StreamManager from './stream-manager';
 | 
			
		||||
import MiOS from '../../../mios';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Server stats stream connection
 | 
			
		||||
 */
 | 
			
		||||
export class ServerStatsStream extends Stream {
 | 
			
		||||
	constructor(os: MiOS) {
 | 
			
		||||
		super(os, 'server-stats');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ServerStatsStreamManager extends StreamManager<ServerStatsStream> {
 | 
			
		||||
	private os: MiOS;
 | 
			
		||||
 | 
			
		||||
	constructor(os: MiOS) {
 | 
			
		||||
		super();
 | 
			
		||||
 | 
			
		||||
		this.os = os;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getConnection() {
 | 
			
		||||
		if (this.connection == null) {
 | 
			
		||||
			this.connection = new ServerStatsStream(this.os);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return this.connection;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -2,6 +2,7 @@ import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
import analogClock from './analog-clock.vue';
 | 
			
		||||
import menu from './menu.vue';
 | 
			
		||||
import noteHeader from './note-header.vue';
 | 
			
		||||
import signin from './signin.vue';
 | 
			
		||||
import signup from './signup.vue';
 | 
			
		||||
import forkit from './forkit.vue';
 | 
			
		||||
@@ -31,6 +32,7 @@ import welcomeTimeline from './welcome-timeline.vue';
 | 
			
		||||
 | 
			
		||||
Vue.component('mk-analog-clock', analogClock);
 | 
			
		||||
Vue.component('mk-menu', menu);
 | 
			
		||||
Vue.component('mk-note-header', noteHeader);
 | 
			
		||||
Vue.component('mk-signin', signin);
 | 
			
		||||
Vue.component('mk-signup', signup);
 | 
			
		||||
Vue.component('mk-forkit', forkit);
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,20 @@ import Vue from 'vue';
 | 
			
		||||
import * as anime from 'animejs';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: ['source', 'compact', 'items'],
 | 
			
		||||
	props: {
 | 
			
		||||
		source: {
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		items: {
 | 
			
		||||
			type: Array,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		compact: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			hukidasi: !this.compact
 | 
			
		||||
@@ -44,13 +57,13 @@ export default Vue.extend({
 | 
			
		||||
				top = y;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (left + width > window.innerWidth) {
 | 
			
		||||
				left = window.innerWidth - width;
 | 
			
		||||
			if (left + width - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
				left = window.innerWidth - width + window.pageXOffset;
 | 
			
		||||
				this.hukidasi = false;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (top + height > window.innerHeight) {
 | 
			
		||||
				top = window.innerHeight - height;
 | 
			
		||||
			if (top + height - window.pageYOffset > window.innerHeight) {
 | 
			
		||||
				top = window.innerHeight - height + window.pageYOffset;
 | 
			
		||||
				this.hukidasi = false;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -139,9 +152,13 @@ $border-color = rgba(27, 31, 35, 0.15)
 | 
			
		||||
			transform-origin center -($balloon-size)
 | 
			
		||||
 | 
			
		||||
			&:before
 | 
			
		||||
			&:after
 | 
			
		||||
				content ""
 | 
			
		||||
				display block
 | 
			
		||||
				position absolute
 | 
			
		||||
				pointer-events none
 | 
			
		||||
 | 
			
		||||
			&:before
 | 
			
		||||
				top -($balloon-size * 2)
 | 
			
		||||
				left s('calc(50% - %s)', $balloon-size)
 | 
			
		||||
				border-top solid $balloon-size transparent
 | 
			
		||||
@@ -150,9 +167,6 @@ $border-color = rgba(27, 31, 35, 0.15)
 | 
			
		||||
				border-bottom solid $balloon-size $border-color
 | 
			
		||||
 | 
			
		||||
			&:after
 | 
			
		||||
				content ""
 | 
			
		||||
				display block
 | 
			
		||||
				position absolute
 | 
			
		||||
				top -($balloon-size * 2) + 1.5px
 | 
			
		||||
				left s('calc(50% - %s)', $balloon-size)
 | 
			
		||||
				border-top solid $balloon-size transparent
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										117
									
								
								src/client/app/common/views/components/note-header.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/client/app/common/views/components/note-header.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
<template>
 | 
			
		||||
<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
 | 
			
		||||
	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
 | 
			
		||||
	<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
 | 
			
		||||
	<span class="is-admin" v-if="note.user.isAdmin">admin</span>
 | 
			
		||||
	<span class="is-bot" v-if="note.user.isBot">bot</span>
 | 
			
		||||
	<span class="is-cat" v-if="note.user.isCat">cat</span>
 | 
			
		||||
	<span class="username"><mk-acct :user="note.user"/></span>
 | 
			
		||||
	<div class="info">
 | 
			
		||||
		<span class="app" v-if="note.app && !mini">via <b>{{ note.app.name }}</b></span>
 | 
			
		||||
		<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
 | 
			
		||||
		<router-link class="created-at" :to="note | notePage">
 | 
			
		||||
			<mk-time :time="note.createdAt"/>
 | 
			
		||||
		</router-link>
 | 
			
		||||
		<span class="visibility" v-if="note.visibility != 'public'">
 | 
			
		||||
			<template v-if="note.visibility == 'home'">%fa:home%</template>
 | 
			
		||||
			<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
 | 
			
		||||
			<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
 | 
			
		||||
			<template v-if="note.visibility == 'private'">%fa:lock%</template>
 | 
			
		||||
		</span>
 | 
			
		||||
	</div>
 | 
			
		||||
</header>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: {
 | 
			
		||||
		note: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		mini: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
@import '~const.styl'
 | 
			
		||||
 | 
			
		||||
root(isDark)
 | 
			
		||||
	display flex
 | 
			
		||||
	align-items baseline
 | 
			
		||||
	white-space nowrap
 | 
			
		||||
 | 
			
		||||
	> .avatar
 | 
			
		||||
		flex-shrink 0
 | 
			
		||||
		margin-right 8px
 | 
			
		||||
		width 20px
 | 
			
		||||
		height 20px
 | 
			
		||||
		border-radius 100%
 | 
			
		||||
 | 
			
		||||
	> .name
 | 
			
		||||
		display block
 | 
			
		||||
		margin 0 .5em 0 0
 | 
			
		||||
		padding 0
 | 
			
		||||
		overflow hidden
 | 
			
		||||
		color isDark ? #fff : #627079
 | 
			
		||||
		font-size 1em
 | 
			
		||||
		font-weight bold
 | 
			
		||||
		text-decoration none
 | 
			
		||||
		text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
		&:hover
 | 
			
		||||
			text-decoration underline
 | 
			
		||||
 | 
			
		||||
	> .is-admin
 | 
			
		||||
	> .is-bot
 | 
			
		||||
	> .is-cat
 | 
			
		||||
		align-self center
 | 
			
		||||
		margin 0 .5em 0 0
 | 
			
		||||
		padding 1px 6px
 | 
			
		||||
		font-size 80%
 | 
			
		||||
		color isDark ? #758188 : #aaa
 | 
			
		||||
		border solid 1px isDark ? #57616f : #ddd
 | 
			
		||||
		border-radius 3px
 | 
			
		||||
 | 
			
		||||
		&.is-admin
 | 
			
		||||
			border-color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
			color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
 | 
			
		||||
	> .username
 | 
			
		||||
		margin 0 .5em 0 0
 | 
			
		||||
		overflow hidden
 | 
			
		||||
		text-overflow ellipsis
 | 
			
		||||
		color isDark ? #606984 : #ccc
 | 
			
		||||
 | 
			
		||||
	> .info
 | 
			
		||||
		margin-left auto
 | 
			
		||||
		font-size 0.9em
 | 
			
		||||
 | 
			
		||||
		> *
 | 
			
		||||
			color isDark ? #606984 : #c0c0c0
 | 
			
		||||
 | 
			
		||||
		> .mobile
 | 
			
		||||
			margin-right 8px
 | 
			
		||||
 | 
			
		||||
		> .app
 | 
			
		||||
			margin-right 8px
 | 
			
		||||
			padding-right 8px
 | 
			
		||||
			border-right solid 1px isDark ? #1c2023 : #eaeaea
 | 
			
		||||
 | 
			
		||||
		> .visibility
 | 
			
		||||
			margin-left 8px
 | 
			
		||||
 | 
			
		||||
.bvonvjxbwzaiskogyhbwgyxvcgserpmu[data-darkmode]
 | 
			
		||||
	root(true)
 | 
			
		||||
 | 
			
		||||
.bvonvjxbwzaiskogyhbwgyxvcgserpmu:not([data-darkmode])
 | 
			
		||||
	root(false)
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
 | 
			
		||||
			ast = this.ast;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (ast.filter(x => x.type != 'hashtag').length == 0) {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		while (ast[ast.length - 1] && (
 | 
			
		||||
			ast[ast.length - 1].type == 'hashtag' ||
 | 
			
		||||
			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
 | 
			
		||||
			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) {
 | 
			
		||||
			ast.pop();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Parse ast to DOM
 | 
			
		||||
		const els = flatten(ast.map(token => {
 | 
			
		||||
			switch (token.type) {
 | 
			
		||||
@@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
 | 
			
		||||
				case 'hashtag':
 | 
			
		||||
					return createElement('a', {
 | 
			
		||||
						attrs: {
 | 
			
		||||
							href: `${url}/search?q=${token.content}`,
 | 
			
		||||
							href: `${url}/tags/${token.hashtag}`,
 | 
			
		||||
							target: '_blank'
 | 
			
		||||
						}
 | 
			
		||||
					}, token.content);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-note-menu" style="position:initial">
 | 
			
		||||
	<mk-menu ref="menu" :source="source" :compact="compact" :items="items" @closed="$destroy"/>
 | 
			
		||||
<div style="position:initial">
 | 
			
		||||
	<mk-menu :source="source" :compact="compact" :items="items" @closed="closed"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -13,21 +13,25 @@ export default Vue.extend({
 | 
			
		||||
		items() {
 | 
			
		||||
			const items = [];
 | 
			
		||||
			items.push({
 | 
			
		||||
				icon: '%fa:star%',
 | 
			
		||||
				text: '%i18n:@favorite%',
 | 
			
		||||
				action: this.favorite
 | 
			
		||||
			});
 | 
			
		||||
			if (this.note.userId == this.$store.state.i.id) {
 | 
			
		||||
				items.push({
 | 
			
		||||
					icon: '%fa:thumbtack%',
 | 
			
		||||
					text: '%i18n:@pin%',
 | 
			
		||||
					action: this.pin
 | 
			
		||||
				});
 | 
			
		||||
				items.push({
 | 
			
		||||
					icon: '%fa:trash-alt R%',
 | 
			
		||||
					text: '%i18n:@delete%',
 | 
			
		||||
					action: this.del
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			if (this.note.uri) {
 | 
			
		||||
				items.push({
 | 
			
		||||
					icon: '%fa:external-link-square-alt%',
 | 
			
		||||
					text: '%i18n:@remote%',
 | 
			
		||||
					action: () => {
 | 
			
		||||
						window.open(this.note.uri, '_blank');
 | 
			
		||||
@@ -63,8 +67,10 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		close() {
 | 
			
		||||
			this.$refs.menu.close();
 | 
			
		||||
		closed() {
 | 
			
		||||
			this.$nextTick(() => {
 | 
			
		||||
				this.$destroy();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -58,18 +58,21 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.mode == 'relative' || this.mode == 'detail') {
 | 
			
		||||
			this.tick();
 | 
			
		||||
			this.tickId = setInterval(this.tick, 5000);
 | 
			
		||||
			this.tickId = window.requestAnimationFrame(this.tick);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	destroyed() {
 | 
			
		||||
		if (this.mode === 'relative' || this.mode === 'detail') {
 | 
			
		||||
			clearInterval(this.tickId);
 | 
			
		||||
			window.clearTimeout(this.tickId);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		tick() {
 | 
			
		||||
			this.now = new Date();
 | 
			
		||||
 | 
			
		||||
			this.tickId = setTimeout(() => {
 | 
			
		||||
				window.requestAnimationFrame(this.tick);
 | 
			
		||||
			}, 10000);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -203,6 +203,7 @@ root(isDark)
 | 
			
		||||
				justify-content center
 | 
			
		||||
				align-items center
 | 
			
		||||
				margin-right 10px
 | 
			
		||||
				width 16px
 | 
			
		||||
 | 
			
		||||
			> *:last-child
 | 
			
		||||
				flex 1 1 auto
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										89
									
								
								src/client/app/common/views/widgets/hashtags.chart.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/client/app/common/views/widgets/hashtags.chart.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
<template>
 | 
			
		||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" style="overflow:visible">
 | 
			
		||||
	<defs>
 | 
			
		||||
		<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
 | 
			
		||||
			<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
 | 
			
		||||
			<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
 | 
			
		||||
		</linearGradient>
 | 
			
		||||
		<mask :id="maskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
 | 
			
		||||
			<polygon
 | 
			
		||||
				:points="polygonPoints"
 | 
			
		||||
				fill="#fff"
 | 
			
		||||
				fill-opacity="0.5"/>
 | 
			
		||||
			<polyline
 | 
			
		||||
				:points="polylinePoints"
 | 
			
		||||
				fill="none"
 | 
			
		||||
				stroke="#fff"
 | 
			
		||||
				stroke-width="2"/>
 | 
			
		||||
			<circle
 | 
			
		||||
				:cx="headX"
 | 
			
		||||
				:cy="headY"
 | 
			
		||||
				r="3"
 | 
			
		||||
				fill="#fff"/>
 | 
			
		||||
		</mask>
 | 
			
		||||
	</defs>
 | 
			
		||||
	<rect
 | 
			
		||||
		x="-10" y="-10"
 | 
			
		||||
		:width="viewBoxX + 20" :height="viewBoxY + 20"
 | 
			
		||||
		:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
 | 
			
		||||
</svg>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import * as uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: {
 | 
			
		||||
		src: {
 | 
			
		||||
			type: Array,
 | 
			
		||||
			required: true
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			viewBoxX: 50,
 | 
			
		||||
			viewBoxY: 30,
 | 
			
		||||
			gradientId: uuid(),
 | 
			
		||||
			maskId: uuid(),
 | 
			
		||||
			polylinePoints: '',
 | 
			
		||||
			polygonPoints: '',
 | 
			
		||||
			headX: null,
 | 
			
		||||
			headY: null,
 | 
			
		||||
			clock: null
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	watch: {
 | 
			
		||||
		src() {
 | 
			
		||||
			this.draw();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	created() {
 | 
			
		||||
		this.draw();
 | 
			
		||||
 | 
			
		||||
		// Vueが何故かWatchを発動させない場合があるので
 | 
			
		||||
		this.clock = setInterval(this.draw, 1000);
 | 
			
		||||
	},
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		clearInterval(this.clock);
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		draw() {
 | 
			
		||||
			const stats = this.src.slice().reverse();
 | 
			
		||||
			const peak = Math.max.apply(null, stats) || 1;
 | 
			
		||||
 | 
			
		||||
			const polylinePoints = stats.map((n, i) => [
 | 
			
		||||
				i * (this.viewBoxX / (stats.length - 1)),
 | 
			
		||||
				(1 - (n / peak)) * this.viewBoxY
 | 
			
		||||
			]);
 | 
			
		||||
 | 
			
		||||
			this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
 | 
			
		||||
 | 
			
		||||
			this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
 | 
			
		||||
 | 
			
		||||
			this.headX = polylinePoints[polylinePoints.length - 1][0];
 | 
			
		||||
			this.headY = polylinePoints[polylinePoints.length - 1][1];
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										118
									
								
								src/client/app/common/views/widgets/hashtags.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/client/app/common/views/widgets/hashtags.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mkw-hashtags">
 | 
			
		||||
	<mk-widget-container :show-header="!props.compact">
 | 
			
		||||
		<template slot="header">%fa:hashtag%%i18n:@title%</template>
 | 
			
		||||
 | 
			
		||||
		<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
 | 
			
		||||
			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 | 
			
		||||
			<p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
 | 
			
		||||
			<transition-group v-else tag="div" name="chart">
 | 
			
		||||
				<div v-for="stat in stats" :key="stat.tag">
 | 
			
		||||
					<div class="tag">
 | 
			
		||||
						<router-link :to="`/tags/${ stat.tag }`" :title="stat.tag">#{{ stat.tag }}</router-link>
 | 
			
		||||
						<p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
 | 
			
		||||
					</div>
 | 
			
		||||
					<x-chart class="chart" :src="stat.chart"/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</transition-group>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-widget-container>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import define from '../../../common/define-widget';
 | 
			
		||||
import XChart from './hashtags.chart.vue';
 | 
			
		||||
 | 
			
		||||
export default define({
 | 
			
		||||
	name: 'hashtags',
 | 
			
		||||
	props: () => ({
 | 
			
		||||
		compact: false
 | 
			
		||||
	})
 | 
			
		||||
}).extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XChart
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			stats: [],
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			clock: null
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.fetch();
 | 
			
		||||
		this.clock = setInterval(this.fetch, 1000 * 60);
 | 
			
		||||
	},
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		clearInterval(this.clock);
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		func() {
 | 
			
		||||
			this.props.compact = !this.props.compact;
 | 
			
		||||
			this.save();
 | 
			
		||||
		},
 | 
			
		||||
		fetch() {
 | 
			
		||||
			(this as any).api('hashtags/trend').then(stats => {
 | 
			
		||||
				this.stats = stats;
 | 
			
		||||
				this.fetching = false;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
root(isDark)
 | 
			
		||||
	.mkw-hashtags--body
 | 
			
		||||
		> .fetching
 | 
			
		||||
		> .empty
 | 
			
		||||
			margin 0
 | 
			
		||||
			padding 16px
 | 
			
		||||
			text-align center
 | 
			
		||||
			color #aaa
 | 
			
		||||
 | 
			
		||||
			> [data-fa]
 | 
			
		||||
				margin-right 4px
 | 
			
		||||
 | 
			
		||||
		> div
 | 
			
		||||
			.chart-move
 | 
			
		||||
				transition transform 1s ease
 | 
			
		||||
 | 
			
		||||
			> div
 | 
			
		||||
				display flex
 | 
			
		||||
				align-items center
 | 
			
		||||
				padding 14px 16px
 | 
			
		||||
 | 
			
		||||
				&:not(:last-child)
 | 
			
		||||
					border-bottom solid 1px isDark ? #393f4f : #eee
 | 
			
		||||
 | 
			
		||||
				> .tag
 | 
			
		||||
					flex 1
 | 
			
		||||
					overflow hidden
 | 
			
		||||
					font-size 14px
 | 
			
		||||
					color isDark ? #9baec8 : #65727b
 | 
			
		||||
 | 
			
		||||
					> a
 | 
			
		||||
						display block
 | 
			
		||||
						width 100%
 | 
			
		||||
						white-space nowrap
 | 
			
		||||
						overflow hidden
 | 
			
		||||
						text-overflow ellipsis
 | 
			
		||||
						color inherit
 | 
			
		||||
 | 
			
		||||
					> p
 | 
			
		||||
						margin 0
 | 
			
		||||
						font-size 75%
 | 
			
		||||
						opacity 0.7
 | 
			
		||||
 | 
			
		||||
				> .chart
 | 
			
		||||
					height 30px
 | 
			
		||||
 | 
			
		||||
.mkw-hashtags[data-darkmode]
 | 
			
		||||
	root(true)
 | 
			
		||||
 | 
			
		||||
.mkw-hashtags:not([data-darkmode])
 | 
			
		||||
	root(false)
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -4,6 +4,7 @@ import wAnalogClock from './analog-clock.vue';
 | 
			
		||||
import wVersion from './version.vue';
 | 
			
		||||
import wRss from './rss.vue';
 | 
			
		||||
import wServer from './server.vue';
 | 
			
		||||
import wPostsMonitor from './posts-monitor.vue';
 | 
			
		||||
import wMemo from './memo.vue';
 | 
			
		||||
import wBroadcast from './broadcast.vue';
 | 
			
		||||
import wCalendar from './calendar.vue';
 | 
			
		||||
@@ -12,6 +13,7 @@ import wSlideshow from './slideshow.vue';
 | 
			
		||||
import wTips from './tips.vue';
 | 
			
		||||
import wDonation from './donation.vue';
 | 
			
		||||
import wNav from './nav.vue';
 | 
			
		||||
import wHashtags from './hashtags.vue';
 | 
			
		||||
 | 
			
		||||
Vue.component('mkw-analog-clock', wAnalogClock);
 | 
			
		||||
Vue.component('mkw-nav', wNav);
 | 
			
		||||
@@ -22,6 +24,8 @@ Vue.component('mkw-tips', wTips);
 | 
			
		||||
Vue.component('mkw-donation', wDonation);
 | 
			
		||||
Vue.component('mkw-broadcast', wBroadcast);
 | 
			
		||||
Vue.component('mkw-server', wServer);
 | 
			
		||||
Vue.component('mkw-posts-monitor', wPostsMonitor);
 | 
			
		||||
Vue.component('mkw-memo', wMemo);
 | 
			
		||||
Vue.component('mkw-rss', wRss);
 | 
			
		||||
Vue.component('mkw-version', wVersion);
 | 
			
		||||
Vue.component('mkw-hashtags', wHashtags);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										211
									
								
								src/client/app/common/views/widgets/posts-monitor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								src/client/app/common/views/widgets/posts-monitor.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,211 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mkw-posts-monitor">
 | 
			
		||||
	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
 | 
			
		||||
		<template slot="header">%fa:chart-line%%i18n:@title%</template>
 | 
			
		||||
		<button slot="func" @click="toggle" title="%i18n:@toggle%">%fa:sort%</button>
 | 
			
		||||
 | 
			
		||||
		<div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }" :data-darkmode="$store.state.device.darkmode">
 | 
			
		||||
			<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 2">
 | 
			
		||||
				<defs>
 | 
			
		||||
					<linearGradient :id="localGradientId" x1="0" x2="0" y1="1" y2="0">
 | 
			
		||||
						<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
 | 
			
		||||
						<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
 | 
			
		||||
					</linearGradient>
 | 
			
		||||
					<mask :id="localMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
 | 
			
		||||
						<polygon
 | 
			
		||||
							:points="localPolygonPoints"
 | 
			
		||||
							fill="#fff"
 | 
			
		||||
							fill-opacity="0.5"/>
 | 
			
		||||
						<polyline
 | 
			
		||||
							:points="localPolylinePoints"
 | 
			
		||||
							fill="none"
 | 
			
		||||
							stroke="#fff"
 | 
			
		||||
							stroke-width="1"/>
 | 
			
		||||
						<circle
 | 
			
		||||
							:cx="localHeadX"
 | 
			
		||||
							:cy="localHeadY"
 | 
			
		||||
							r="1.5"
 | 
			
		||||
							fill="#fff"/>
 | 
			
		||||
					</mask>
 | 
			
		||||
				</defs>
 | 
			
		||||
				<rect
 | 
			
		||||
					x="-2" y="-2"
 | 
			
		||||
					:width="viewBoxX + 4" :height="viewBoxY + 4"
 | 
			
		||||
					:style="`stroke: none; fill: url(#${ localGradientId }); mask: url(#${ localMaskId })`"/>
 | 
			
		||||
				<text x="1" y="5">Local</text>
 | 
			
		||||
			</svg>
 | 
			
		||||
			<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 1">
 | 
			
		||||
				<defs>
 | 
			
		||||
					<linearGradient :id="fediGradientId" x1="0" x2="0" y1="1" y2="0">
 | 
			
		||||
						<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
 | 
			
		||||
						<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
 | 
			
		||||
					</linearGradient>
 | 
			
		||||
					<mask :id="fediMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
 | 
			
		||||
						<polygon
 | 
			
		||||
							:points="fediPolygonPoints"
 | 
			
		||||
							fill="#fff"
 | 
			
		||||
							fill-opacity="0.5"/>
 | 
			
		||||
						<polyline
 | 
			
		||||
							:points="fediPolylinePoints"
 | 
			
		||||
							fill="none"
 | 
			
		||||
							stroke="#fff"
 | 
			
		||||
							stroke-width="1"/>
 | 
			
		||||
						<circle
 | 
			
		||||
							:cx="fediHeadX"
 | 
			
		||||
							:cy="fediHeadY"
 | 
			
		||||
							r="1.5"
 | 
			
		||||
							fill="#fff"/>
 | 
			
		||||
					</mask>
 | 
			
		||||
				</defs>
 | 
			
		||||
				<rect
 | 
			
		||||
					x="-2" y="-2"
 | 
			
		||||
					:width="viewBoxX + 4" :height="viewBoxY + 4"
 | 
			
		||||
					:style="`stroke: none; fill: url(#${ fediGradientId }); mask: url(#${ fediMaskId })`"/>
 | 
			
		||||
				<text x="1" y="5">Fedi</text>
 | 
			
		||||
			</svg>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-widget-container>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import define from '../../../common/define-widget';
 | 
			
		||||
import * as uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
export default define({
 | 
			
		||||
	name: 'server',
 | 
			
		||||
	props: () => ({
 | 
			
		||||
		design: 0,
 | 
			
		||||
		view: 0
 | 
			
		||||
	})
 | 
			
		||||
}).extend({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			connection: null,
 | 
			
		||||
			connectionId: null,
 | 
			
		||||
			viewBoxY: 30,
 | 
			
		||||
			stats: [],
 | 
			
		||||
			fediGradientId: uuid(),
 | 
			
		||||
			fediMaskId: uuid(),
 | 
			
		||||
			localGradientId: uuid(),
 | 
			
		||||
			localMaskId: uuid(),
 | 
			
		||||
			fediPolylinePoints: '',
 | 
			
		||||
			localPolylinePoints: '',
 | 
			
		||||
			fediPolygonPoints: '',
 | 
			
		||||
			localPolygonPoints: '',
 | 
			
		||||
			fediHeadX: null,
 | 
			
		||||
			fediHeadY: null,
 | 
			
		||||
			localHeadX: null,
 | 
			
		||||
			localHeadY: null
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		viewBoxX(): number {
 | 
			
		||||
			return this.props.view == 0 ? 50 : 100;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	watch: {
 | 
			
		||||
		viewBoxX() {
 | 
			
		||||
			this.draw();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.connection = (this as any).os.streams.notesStatsStream.getConnection();
 | 
			
		||||
		this.connectionId = (this as any).os.streams.notesStatsStream.use();
 | 
			
		||||
 | 
			
		||||
		this.connection.on('stats', this.onStats);
 | 
			
		||||
		this.connection.on('statsLog', this.onStatsLog);
 | 
			
		||||
		this.connection.send({
 | 
			
		||||
			type: 'requestLog',
 | 
			
		||||
			id: Math.random().toString()
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		this.connection.off('stats', this.onStats);
 | 
			
		||||
		this.connection.off('statsLog', this.onStatsLog);
 | 
			
		||||
		(this as any).os.streams.notesStatsStream.dispose(this.connectionId);
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		toggle() {
 | 
			
		||||
			if (this.props.view == 2) {
 | 
			
		||||
				this.props.view = 0;
 | 
			
		||||
			} else {
 | 
			
		||||
				this.props.view++;
 | 
			
		||||
			}
 | 
			
		||||
			this.save();
 | 
			
		||||
		},
 | 
			
		||||
		func() {
 | 
			
		||||
			if (this.props.design == 2) {
 | 
			
		||||
				this.props.design = 0;
 | 
			
		||||
			} else {
 | 
			
		||||
				this.props.design++;
 | 
			
		||||
			}
 | 
			
		||||
			this.save();
 | 
			
		||||
		},
 | 
			
		||||
		draw() {
 | 
			
		||||
			const stats = this.props.view == 0 ? this.stats.slice(-50) : this.stats;
 | 
			
		||||
			const fediPeak = Math.max.apply(null, stats.map(x => x.all)) || 1;
 | 
			
		||||
			const localPeak = Math.max.apply(null, stats.map(x => x.local)) || 1;
 | 
			
		||||
 | 
			
		||||
			const fediPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.all / fediPeak)) * this.viewBoxY]);
 | 
			
		||||
			const localPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.local / localPeak)) * this.viewBoxY]);
 | 
			
		||||
			this.fediPolylinePoints = fediPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
 | 
			
		||||
			this.localPolylinePoints = localPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
 | 
			
		||||
 | 
			
		||||
			this.fediPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.fediPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
 | 
			
		||||
			this.localPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.localPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
 | 
			
		||||
 | 
			
		||||
			this.fediHeadX = fediPolylinePoints[fediPolylinePoints.length - 1][0];
 | 
			
		||||
			this.fediHeadY = fediPolylinePoints[fediPolylinePoints.length - 1][1];
 | 
			
		||||
			this.localHeadX = localPolylinePoints[localPolylinePoints.length - 1][0];
 | 
			
		||||
			this.localHeadY = localPolylinePoints[localPolylinePoints.length - 1][1];
 | 
			
		||||
		},
 | 
			
		||||
		onStats(stats) {
 | 
			
		||||
			this.stats.push(stats);
 | 
			
		||||
			if (this.stats.length > 100) this.stats.shift();
 | 
			
		||||
			this.draw();
 | 
			
		||||
		},
 | 
			
		||||
		onStatsLog(statsLog) {
 | 
			
		||||
			statsLog.forEach(stats => this.onStats(stats));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
root(isDark)
 | 
			
		||||
	&.dual
 | 
			
		||||
		> svg
 | 
			
		||||
			width 50%
 | 
			
		||||
			float left
 | 
			
		||||
 | 
			
		||||
			&:first-child
 | 
			
		||||
				padding-right 5px
 | 
			
		||||
 | 
			
		||||
			&:last-child
 | 
			
		||||
				padding-left 5px
 | 
			
		||||
 | 
			
		||||
	> svg
 | 
			
		||||
		display block
 | 
			
		||||
		padding 10px
 | 
			
		||||
		width 100%
 | 
			
		||||
 | 
			
		||||
		> text
 | 
			
		||||
			font-size 5px
 | 
			
		||||
			fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
 | 
			
		||||
 | 
			
		||||
			> tspan
 | 
			
		||||
				opacity 0.5
 | 
			
		||||
 | 
			
		||||
	&:after
 | 
			
		||||
		content ""
 | 
			
		||||
		display block
 | 
			
		||||
		clear both
 | 
			
		||||
 | 
			
		||||
.qpdmibaztplkylerhdbllwcokyrfxeyj[data-darkmode]
 | 
			
		||||
	root(true)
 | 
			
		||||
 | 
			
		||||
.qpdmibaztplkylerhdbllwcokyrfxeyj:not([data-darkmode])
 | 
			
		||||
	root(false)
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="cpu-memory">
 | 
			
		||||
	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none">
 | 
			
		||||
	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
 | 
			
		||||
		<defs>
 | 
			
		||||
			<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
 | 
			
		||||
				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
 | 
			
		||||
@@ -16,15 +16,20 @@
 | 
			
		||||
					fill="none"
 | 
			
		||||
					stroke="#fff"
 | 
			
		||||
					stroke-width="1"/>
 | 
			
		||||
				<circle
 | 
			
		||||
					:cx="cpuHeadX"
 | 
			
		||||
					:cy="cpuHeadY"
 | 
			
		||||
					r="1.5"
 | 
			
		||||
					fill="#fff"/>
 | 
			
		||||
			</mask>
 | 
			
		||||
		</defs>
 | 
			
		||||
		<rect
 | 
			
		||||
			x="-1" y="-1"
 | 
			
		||||
			:width="viewBoxX + 2" :height="viewBoxY + 2"
 | 
			
		||||
			x="-2" y="-2"
 | 
			
		||||
			:width="viewBoxX + 4" :height="viewBoxY + 4"
 | 
			
		||||
			:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/>
 | 
			
		||||
		<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
 | 
			
		||||
	</svg>
 | 
			
		||||
	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none">
 | 
			
		||||
	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
 | 
			
		||||
		<defs>
 | 
			
		||||
			<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
 | 
			
		||||
				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
 | 
			
		||||
@@ -40,11 +45,16 @@
 | 
			
		||||
					fill="none"
 | 
			
		||||
					stroke="#fff"
 | 
			
		||||
					stroke-width="1"/>
 | 
			
		||||
				<circle
 | 
			
		||||
					:cx="memHeadX"
 | 
			
		||||
					:cy="memHeadY"
 | 
			
		||||
					r="1.5"
 | 
			
		||||
					fill="#fff"/>
 | 
			
		||||
			</mask>
 | 
			
		||||
		</defs>
 | 
			
		||||
		<rect
 | 
			
		||||
			x="-1" y="-1"
 | 
			
		||||
			:width="viewBoxX + 2" :height="viewBoxY + 2"
 | 
			
		||||
			x="-2" y="-2"
 | 
			
		||||
			:width="viewBoxX + 4" :height="viewBoxY + 4"
 | 
			
		||||
			:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/>
 | 
			
		||||
		<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
 | 
			
		||||
	</svg>
 | 
			
		||||
@@ -70,15 +80,25 @@ export default Vue.extend({
 | 
			
		||||
			memPolylinePoints: '',
 | 
			
		||||
			cpuPolygonPoints: '',
 | 
			
		||||
			memPolygonPoints: '',
 | 
			
		||||
			cpuHeadX: null,
 | 
			
		||||
			cpuHeadY: null,
 | 
			
		||||
			memHeadX: null,
 | 
			
		||||
			memHeadY: null,
 | 
			
		||||
			cpuP: '',
 | 
			
		||||
			memP: ''
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.connection.on('stats', this.onStats);
 | 
			
		||||
		this.connection.on('statsLog', this.onStatsLog);
 | 
			
		||||
		this.connection.send({
 | 
			
		||||
			type: 'requestLog',
 | 
			
		||||
			id: Math.random().toString()
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		this.connection.off('stats', this.onStats);
 | 
			
		||||
		this.connection.off('statsLog', this.onStatsLog);
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		onStats(stats) {
 | 
			
		||||
@@ -86,14 +106,24 @@ export default Vue.extend({
 | 
			
		||||
			this.stats.push(stats);
 | 
			
		||||
			if (this.stats.length > 50) this.stats.shift();
 | 
			
		||||
 | 
			
		||||
			this.cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' ');
 | 
			
		||||
			this.memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' ');
 | 
			
		||||
			const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]);
 | 
			
		||||
			const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]);
 | 
			
		||||
			this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
 | 
			
		||||
			this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
 | 
			
		||||
 | 
			
		||||
			this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
 | 
			
		||||
			this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
 | 
			
		||||
 | 
			
		||||
			this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
 | 
			
		||||
			this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
 | 
			
		||||
			this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
 | 
			
		||||
			this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
 | 
			
		||||
 | 
			
		||||
			this.cpuP = (stats.cpu_usage * 100).toFixed(0);
 | 
			
		||||
			this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
 | 
			
		||||
		},
 | 
			
		||||
		onStatsLog(statsLog) {
 | 
			
		||||
			statsLog.forEach(stats => this.onStats(stats));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -55,11 +55,11 @@ export default define({
 | 
			
		||||
			this.fetching = false;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.connection = (this as any).os.streams.serverStream.getConnection();
 | 
			
		||||
		this.connectionId = (this as any).os.streams.serverStream.use();
 | 
			
		||||
		this.connection = (this as any).os.streams.serverStatsStream.getConnection();
 | 
			
		||||
		this.connectionId = (this as any).os.streams.serverStatsStream.use();
 | 
			
		||||
	},
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		(this as any).os.streams.serverStream.dispose(this.connectionId);
 | 
			
		||||
		(this as any).os.streams.serverStatsStream.dispose(this.connectionId);
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		toggle() {
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ import MkHomeCustomize from './views/pages/home-customize.vue';
 | 
			
		||||
import MkMessagingRoom from './views/pages/messaging-room.vue';
 | 
			
		||||
import MkNote from './views/pages/note.vue';
 | 
			
		||||
import MkSearch from './views/pages/search.vue';
 | 
			
		||||
import MkTag from './views/pages/tag.vue';
 | 
			
		||||
import MkOthello from './views/pages/othello.vue';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -60,6 +61,7 @@ init(async (launch) => {
 | 
			
		||||
			{ path: '/i/lists/:list', component: MkUserList },
 | 
			
		||||
			{ path: '/selectdrive', component: MkSelectDrive },
 | 
			
		||||
			{ path: '/search', component: MkSearch },
 | 
			
		||||
			{ path: '/tags/:tag', component: MkTag },
 | 
			
		||||
			{ path: '/othello', component: MkOthello },
 | 
			
		||||
			{ path: '/othello/:game', component: MkOthello },
 | 
			
		||||
			{ path: '/@:user', component: MkUser },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<svg viewBox="0 0 21 7" preserveAspectRatio="none">
 | 
			
		||||
<svg viewBox="0 0 21 7">
 | 
			
		||||
	<rect v-for="record in data" class="day"
 | 
			
		||||
		width="1" height="1"
 | 
			
		||||
		:x="record.x" :y="record.date.weekday"
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
		style="pointer-events: none;"/>
 | 
			
		||||
	<rect class="today"
 | 
			
		||||
		width="1" height="1"
 | 
			
		||||
		:x="data[data.length - 1].x" :y="data[data.length - 1].date.weekday"
 | 
			
		||||
		:x="data[0].x" :y="data[0].date.weekday"
 | 
			
		||||
		rx="1" ry="1"
 | 
			
		||||
		fill="none"
 | 
			
		||||
		stroke-width="0.1"
 | 
			
		||||
@@ -33,7 +33,7 @@ export default Vue.extend({
 | 
			
		||||
		const peak = Math.max.apply(null, this.data.map(d => d.total));
 | 
			
		||||
 | 
			
		||||
		let x = 0;
 | 
			
		||||
		this.data.reverse().forEach(d => {
 | 
			
		||||
		this.data.slice().reverse().forEach(d => {
 | 
			
		||||
			d.x = x;
 | 
			
		||||
			d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" @mousedown.prevent="onMousedown">
 | 
			
		||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
 | 
			
		||||
	<title>%i18n:@total%<br/>%i18n:@notes%<br/>%i18n:@replies%<br/>%i18n:@renotes%</title>
 | 
			
		||||
	<polyline
 | 
			
		||||
		:points="pointsNote"
 | 
			
		||||
@@ -55,7 +55,6 @@ export default Vue.extend({
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	created() {
 | 
			
		||||
		this.data.reverse();
 | 
			
		||||
		this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
 | 
			
		||||
		this.render();
 | 
			
		||||
	},
 | 
			
		||||
@@ -63,10 +62,11 @@ export default Vue.extend({
 | 
			
		||||
		render() {
 | 
			
		||||
			const peak = Math.max.apply(null, this.data.map(d => d.total));
 | 
			
		||||
			if (peak != 0) {
 | 
			
		||||
				this.pointsNote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
 | 
			
		||||
				this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
 | 
			
		||||
				this.pointsRenote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
 | 
			
		||||
				this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
 | 
			
		||||
				const data = this.data.slice().reverse();
 | 
			
		||||
				this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
 | 
			
		||||
				this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
 | 
			
		||||
				this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
 | 
			
		||||
				this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		onMousedown(e) {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,12 +23,12 @@ export default Vue.extend({
 | 
			
		||||
			let x = this.x;
 | 
			
		||||
			let y = this.y;
 | 
			
		||||
 | 
			
		||||
			if (x + width > window.innerWidth) {
 | 
			
		||||
				x = window.innerWidth - width;
 | 
			
		||||
			if (x + width - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
				x = window.innerWidth - width + window.pageXOffset;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (y + height > window.innerHeight) {
 | 
			
		||||
				y = window.innerHeight - height;
 | 
			
		||||
			if (y + height - window.pageYOffset > window.innerHeight) {
 | 
			
		||||
				y = window.innerHeight - height + window.pageYOffset;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.$el.style.left = x + 'px';
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,8 @@
 | 
			
		||||
					<option value="post-form">%i18n:common.widgets.post-form%</option>
 | 
			
		||||
					<option value="messaging">%i18n:common.widgets.messaging%</option>
 | 
			
		||||
					<option value="memo">%i18n:common.widgets.memo%</option>
 | 
			
		||||
					<option value="hashtags">%i18n:common.widgets.hashtags%</option>
 | 
			
		||||
					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
 | 
			
		||||
					<option value="server">%i18n:common.widgets.server%</option>
 | 
			
		||||
					<option value="donation">%i18n:common.widgets.donation%</option>
 | 
			
		||||
					<option value="nav">%i18n:common.widgets.nav%</option>
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
			<mk-poll v-if="p.poll" :note="p"/>
 | 
			
		||||
			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 | 
			
		||||
			<div class="tags" v-if="p.tags && p.tags.length > 0">
 | 
			
		||||
				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 | 
			
		||||
				<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
 | 
			
		||||
			</div>
 | 
			
		||||
			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
			
		||||
			<div class="map" v-if="p.geo" ref="map"></div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,8 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-note-preview" :title="title">
 | 
			
		||||
	<mk-avatar class="avatar" :user="note.user"/>
 | 
			
		||||
	<mk-avatar class="avatar" :user="note.user" v-if="!mini"/>
 | 
			
		||||
	<div class="main">
 | 
			
		||||
		<header>
 | 
			
		||||
			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
 | 
			
		||||
			<span class="username"><mk-acct :user="note.user"/></span>
 | 
			
		||||
			<div class="info">
 | 
			
		||||
				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
 | 
			
		||||
				<router-link class="created-at" :to="note | notePage">
 | 
			
		||||
					<mk-time :time="note.createdAt"/>
 | 
			
		||||
				</router-link>
 | 
			
		||||
				<span class="visibility" v-if="note.visibility != 'public'">
 | 
			
		||||
					<template v-if="note.visibility == 'home'">%fa:home%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'private'">%fa:lock%</template>
 | 
			
		||||
				</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		</header>
 | 
			
		||||
		<mk-note-header class="header" :note="note" :mini="true"/>
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<mk-sub-note-content class="text" :note="note"/>
 | 
			
		||||
		</div>
 | 
			
		||||
@@ -30,7 +15,17 @@ import Vue from 'vue';
 | 
			
		||||
import dateStringify from '../../../common/scripts/date-stringify';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: ['note'],
 | 
			
		||||
	props: {
 | 
			
		||||
		note: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		mini: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		title(): string {
 | 
			
		||||
			return dateStringify(this.note.createdAt);
 | 
			
		||||
@@ -56,43 +51,6 @@ root(isDark)
 | 
			
		||||
		flex 1
 | 
			
		||||
		min-width 0
 | 
			
		||||
 | 
			
		||||
		> header
 | 
			
		||||
			display flex
 | 
			
		||||
			align-items baseline
 | 
			
		||||
			white-space nowrap
 | 
			
		||||
 | 
			
		||||
			> .name
 | 
			
		||||
				margin 0 .5em 0 0
 | 
			
		||||
				padding 0
 | 
			
		||||
				overflow hidden
 | 
			
		||||
				color isDark ? #fff : #607073
 | 
			
		||||
				font-size 1em
 | 
			
		||||
				font-weight bold
 | 
			
		||||
				text-decoration none
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
				&:hover
 | 
			
		||||
					text-decoration underline
 | 
			
		||||
 | 
			
		||||
			> .username
 | 
			
		||||
				margin 0 .5em 0 0
 | 
			
		||||
				overflow hidden
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
				color isDark ? #606984 : #d1d8da
 | 
			
		||||
 | 
			
		||||
			> .info
 | 
			
		||||
				margin-left auto
 | 
			
		||||
				font-size 0.9em
 | 
			
		||||
 | 
			
		||||
				> *
 | 
			
		||||
					color isDark ? #606984 : #b2b8bb
 | 
			
		||||
 | 
			
		||||
				> .mobile
 | 
			
		||||
					margin-right 6px
 | 
			
		||||
 | 
			
		||||
				> .visibility
 | 
			
		||||
					margin-left 6px
 | 
			
		||||
 | 
			
		||||
		> .body
 | 
			
		||||
 | 
			
		||||
			> .text
 | 
			
		||||
 
 | 
			
		||||
@@ -2,25 +2,7 @@
 | 
			
		||||
<div class="sub" :title="title">
 | 
			
		||||
	<mk-avatar class="avatar" :user="note.user"/>
 | 
			
		||||
	<div class="main">
 | 
			
		||||
		<header>
 | 
			
		||||
			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
 | 
			
		||||
			<span class="is-admin" v-if="note.user.isAdmin">admin</span>
 | 
			
		||||
			<span class="is-bot" v-if="note.user.isBot">bot</span>
 | 
			
		||||
			<span class="is-cat" v-if="note.user.isCat">cat</span>
 | 
			
		||||
			<span class="username"><mk-acct :user="note.user"/></span>
 | 
			
		||||
			<div class="info">
 | 
			
		||||
				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
 | 
			
		||||
				<router-link class="created-at" :to="note | notePage">
 | 
			
		||||
					<mk-time :time="note.createdAt"/>
 | 
			
		||||
				</router-link>
 | 
			
		||||
				<span class="visibility" v-if="note.visibility != 'public'">
 | 
			
		||||
					<template v-if="note.visibility == 'home'">%fa:home%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'private'">%fa:lock%</template>
 | 
			
		||||
				</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		</header>
 | 
			
		||||
		<mk-note-header class="header" :note="note"/>
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<mk-sub-note-content class="text" :note="note"/>
 | 
			
		||||
		</div>
 | 
			
		||||
@@ -62,57 +44,8 @@ root(isDark)
 | 
			
		||||
		flex 1
 | 
			
		||||
		min-width 0
 | 
			
		||||
 | 
			
		||||
		> header
 | 
			
		||||
			display flex
 | 
			
		||||
			align-items baseline
 | 
			
		||||
		> .header
 | 
			
		||||
			margin-bottom 2px
 | 
			
		||||
			white-space nowrap
 | 
			
		||||
 | 
			
		||||
			> .name
 | 
			
		||||
				display block
 | 
			
		||||
				margin 0 .5em 0 0
 | 
			
		||||
				padding 0
 | 
			
		||||
				overflow hidden
 | 
			
		||||
				color isDark ? #fff : #607073
 | 
			
		||||
				font-size 1em
 | 
			
		||||
				font-weight bold
 | 
			
		||||
				text-decoration none
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
				&:hover
 | 
			
		||||
					text-decoration underline
 | 
			
		||||
 | 
			
		||||
			> .is-admin
 | 
			
		||||
			> .is-bot
 | 
			
		||||
			> .is-cat
 | 
			
		||||
				align-self center
 | 
			
		||||
				margin 0 0.5em 0 0
 | 
			
		||||
				padding 1px 5px
 | 
			
		||||
				font-size 10px
 | 
			
		||||
				color isDark ? #758188 : #aaa
 | 
			
		||||
				border solid 1px isDark ? #57616f : #ddd
 | 
			
		||||
				border-radius 3px
 | 
			
		||||
 | 
			
		||||
				&.is-admin
 | 
			
		||||
					border-color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
					color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
 | 
			
		||||
			> .username
 | 
			
		||||
				margin 0 .5em 0 0
 | 
			
		||||
				color isDark ? #606984 : #d1d8da
 | 
			
		||||
 | 
			
		||||
			> .info
 | 
			
		||||
				margin-left auto
 | 
			
		||||
				font-size 0.9em
 | 
			
		||||
 | 
			
		||||
				> *
 | 
			
		||||
					color isDark ? #606984 : #b2b8bb
 | 
			
		||||
 | 
			
		||||
				> .mobile
 | 
			
		||||
					margin-right 6px
 | 
			
		||||
 | 
			
		||||
				> .visibility
 | 
			
		||||
					margin-left 6px
 | 
			
		||||
 | 
			
		||||
		> .body
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,26 +14,7 @@
 | 
			
		||||
	<article>
 | 
			
		||||
		<mk-avatar class="avatar" :user="p.user"/>
 | 
			
		||||
		<div class="main">
 | 
			
		||||
			<header>
 | 
			
		||||
				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
 | 
			
		||||
				<span class="is-admin" v-if="p.user.isAdmin">admin</span>
 | 
			
		||||
				<span class="is-bot" v-if="p.user.isBot">bot</span>
 | 
			
		||||
				<span class="is-cat" v-if="p.user.isCat">cat</span>
 | 
			
		||||
				<span class="username"><mk-acct :user="p.user"/></span>
 | 
			
		||||
				<div class="info">
 | 
			
		||||
					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 | 
			
		||||
					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
 | 
			
		||||
					<router-link class="created-at" :to="p | notePage">
 | 
			
		||||
						<mk-time :time="p.createdAt"/>
 | 
			
		||||
					</router-link>
 | 
			
		||||
					<span class="visibility" v-if="p.visibility != 'public'">
 | 
			
		||||
						<template v-if="p.visibility == 'home'">%fa:home%</template>
 | 
			
		||||
						<template v-if="p.visibility == 'followers'">%fa:unlock%</template>
 | 
			
		||||
						<template v-if="p.visibility == 'specified'">%fa:envelope%</template>
 | 
			
		||||
						<template v-if="p.visibility == 'private'">%fa:lock%</template>
 | 
			
		||||
					</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</header>
 | 
			
		||||
			<mk-note-header class="header" :note="p"/>
 | 
			
		||||
			<div class="body">
 | 
			
		||||
				<p v-if="p.cw != null" class="cw">
 | 
			
		||||
					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
 | 
			
		||||
@@ -52,7 +33,7 @@
 | 
			
		||||
					</div>
 | 
			
		||||
					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 | 
			
		||||
					<div class="tags" v-if="p.tags && p.tags.length > 0">
 | 
			
		||||
						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 | 
			
		||||
						<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
 | 
			
		||||
					</div>
 | 
			
		||||
					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 | 
			
		||||
					<div class="map" v-if="p.geo" ref="map"></div>
 | 
			
		||||
@@ -409,64 +390,8 @@ root(isDark)
 | 
			
		||||
			flex 1
 | 
			
		||||
			min-width 0
 | 
			
		||||
 | 
			
		||||
			> header
 | 
			
		||||
				display flex
 | 
			
		||||
				align-items baseline
 | 
			
		||||
			> .header
 | 
			
		||||
				margin-bottom 4px
 | 
			
		||||
				white-space nowrap
 | 
			
		||||
 | 
			
		||||
				> .name
 | 
			
		||||
					display block
 | 
			
		||||
					margin 0 .5em 0 0
 | 
			
		||||
					padding 0
 | 
			
		||||
					overflow hidden
 | 
			
		||||
					color isDark ? #fff : #627079
 | 
			
		||||
					font-size 1em
 | 
			
		||||
					font-weight bold
 | 
			
		||||
					text-decoration none
 | 
			
		||||
					text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
					&:hover
 | 
			
		||||
						text-decoration underline
 | 
			
		||||
 | 
			
		||||
				> .is-admin
 | 
			
		||||
				> .is-bot
 | 
			
		||||
				> .is-cat
 | 
			
		||||
					align-self center
 | 
			
		||||
					margin 0 .5em 0 0
 | 
			
		||||
					padding 1px 6px
 | 
			
		||||
					font-size 12px
 | 
			
		||||
					color isDark ? #758188 : #aaa
 | 
			
		||||
					border solid 1px isDark ? #57616f : #ddd
 | 
			
		||||
					border-radius 3px
 | 
			
		||||
 | 
			
		||||
					&.is-admin
 | 
			
		||||
						border-color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
						color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
 | 
			
		||||
				> .username
 | 
			
		||||
					margin 0 .5em 0 0
 | 
			
		||||
					overflow hidden
 | 
			
		||||
					text-overflow ellipsis
 | 
			
		||||
					color isDark ? #606984 : #ccc
 | 
			
		||||
 | 
			
		||||
				> .info
 | 
			
		||||
					margin-left auto
 | 
			
		||||
					font-size 0.9em
 | 
			
		||||
 | 
			
		||||
					> *
 | 
			
		||||
						color isDark ? #606984 : #c0c0c0
 | 
			
		||||
 | 
			
		||||
					> .mobile
 | 
			
		||||
						margin-right 8px
 | 
			
		||||
 | 
			
		||||
					> .app
 | 
			
		||||
						margin-right 8px
 | 
			
		||||
						padding-right 8px
 | 
			
		||||
						border-right solid 1px isDark ? #1c2023 : #eaeaea
 | 
			
		||||
 | 
			
		||||
					> .visibility
 | 
			
		||||
						margin-left 8px
 | 
			
		||||
 | 
			
		||||
			> .body
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -206,7 +206,7 @@ root(isDark)
 | 
			
		||||
				margin 0
 | 
			
		||||
				padding 16px
 | 
			
		||||
				overflow-wrap break-word
 | 
			
		||||
				font-size 12px
 | 
			
		||||
				font-size 13px
 | 
			
		||||
				border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05)
 | 
			
		||||
 | 
			
		||||
				&:last-child
 | 
			
		||||
 
 | 
			
		||||
@@ -50,6 +50,7 @@ import * as XDraggable from 'vuedraggable';
 | 
			
		||||
import getKao from '../../../common/scripts/get-kao';
 | 
			
		||||
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
 | 
			
		||||
import parse from '../../../../../text/parse';
 | 
			
		||||
import { host } from '../../../config';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
@@ -129,6 +130,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
				// 自分は除外
 | 
			
		||||
				if (this.$store.state.i.username == x.username && x.host == null) return;
 | 
			
		||||
				if (this.$store.state.i.username == x.username && x.host == host) return;
 | 
			
		||||
 | 
			
		||||
				// 重複は除外
 | 
			
		||||
				if (this.text.indexOf(`${mention} `) != -1) return;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,11 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		onSubmit() {
 | 
			
		||||
			location.href = `/search?q=${encodeURIComponent(this.q)}`;
 | 
			
		||||
			if (this.q.startsWith('#')) {
 | 
			
		||||
				this.$router.push(`/tags/${encodeURIComponent(this.q.substr(1))}`);
 | 
			
		||||
			} else {
 | 
			
		||||
				this.$router.push(`/search?q=${encodeURIComponent(this.q)}`);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-ui" :style="style">
 | 
			
		||||
	<x-header class="header"/>
 | 
			
		||||
	<x-header class="header" v-show="!zenMode"/>
 | 
			
		||||
	<div class="content">
 | 
			
		||||
		<slot></slot>
 | 
			
		||||
	</div>
 | 
			
		||||
@@ -16,6 +16,11 @@ export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XHeader
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			zenMode: false
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		style(): any {
 | 
			
		||||
			if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
 | 
			
		||||
@@ -39,6 +44,11 @@ export default Vue.extend({
 | 
			
		||||
				e.preventDefault();
 | 
			
		||||
				(this as any).apis.post();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (e.which == 90) { // z
 | 
			
		||||
				e.preventDefault();
 | 
			
		||||
				this.zenMode = !this.zenMode;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
	<x-notes ref="timeline" :more="existMore ? more : null"/>
 | 
			
		||||
	<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
@@ -23,6 +23,11 @@ export default Vue.extend({
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		},
 | 
			
		||||
		mediaView: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,25 +2,7 @@
 | 
			
		||||
<div class="fnlfosztlhtptnongximhlbykxblytcq">
 | 
			
		||||
	<mk-avatar class="avatar" :user="note.user"/>
 | 
			
		||||
	<div class="main">
 | 
			
		||||
		<header>
 | 
			
		||||
			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
 | 
			
		||||
			<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span>
 | 
			
		||||
			<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span>
 | 
			
		||||
			<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span>
 | 
			
		||||
			<span class="username"><mk-acct :user="note.user"/></span>
 | 
			
		||||
			<div class="info">
 | 
			
		||||
				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
 | 
			
		||||
				<router-link class="created-at" :to="note | notePage">
 | 
			
		||||
					<mk-time :time="note.createdAt"/>
 | 
			
		||||
				</router-link>
 | 
			
		||||
				<span class="visibility" v-if="note.visibility != 'public'">
 | 
			
		||||
					<template v-if="note.visibility == 'home'">%fa:home%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'private'">%fa:lock%</template>
 | 
			
		||||
				</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		</header>
 | 
			
		||||
		<mk-note-header class="header" :note="note" :mini="true"/>
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<mk-sub-note-content class="text" :note="note"/>
 | 
			
		||||
		</div>
 | 
			
		||||
@@ -72,66 +54,8 @@ root(isDark)
 | 
			
		||||
		flex 1
 | 
			
		||||
		min-width 0
 | 
			
		||||
 | 
			
		||||
		> header
 | 
			
		||||
			display flex
 | 
			
		||||
			align-items baseline
 | 
			
		||||
		> .header
 | 
			
		||||
			margin-bottom 2px
 | 
			
		||||
			white-space nowrap
 | 
			
		||||
 | 
			
		||||
			> .avatar
 | 
			
		||||
				flex-shrink 0
 | 
			
		||||
				margin-right 8px
 | 
			
		||||
				width 18px
 | 
			
		||||
				height 18px
 | 
			
		||||
				border-radius 100%
 | 
			
		||||
 | 
			
		||||
			> .name
 | 
			
		||||
				display block
 | 
			
		||||
				margin 0 0.5em 0 0
 | 
			
		||||
				padding 0
 | 
			
		||||
				overflow hidden
 | 
			
		||||
				color isDark ? #fff : #607073
 | 
			
		||||
				font-size 1em
 | 
			
		||||
				font-weight 700
 | 
			
		||||
				text-align left
 | 
			
		||||
				text-decoration none
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
				&:hover
 | 
			
		||||
					text-decoration underline
 | 
			
		||||
 | 
			
		||||
			> .is-admin
 | 
			
		||||
			> .is-bot
 | 
			
		||||
			> .is-cat
 | 
			
		||||
				align-self center
 | 
			
		||||
				margin 0 0.5em 0 0
 | 
			
		||||
				padding 1px 5px
 | 
			
		||||
				font-size 0.8em
 | 
			
		||||
				color isDark ? #758188 : #aaa
 | 
			
		||||
				border solid 1px isDark ? #57616f : #ddd
 | 
			
		||||
				border-radius 3px
 | 
			
		||||
 | 
			
		||||
				&.is-admin
 | 
			
		||||
					border-color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
					color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
 | 
			
		||||
			> .username
 | 
			
		||||
				text-align left
 | 
			
		||||
				margin 0
 | 
			
		||||
				color isDark ? #606984 : #d1d8da
 | 
			
		||||
 | 
			
		||||
			> .info
 | 
			
		||||
				margin-left auto
 | 
			
		||||
				font-size 0.9em
 | 
			
		||||
 | 
			
		||||
				> *
 | 
			
		||||
					color isDark ? #606984 : #b2b8bb
 | 
			
		||||
 | 
			
		||||
				> .mobile
 | 
			
		||||
					margin-right 6px
 | 
			
		||||
 | 
			
		||||
				> .visibility
 | 
			
		||||
					margin-left 6px
 | 
			
		||||
 | 
			
		||||
		> .body
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }">
 | 
			
		||||
<div v-if="!mediaView" class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }">
 | 
			
		||||
	<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
 | 
			
		||||
		<x-sub :note="p.reply"/>
 | 
			
		||||
	</div>
 | 
			
		||||
@@ -14,25 +14,7 @@
 | 
			
		||||
	<article>
 | 
			
		||||
		<mk-avatar class="avatar" :user="p.user"/>
 | 
			
		||||
		<div class="main">
 | 
			
		||||
			<header>
 | 
			
		||||
				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
 | 
			
		||||
				<span class="is-admin" v-if="p.user.isAdmin">admin</span>
 | 
			
		||||
				<span class="is-bot" v-if="p.user.isBot">bot</span>
 | 
			
		||||
				<span class="is-cat" v-if="p.user.isCat">cat</span>
 | 
			
		||||
				<span class="username"><mk-acct :user="p.user"/></span>
 | 
			
		||||
				<div class="info">
 | 
			
		||||
					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
 | 
			
		||||
					<router-link class="created-at" :to="p | notePage">
 | 
			
		||||
						<mk-time :time="p.createdAt"/>
 | 
			
		||||
					</router-link>
 | 
			
		||||
					<span class="visibility" v-if="p.visibility != 'public'">
 | 
			
		||||
						<template v-if="p.visibility == 'home'">%fa:home%</template>
 | 
			
		||||
						<template v-if="p.visibility == 'followers'">%fa:unlock%</template>
 | 
			
		||||
						<template v-if="p.visibility == 'specified'">%fa:envelope%</template>
 | 
			
		||||
						<template v-if="p.visibility == 'private'">%fa:lock%</template>
 | 
			
		||||
					</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</header>
 | 
			
		||||
			<mk-note-header class="header" :note="p" :mini="true"/>
 | 
			
		||||
			<div class="body">
 | 
			
		||||
				<p v-if="p.cw != null" class="cw">
 | 
			
		||||
					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
 | 
			
		||||
@@ -51,11 +33,11 @@
 | 
			
		||||
					</div>
 | 
			
		||||
					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 | 
			
		||||
					<div class="tags" v-if="p.tags && p.tags.length > 0">
 | 
			
		||||
						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 | 
			
		||||
						<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
 | 
			
		||||
					</div>
 | 
			
		||||
					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
			
		||||
					<div class="renote" v-if="p.renote">
 | 
			
		||||
						<mk-note-preview :note="p.renote"/>
 | 
			
		||||
						<mk-note-preview :note="p.renote" :mini="true"/>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 | 
			
		||||
@@ -73,6 +55,14 @@
 | 
			
		||||
		</div>
 | 
			
		||||
	</article>
 | 
			
		||||
</div>
 | 
			
		||||
<div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi">
 | 
			
		||||
	<div v-if="note.media.length > 0">
 | 
			
		||||
		<mk-media-list :media-list="note.media"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div v-if="note.renote && note.renote.media.length > 0">
 | 
			
		||||
		<mk-media-list :media-list="note.renote.media"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
@@ -89,7 +79,17 @@ export default Vue.extend({
 | 
			
		||||
		XSub
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: ['note'],
 | 
			
		||||
	props: {
 | 
			
		||||
		note: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		mediaView: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
@@ -217,8 +217,18 @@ export default Vue.extend({
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
@import '~const.styl'
 | 
			
		||||
 | 
			
		||||
mediaRoot(isDark)
 | 
			
		||||
	font-size 13px
 | 
			
		||||
	margin 4px 12px
 | 
			
		||||
 | 
			
		||||
	&:first-child
 | 
			
		||||
		margin-top 12px
 | 
			
		||||
 | 
			
		||||
	&:last-child
 | 
			
		||||
		margin-bottom 12px
 | 
			
		||||
 | 
			
		||||
root(isDark)
 | 
			
		||||
	font-size 12px
 | 
			
		||||
	font-size 13px
 | 
			
		||||
	border-bottom solid 1px isDark ? #1c2023 : #eaeaea
 | 
			
		||||
 | 
			
		||||
	&:last-of-type
 | 
			
		||||
@@ -234,7 +244,7 @@ root(isDark)
 | 
			
		||||
	> .renote
 | 
			
		||||
		display flex
 | 
			
		||||
		align-items center
 | 
			
		||||
		padding 8px 16px
 | 
			
		||||
		padding 8px 16px 0 16px
 | 
			
		||||
		line-height 28px
 | 
			
		||||
		white-space pre
 | 
			
		||||
		color #9dbb00
 | 
			
		||||
@@ -275,7 +285,7 @@ root(isDark)
 | 
			
		||||
 | 
			
		||||
	> article
 | 
			
		||||
		display flex
 | 
			
		||||
		padding 16px 16px 9px
 | 
			
		||||
		padding 16px 16px 4px
 | 
			
		||||
 | 
			
		||||
		> .avatar
 | 
			
		||||
			flex-shrink 0
 | 
			
		||||
@@ -292,62 +302,6 @@ root(isDark)
 | 
			
		||||
			flex 1
 | 
			
		||||
			min-width 0
 | 
			
		||||
 | 
			
		||||
			> header
 | 
			
		||||
				display flex
 | 
			
		||||
				align-items baseline
 | 
			
		||||
				white-space nowrap
 | 
			
		||||
 | 
			
		||||
				> .avatar
 | 
			
		||||
					flex-shrink 0
 | 
			
		||||
					margin-right 8px
 | 
			
		||||
					width 20px
 | 
			
		||||
					height 20px
 | 
			
		||||
					border-radius 100%
 | 
			
		||||
 | 
			
		||||
				> .name
 | 
			
		||||
					display block
 | 
			
		||||
					margin 0 0.5em 0 0
 | 
			
		||||
					padding 0
 | 
			
		||||
					overflow hidden
 | 
			
		||||
					color isDark ? #fff : #627079
 | 
			
		||||
					font-weight bold
 | 
			
		||||
					text-decoration none
 | 
			
		||||
					text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
				> .is-admin
 | 
			
		||||
				> .is-bot
 | 
			
		||||
				> .is-cat
 | 
			
		||||
					align-self center
 | 
			
		||||
					margin 0 0.5em 0 0
 | 
			
		||||
					padding 1px 6px
 | 
			
		||||
					font-size 0.8em
 | 
			
		||||
					color isDark ? #758188 : #aaa
 | 
			
		||||
					border solid 1px isDark ? #57616f : #ddd
 | 
			
		||||
					border-radius 3px
 | 
			
		||||
 | 
			
		||||
					&.is-admin
 | 
			
		||||
						border-color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
						color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
 | 
			
		||||
				> .username
 | 
			
		||||
					margin 0 0.5em 0 0
 | 
			
		||||
					overflow hidden
 | 
			
		||||
					text-overflow ellipsis
 | 
			
		||||
					color isDark ? #606984 : #ccc
 | 
			
		||||
 | 
			
		||||
				> .info
 | 
			
		||||
					margin-left auto
 | 
			
		||||
					font-size 0.9em
 | 
			
		||||
 | 
			
		||||
					> *
 | 
			
		||||
						color isDark ? #606984 : #c0c0c0
 | 
			
		||||
 | 
			
		||||
					> .mobile
 | 
			
		||||
						margin-right 6px
 | 
			
		||||
 | 
			
		||||
					> .visibility
 | 
			
		||||
						margin-left 6px
 | 
			
		||||
 | 
			
		||||
			> .body
 | 
			
		||||
 | 
			
		||||
				> .cw
 | 
			
		||||
@@ -482,7 +436,7 @@ root(isDark)
 | 
			
		||||
			> footer
 | 
			
		||||
				> button
 | 
			
		||||
					margin 0
 | 
			
		||||
					padding 8px
 | 
			
		||||
					padding 4px 8px 8px 8px
 | 
			
		||||
					background transparent
 | 
			
		||||
					border none
 | 
			
		||||
					box-shadow none
 | 
			
		||||
@@ -510,4 +464,10 @@ root(isDark)
 | 
			
		||||
.zyjjkidcqjnlegkqebitfviomuqmseqk:not([data-darkmode])
 | 
			
		||||
	root(false)
 | 
			
		||||
 | 
			
		||||
.srwrkujossgfuhrbnvqkybtzxpblgchi[data-darkmode]
 | 
			
		||||
	mediaRoot(true)
 | 
			
		||||
 | 
			
		||||
.srwrkujossgfuhrbnvqkybtzxpblgchi:not([data-darkmode])
 | 
			
		||||
	mediaRoot(false)
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
 | 
			
		||||
	<transition-group name="mk-notes" class="transition">
 | 
			
		||||
		<template v-for="(note, i) in _notes">
 | 
			
		||||
			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
 | 
			
		||||
			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :media-view="mediaView"/>
 | 
			
		||||
			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
 | 
			
		||||
				<span>%fa:angle-up%{{ note._datetext }}</span>
 | 
			
		||||
				<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
 | 
			
		||||
@@ -31,7 +31,7 @@ import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
import XNote from './deck.note.vue';
 | 
			
		||||
 | 
			
		||||
const displayLimit = 30;
 | 
			
		||||
const displayLimit = 20;
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
@@ -44,6 +44,11 @@ export default Vue.extend({
 | 
			
		||||
		more: {
 | 
			
		||||
			type: Function,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		mediaView: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,7 @@ export default Vue.extend({
 | 
			
		||||
root(isDark)
 | 
			
		||||
	> .notification
 | 
			
		||||
		padding 16px
 | 
			
		||||
		font-size 12px
 | 
			
		||||
		font-size 13px
 | 
			
		||||
		overflow-wrap break-word
 | 
			
		||||
 | 
			
		||||
		&:after
 | 
			
		||||
 
 | 
			
		||||
@@ -21,20 +21,27 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import XNotification from './deck.notification.vue';
 | 
			
		||||
 | 
			
		||||
const displayLimit = 20;
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XNotification
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	inject: ['column', 'isScrollTop', 'count'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			fetchingMoreNotifications: false,
 | 
			
		||||
			notifications: [],
 | 
			
		||||
			queue: [],
 | 
			
		||||
			moreNotifications: false,
 | 
			
		||||
			connection: null,
 | 
			
		||||
			connectionId: null
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		_notifications(): any[] {
 | 
			
		||||
			return (this.notifications as any).map(notification => {
 | 
			
		||||
@@ -46,12 +53,22 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		queue(q) {
 | 
			
		||||
			this.count(q.length);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.connection = (this as any).os.stream.getConnection();
 | 
			
		||||
		this.connectionId = (this as any).os.stream.use();
 | 
			
		||||
 | 
			
		||||
		this.connection.on('notification', this.onNotification);
 | 
			
		||||
 | 
			
		||||
		this.column.$on('top', this.onTop);
 | 
			
		||||
		this.column.$on('bottom', this.onBottom);
 | 
			
		||||
 | 
			
		||||
		const max = 10;
 | 
			
		||||
 | 
			
		||||
		(this as any).api('i/notifications', {
 | 
			
		||||
@@ -66,15 +83,20 @@ export default Vue.extend({
 | 
			
		||||
			this.fetching = false;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		this.connection.off('notification', this.onNotification);
 | 
			
		||||
		(this as any).os.stream.dispose(this.connectionId);
 | 
			
		||||
 | 
			
		||||
		this.column.$off('top', this.onTop);
 | 
			
		||||
		this.column.$off('bottom', this.onBottom);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetchMoreNotifications() {
 | 
			
		||||
			this.fetchingMoreNotifications = true;
 | 
			
		||||
 | 
			
		||||
			const max = 30;
 | 
			
		||||
			const max = 20;
 | 
			
		||||
 | 
			
		||||
			(this as any).api('i/notifications', {
 | 
			
		||||
				limit: max + 1,
 | 
			
		||||
@@ -90,6 +112,7 @@ export default Vue.extend({
 | 
			
		||||
				this.fetchingMoreNotifications = false;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onNotification(notification) {
 | 
			
		||||
			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
 | 
			
		||||
			this.connection.send({
 | 
			
		||||
@@ -97,7 +120,34 @@ export default Vue.extend({
 | 
			
		||||
				id: notification.id
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.notifications.unshift(notification);
 | 
			
		||||
			this.prepend(notification);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		prepend(notification) {
 | 
			
		||||
			if (this.isScrollTop()) {
 | 
			
		||||
				// Prepend the notification
 | 
			
		||||
				this.notifications.unshift(notification);
 | 
			
		||||
 | 
			
		||||
				// オーバーフローしたら古い通知は捨てる
 | 
			
		||||
				if (this.notifications.length >= displayLimit) {
 | 
			
		||||
					this.notifications = this.notifications.slice(0, displayLimit);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				this.queue.push(notification);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		releaseQueue() {
 | 
			
		||||
			this.queue.forEach(n => this.prepend(n));
 | 
			
		||||
			this.queue = [];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onTop() {
 | 
			
		||||
			this.releaseQueue();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onBottom() {
 | 
			
		||||
			this.fetchMoreNotifications();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,12 @@
 | 
			
		||||
		<span>{{ name }}</span>
 | 
			
		||||
	</span>
 | 
			
		||||
 | 
			
		||||
	<div class="editor" v-if="edit">
 | 
			
		||||
	<div class="editor" style="padding:0 12px" v-if="edit">
 | 
			
		||||
		<mk-switch v-model="column.isMediaOnly" @change="onChangeSettings" text="%i18n:@is-media-only%"/>
 | 
			
		||||
		<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly"/>
 | 
			
		||||
	<x-tl v-else :src="column.type" :media-only="column.isMediaOnly"/>
 | 
			
		||||
	<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
 | 
			
		||||
	<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
 | 
			
		||||
</x-column>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
	<x-notes ref="timeline" :more="existMore ? more : null"/>
 | 
			
		||||
	<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
@@ -23,6 +23,11 @@ export default Vue.extend({
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		},
 | 
			
		||||
		mediaView: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
	<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
 | 
			
		||||
		<template v-if="edit">
 | 
			
		||||
			<header>
 | 
			
		||||
				<select v-model="widgetAdderSelected">
 | 
			
		||||
				<select v-model="widgetAdderSelected" @change="addWidget">
 | 
			
		||||
					<option value="profile">%i18n:common.widgets.profile%</option>
 | 
			
		||||
					<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
 | 
			
		||||
					<option value="calendar">%i18n:common.widgets.calendar%</option>
 | 
			
		||||
@@ -23,25 +23,22 @@
 | 
			
		||||
					<option value="post-form">%i18n:common.widgets.post-form%</option>
 | 
			
		||||
					<option value="messaging">%i18n:common.widgets.messaging%</option>
 | 
			
		||||
					<option value="memo">%i18n:common.widgets.memo%</option>
 | 
			
		||||
					<option value="hashtags">%i18n:common.widgets.hashtags%</option>
 | 
			
		||||
					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
 | 
			
		||||
					<option value="server">%i18n:common.widgets.server%</option>
 | 
			
		||||
					<option value="donation">%i18n:common.widgets.donation%</option>
 | 
			
		||||
					<option value="nav">%i18n:common.widgets.nav%</option>
 | 
			
		||||
					<option value="tips">%i18n:common.widgets.tips%</option>
 | 
			
		||||
				</select>
 | 
			
		||||
				<button @click="addWidget">%i18n:@add%</button>
 | 
			
		||||
			</header>
 | 
			
		||||
			<x-draggable
 | 
			
		||||
				:list="column.widgets"
 | 
			
		||||
				:options="{ handle: '.handle', animation: 150 }"
 | 
			
		||||
				:options="{ animation: 150 }"
 | 
			
		||||
				@sort="onWidgetSort"
 | 
			
		||||
			>
 | 
			
		||||
				<div v-for="widget in column.widgets" class="customize-container" :key="widget.id">
 | 
			
		||||
					<header>
 | 
			
		||||
						<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
 | 
			
		||||
					</header>
 | 
			
		||||
					<div @click="widgetFunc(widget.id)">
 | 
			
		||||
						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
 | 
			
		||||
					</div>
 | 
			
		||||
				<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
 | 
			
		||||
					<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
 | 
			
		||||
					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</x-draggable>
 | 
			
		||||
		</template>
 | 
			
		||||
@@ -140,6 +137,13 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
root(isDark)
 | 
			
		||||
	.gqpwvtwtprsbmnssnbicggtwqhmylhnq
 | 
			
		||||
		> header
 | 
			
		||||
			padding 16px
 | 
			
		||||
 | 
			
		||||
			> *
 | 
			
		||||
				width 100%
 | 
			
		||||
				padding 4px
 | 
			
		||||
 | 
			
		||||
		.widget, .customize-container
 | 
			
		||||
			margin 8px
 | 
			
		||||
 | 
			
		||||
@@ -147,7 +151,21 @@ root(isDark)
 | 
			
		||||
				margin-top 0
 | 
			
		||||
 | 
			
		||||
		.customize-container
 | 
			
		||||
			background #fff
 | 
			
		||||
			cursor move
 | 
			
		||||
 | 
			
		||||
			> *:not(.remove)
 | 
			
		||||
				pointer-events none
 | 
			
		||||
 | 
			
		||||
			> .remove
 | 
			
		||||
				position absolute
 | 
			
		||||
				z-index 1
 | 
			
		||||
				top 8px
 | 
			
		||||
				right 8px
 | 
			
		||||
				width 32px
 | 
			
		||||
				height 32px
 | 
			
		||||
				color #fff
 | 
			
		||||
				background rgba(#000, 0.7)
 | 
			
		||||
				border-radius 4px
 | 
			
		||||
 | 
			
		||||
		> header
 | 
			
		||||
			color isDark ? #fff : #000
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										128
									
								
								src/client/app/desktop/views/pages/tag.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/client/app/desktop/views/pages/tag.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
			
		||||
<template>
 | 
			
		||||
<mk-ui>
 | 
			
		||||
	<header :class="$style.header">
 | 
			
		||||
		<h1>#{{ $route.params.tag }}</h1>
 | 
			
		||||
	</header>
 | 
			
		||||
	<div :class="$style.loading" v-if="fetching">
 | 
			
		||||
		<mk-ellipsis-icon/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<p :class="$style.empty" v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p>
 | 
			
		||||
	<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
 | 
			
		||||
</mk-ui>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import Progress from '../../../common/scripts/loading';
 | 
			
		||||
 | 
			
		||||
const limit = 20;
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			moreFetching: false,
 | 
			
		||||
			existMore: false,
 | 
			
		||||
			offset: 0,
 | 
			
		||||
			empty: false
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	watch: {
 | 
			
		||||
		$route: 'fetch'
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		document.addEventListener('keydown', this.onDocumentKeydown);
 | 
			
		||||
		window.addEventListener('scroll', this.onScroll, { passive: true });
 | 
			
		||||
 | 
			
		||||
		this.fetch();
 | 
			
		||||
	},
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		document.removeEventListener('keydown', this.onDocumentKeydown);
 | 
			
		||||
		window.removeEventListener('scroll', this.onScroll);
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		onDocumentKeydown(e) {
 | 
			
		||||
			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
 | 
			
		||||
				if (e.which == 84) { // t
 | 
			
		||||
					(this.$refs.timeline as any).focus();
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		fetch() {
 | 
			
		||||
			this.fetching = true;
 | 
			
		||||
			Progress.start();
 | 
			
		||||
 | 
			
		||||
			(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
 | 
			
		||||
				(this as any).api('notes/search_by_tag', {
 | 
			
		||||
					limit: limit + 1,
 | 
			
		||||
					offset: this.offset,
 | 
			
		||||
					tag: this.$route.params.tag
 | 
			
		||||
				}).then(notes => {
 | 
			
		||||
					if (notes.length == 0) this.empty = true;
 | 
			
		||||
					if (notes.length == limit + 1) {
 | 
			
		||||
						notes.pop();
 | 
			
		||||
						this.existMore = true;
 | 
			
		||||
					}
 | 
			
		||||
					res(notes);
 | 
			
		||||
					this.fetching = false;
 | 
			
		||||
					Progress.done();
 | 
			
		||||
				}, rej);
 | 
			
		||||
			}));
 | 
			
		||||
		},
 | 
			
		||||
		more() {
 | 
			
		||||
			this.offset += limit;
 | 
			
		||||
 | 
			
		||||
			const promise = (this as any).api('notes/search_by_tag', {
 | 
			
		||||
				limit: limit + 1,
 | 
			
		||||
				offset: this.offset,
 | 
			
		||||
				tag: this.$route.params.tag
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			promise.then(notes => {
 | 
			
		||||
				if (notes.length == limit + 1) {
 | 
			
		||||
					notes.pop();
 | 
			
		||||
				} else {
 | 
			
		||||
					this.existMore = false;
 | 
			
		||||
				}
 | 
			
		||||
				notes.forEach(n => (this.$refs.timeline as any).append(n));
 | 
			
		||||
				this.moreFetching = false;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return promise;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" module>
 | 
			
		||||
.header
 | 
			
		||||
	width 100%
 | 
			
		||||
	max-width 600px
 | 
			
		||||
	margin 0 auto
 | 
			
		||||
	color #555
 | 
			
		||||
 | 
			
		||||
.notes
 | 
			
		||||
	width 600px
 | 
			
		||||
	margin 0 auto
 | 
			
		||||
	border solid 1px rgba(#000, 0.075)
 | 
			
		||||
	border-radius 6px
 | 
			
		||||
	overflow hidden
 | 
			
		||||
 | 
			
		||||
.loading
 | 
			
		||||
	padding 64px 0
 | 
			
		||||
 | 
			
		||||
.empty
 | 
			
		||||
	display block
 | 
			
		||||
	margin 0 auto
 | 
			
		||||
	padding 32px
 | 
			
		||||
	max-width 400px
 | 
			
		||||
	text-align center
 | 
			
		||||
	color #999
 | 
			
		||||
 | 
			
		||||
	> [data-fa]
 | 
			
		||||
		display block
 | 
			
		||||
		margin-bottom 16px
 | 
			
		||||
		font-size 3em
 | 
			
		||||
		color #ccc
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -32,42 +32,30 @@ body > noscript {
 | 
			
		||||
	left: 0;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	background: #fff;
 | 
			
		||||
	cursor: wait;
 | 
			
		||||
}
 | 
			
		||||
	#ini > p {
 | 
			
		||||
		display: block;
 | 
			
		||||
		user-select: none;
 | 
			
		||||
		margin: 32px;
 | 
			
		||||
		font-size: 4em;
 | 
			
		||||
		color: #555;
 | 
			
		||||
	#ini > svg {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		right: 0;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		margin: auto;
 | 
			
		||||
		width: 64px;
 | 
			
		||||
		height: 64px;
 | 
			
		||||
		animation: ini 0.6s infinite linear;
 | 
			
		||||
	}
 | 
			
		||||
		#ini > p > span {
 | 
			
		||||
			animation: ini 1.4s infinite ease-in-out both;
 | 
			
		||||
		}
 | 
			
		||||
		#ini > p > span:nth-child(1) {
 | 
			
		||||
			animation-delay: 0s;
 | 
			
		||||
		}
 | 
			
		||||
		#ini > p > span:nth-child(2) {
 | 
			
		||||
			animation-delay: 0.16s;
 | 
			
		||||
		}
 | 
			
		||||
		#ini > p > span:nth-child(3) {
 | 
			
		||||
			animation-delay: 0.32s;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
html[data-darkmode] #ini {
 | 
			
		||||
	background: #191b22;
 | 
			
		||||
}
 | 
			
		||||
	html[data-darkmode] #ini > p {
 | 
			
		||||
		color: #fff;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@keyframes ini {
 | 
			
		||||
	0%, 80%, 100% {
 | 
			
		||||
		opacity: 1;
 | 
			
		||||
	from {
 | 
			
		||||
		transform: rotate(0deg);
 | 
			
		||||
	}
 | 
			
		||||
	40% {
 | 
			
		||||
		opacity: 0;
 | 
			
		||||
	to {
 | 
			
		||||
		transform: rotate(360deg);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,8 @@ import Progress from './common/scripts/loading';
 | 
			
		||||
import Connection from './common/scripts/streaming/stream';
 | 
			
		||||
import { HomeStreamManager } from './common/scripts/streaming/home';
 | 
			
		||||
import { DriveStreamManager } from './common/scripts/streaming/drive';
 | 
			
		||||
import { ServerStreamManager } from './common/scripts/streaming/server';
 | 
			
		||||
import { ServerStatsStreamManager } from './common/scripts/streaming/server-stats';
 | 
			
		||||
import { NotesStatsStreamManager } from './common/scripts/streaming/notes-stats';
 | 
			
		||||
import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index';
 | 
			
		||||
import { OthelloStreamManager } from './common/scripts/streaming/othello';
 | 
			
		||||
 | 
			
		||||
@@ -104,14 +105,16 @@ export default class MiOS extends EventEmitter {
 | 
			
		||||
		localTimelineStream: LocalTimelineStreamManager;
 | 
			
		||||
		globalTimelineStream: GlobalTimelineStreamManager;
 | 
			
		||||
		driveStream: DriveStreamManager;
 | 
			
		||||
		serverStream: ServerStreamManager;
 | 
			
		||||
		serverStatsStream: ServerStatsStreamManager;
 | 
			
		||||
		notesStatsStream: NotesStatsStreamManager;
 | 
			
		||||
		messagingIndexStream: MessagingIndexStreamManager;
 | 
			
		||||
		othelloStream: OthelloStreamManager;
 | 
			
		||||
	} = {
 | 
			
		||||
		localTimelineStream: null,
 | 
			
		||||
		globalTimelineStream: null,
 | 
			
		||||
		driveStream: null,
 | 
			
		||||
		serverStream: null,
 | 
			
		||||
		serverStatsStream: null,
 | 
			
		||||
		notesStatsStream: null,
 | 
			
		||||
		messagingIndexStream: null,
 | 
			
		||||
		othelloStream: null
 | 
			
		||||
	};
 | 
			
		||||
@@ -218,7 +221,8 @@ export default class MiOS extends EventEmitter {
 | 
			
		||||
		this.store = initStore(this);
 | 
			
		||||
 | 
			
		||||
		//#region Init stream managers
 | 
			
		||||
		this.streams.serverStream = new ServerStreamManager(this);
 | 
			
		||||
		this.streams.serverStatsStream = new ServerStatsStreamManager(this);
 | 
			
		||||
		this.streams.notesStatsStream = new NotesStatsStreamManager(this);
 | 
			
		||||
 | 
			
		||||
		this.once('signedin', () => {
 | 
			
		||||
			// Init home stream manager
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,7 @@ import MkUserLists from './views/pages/user-lists.vue';
 | 
			
		||||
import MkUserList from './views/pages/user-list.vue';
 | 
			
		||||
import MkSettings from './views/pages/settings.vue';
 | 
			
		||||
import MkOthello from './views/pages/othello.vue';
 | 
			
		||||
import MkTag from './views/pages/tag.vue';
 | 
			
		||||
 | 
			
		||||
Vue.use(MdCard);
 | 
			
		||||
Vue.use(MdButton);
 | 
			
		||||
@@ -88,6 +89,7 @@ init((launch) => {
 | 
			
		||||
			{ path: '/i/drive/file/:file', component: MkDrive },
 | 
			
		||||
			{ path: '/selectdrive', component: MkSelectDrive },
 | 
			
		||||
			{ path: '/search', component: MkSearch },
 | 
			
		||||
			{ path: '/tags/:tag', component: MkTag },
 | 
			
		||||
			{ path: '/othello', name: 'othello', component: MkOthello },
 | 
			
		||||
			{ path: '/othello/:game', component: MkOthello },
 | 
			
		||||
			{ path: '/@:user', component: MkUser },
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@
 | 
			
		||||
				<mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="tags" v-if="p.tags && p.tags.length > 0">
 | 
			
		||||
				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 | 
			
		||||
				<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="media" v-if="p.media.length > 0">
 | 
			
		||||
				<mk-media-list :media-list="p.media" :raw="true"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,26 +2,7 @@
 | 
			
		||||
<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }">
 | 
			
		||||
	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
 | 
			
		||||
	<div class="main">
 | 
			
		||||
		<header>
 | 
			
		||||
			<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
 | 
			
		||||
			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
 | 
			
		||||
			<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span>
 | 
			
		||||
			<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span>
 | 
			
		||||
			<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span>
 | 
			
		||||
			<span class="username"><mk-acct :user="note.user"/></span>
 | 
			
		||||
			<div class="info">
 | 
			
		||||
				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
 | 
			
		||||
				<router-link class="created-at" :to="note | notePage">
 | 
			
		||||
					<mk-time :time="note.createdAt"/>
 | 
			
		||||
				</router-link>
 | 
			
		||||
				<span class="visibility" v-if="note.visibility != 'public'">
 | 
			
		||||
					<template v-if="note.visibility == 'home'">%fa:home%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'private'">%fa:lock%</template>
 | 
			
		||||
				</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		</header>
 | 
			
		||||
		<mk-note-header class="header" :note="note" :mini="true"/>
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<mk-sub-note-content class="text" :note="note"/>
 | 
			
		||||
		</div>
 | 
			
		||||
@@ -79,64 +60,8 @@ root(isDark)
 | 
			
		||||
		flex 1
 | 
			
		||||
		min-width 0
 | 
			
		||||
 | 
			
		||||
		> header
 | 
			
		||||
			display flex
 | 
			
		||||
			align-items baseline
 | 
			
		||||
		> .header
 | 
			
		||||
			margin-bottom 2px
 | 
			
		||||
			white-space nowrap
 | 
			
		||||
 | 
			
		||||
			> .avatar
 | 
			
		||||
				flex-shrink 0
 | 
			
		||||
				margin-right 8px
 | 
			
		||||
				width 18px
 | 
			
		||||
				height 18px
 | 
			
		||||
				border-radius 100%
 | 
			
		||||
 | 
			
		||||
			> .name
 | 
			
		||||
				display block
 | 
			
		||||
				margin 0 .5em 0 0
 | 
			
		||||
				padding 0
 | 
			
		||||
				overflow hidden
 | 
			
		||||
				color isDark ? #fff : #607073
 | 
			
		||||
				font-size 1em
 | 
			
		||||
				font-weight 700
 | 
			
		||||
				text-align left
 | 
			
		||||
				text-decoration none
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
			> .is-admin
 | 
			
		||||
			> .is-bot
 | 
			
		||||
			> .is-cat
 | 
			
		||||
				align-self center
 | 
			
		||||
				margin 0 0.5em 0 0
 | 
			
		||||
				padding 1px 6px
 | 
			
		||||
				font-size 0.8em
 | 
			
		||||
				color isDark ? #758188 : #aaa
 | 
			
		||||
				border solid 1px isDark ? #57616f : #ddd
 | 
			
		||||
				border-radius 3px
 | 
			
		||||
 | 
			
		||||
				&.is-admin
 | 
			
		||||
					border-color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
					color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
 | 
			
		||||
			> .username
 | 
			
		||||
				margin 0 .5em 0 0
 | 
			
		||||
				overflow hidden
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
				color isDark ? #606984 : #d1d8da
 | 
			
		||||
 | 
			
		||||
			> .info
 | 
			
		||||
				margin-left auto
 | 
			
		||||
				font-size 0.9em
 | 
			
		||||
 | 
			
		||||
				> *
 | 
			
		||||
					color isDark ? #606984 : #b2b8bb
 | 
			
		||||
 | 
			
		||||
				> .mobile
 | 
			
		||||
					margin-right 6px
 | 
			
		||||
 | 
			
		||||
				> .visibility
 | 
			
		||||
					margin-left 6px
 | 
			
		||||
 | 
			
		||||
		> .body
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,26 +2,7 @@
 | 
			
		||||
<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }">
 | 
			
		||||
	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
 | 
			
		||||
	<div class="main">
 | 
			
		||||
		<header>
 | 
			
		||||
			<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
 | 
			
		||||
			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
 | 
			
		||||
			<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span>
 | 
			
		||||
			<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span>
 | 
			
		||||
			<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span>
 | 
			
		||||
			<span class="username"><mk-acct :user="note.user"/></span>
 | 
			
		||||
			<div class="info">
 | 
			
		||||
				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
 | 
			
		||||
				<router-link class="created-at" :to="note | notePage">
 | 
			
		||||
					<mk-time :time="note.createdAt"/>
 | 
			
		||||
				</router-link>
 | 
			
		||||
				<span class="visibility" v-if="note.visibility != 'public'">
 | 
			
		||||
					<template v-if="note.visibility == 'home'">%fa:home%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
 | 
			
		||||
					<template v-if="note.visibility == 'private'">%fa:lock%</template>
 | 
			
		||||
				</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		</header>
 | 
			
		||||
		<mk-note-header class="header" :note="note" :mini="true"/>
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<mk-sub-note-content class="text" :note="note"/>
 | 
			
		||||
		</div>
 | 
			
		||||
@@ -92,66 +73,8 @@ root(isDark)
 | 
			
		||||
		flex 1
 | 
			
		||||
		min-width 0
 | 
			
		||||
 | 
			
		||||
		> header
 | 
			
		||||
			display flex
 | 
			
		||||
			align-items baseline
 | 
			
		||||
		> .header
 | 
			
		||||
			margin-bottom 2px
 | 
			
		||||
			white-space nowrap
 | 
			
		||||
 | 
			
		||||
			> .avatar
 | 
			
		||||
				flex-shrink 0
 | 
			
		||||
				margin-right 8px
 | 
			
		||||
				width 18px
 | 
			
		||||
				height 18px
 | 
			
		||||
				border-radius 100%
 | 
			
		||||
 | 
			
		||||
			> .name
 | 
			
		||||
				display block
 | 
			
		||||
				margin 0 0.5em 0 0
 | 
			
		||||
				padding 0
 | 
			
		||||
				overflow hidden
 | 
			
		||||
				color isDark ? #fff : #607073
 | 
			
		||||
				font-size 1em
 | 
			
		||||
				font-weight 700
 | 
			
		||||
				text-align left
 | 
			
		||||
				text-decoration none
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
				&:hover
 | 
			
		||||
					text-decoration underline
 | 
			
		||||
 | 
			
		||||
			> .is-admin
 | 
			
		||||
			> .is-bot
 | 
			
		||||
			> .is-cat
 | 
			
		||||
				align-self center
 | 
			
		||||
				margin 0 0.5em 0 0
 | 
			
		||||
				padding 1px 5px
 | 
			
		||||
				font-size 0.8em
 | 
			
		||||
				color isDark ? #758188 : #aaa
 | 
			
		||||
				border solid 1px isDark ? #57616f : #ddd
 | 
			
		||||
				border-radius 3px
 | 
			
		||||
 | 
			
		||||
				&.is-admin
 | 
			
		||||
					border-color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
					color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
 | 
			
		||||
			> .username
 | 
			
		||||
				text-align left
 | 
			
		||||
				margin 0
 | 
			
		||||
				color isDark ? #606984 : #d1d8da
 | 
			
		||||
 | 
			
		||||
			> .info
 | 
			
		||||
				margin-left auto
 | 
			
		||||
				font-size 0.9em
 | 
			
		||||
 | 
			
		||||
				> *
 | 
			
		||||
					color isDark ? #606984 : #b2b8bb
 | 
			
		||||
 | 
			
		||||
				> .mobile
 | 
			
		||||
					margin-right 6px
 | 
			
		||||
 | 
			
		||||
				> .visibility
 | 
			
		||||
					margin-left 6px
 | 
			
		||||
 | 
			
		||||
		> .body
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,26 +14,7 @@
 | 
			
		||||
	<article>
 | 
			
		||||
		<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/>
 | 
			
		||||
		<div class="main">
 | 
			
		||||
			<header>
 | 
			
		||||
				<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle == 'smart'"/>
 | 
			
		||||
				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
 | 
			
		||||
				<span class="is-admin" v-if="p.user.isAdmin">admin</span>
 | 
			
		||||
				<span class="is-bot" v-if="p.user.isBot">bot</span>
 | 
			
		||||
				<span class="is-cat" v-if="p.user.isCat">cat</span>
 | 
			
		||||
				<span class="username"><mk-acct :user="p.user"/></span>
 | 
			
		||||
				<div class="info">
 | 
			
		||||
					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
 | 
			
		||||
					<router-link class="created-at" :to="p | notePage">
 | 
			
		||||
						<mk-time :time="p.createdAt"/>
 | 
			
		||||
					</router-link>
 | 
			
		||||
					<span class="visibility" v-if="p.visibility != 'public'">
 | 
			
		||||
						<template v-if="p.visibility == 'home'">%fa:home%</template>
 | 
			
		||||
						<template v-if="p.visibility == 'followers'">%fa:unlock%</template>
 | 
			
		||||
						<template v-if="p.visibility == 'specified'">%fa:envelope%</template>
 | 
			
		||||
						<template v-if="p.visibility == 'private'">%fa:lock%</template>
 | 
			
		||||
					</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</header>
 | 
			
		||||
			<mk-note-header class="header" :note="p" :mini="true"/>
 | 
			
		||||
			<div class="body">
 | 
			
		||||
				<p v-if="p.cw != null" class="cw">
 | 
			
		||||
					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
 | 
			
		||||
@@ -52,7 +33,7 @@
 | 
			
		||||
					</div>
 | 
			
		||||
					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 | 
			
		||||
					<div class="tags" v-if="p.tags && p.tags.length > 0">
 | 
			
		||||
						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 | 
			
		||||
						<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
 | 
			
		||||
					</div>
 | 
			
		||||
					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 | 
			
		||||
					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
			
		||||
@@ -358,65 +339,10 @@ root(isDark)
 | 
			
		||||
			flex 1
 | 
			
		||||
			min-width 0
 | 
			
		||||
 | 
			
		||||
			> header
 | 
			
		||||
				display flex
 | 
			
		||||
				align-items baseline
 | 
			
		||||
				white-space nowrap
 | 
			
		||||
 | 
			
		||||
			> .header
 | 
			
		||||
				@media (min-width 500px)
 | 
			
		||||
					margin-bottom 2px
 | 
			
		||||
 | 
			
		||||
				> .avatar
 | 
			
		||||
					flex-shrink 0
 | 
			
		||||
					margin-right 8px
 | 
			
		||||
					width 20px
 | 
			
		||||
					height 20px
 | 
			
		||||
					border-radius 100%
 | 
			
		||||
 | 
			
		||||
				> .name
 | 
			
		||||
					display block
 | 
			
		||||
					margin 0 0.5em 0 0
 | 
			
		||||
					padding 0
 | 
			
		||||
					overflow hidden
 | 
			
		||||
					color isDark ? #fff : #627079
 | 
			
		||||
					font-weight bold
 | 
			
		||||
					text-decoration none
 | 
			
		||||
					text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
				> .is-admin
 | 
			
		||||
				> .is-bot
 | 
			
		||||
				> .is-cat
 | 
			
		||||
					align-self center
 | 
			
		||||
					margin 0 0.5em 0 0
 | 
			
		||||
					padding 1px 6px
 | 
			
		||||
					font-size 0.8em
 | 
			
		||||
					color isDark ? #758188 : #aaa
 | 
			
		||||
					border solid 1px isDark ? #57616f : #ddd
 | 
			
		||||
					border-radius 3px
 | 
			
		||||
 | 
			
		||||
					&.is-admin
 | 
			
		||||
						border-color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
						color isDark ? #d42c41 : #f56a7b
 | 
			
		||||
 | 
			
		||||
				> .username
 | 
			
		||||
					margin 0 0.5em 0 0
 | 
			
		||||
					overflow hidden
 | 
			
		||||
					text-overflow ellipsis
 | 
			
		||||
					color isDark ? #606984 : #ccc
 | 
			
		||||
 | 
			
		||||
				> .info
 | 
			
		||||
					margin-left auto
 | 
			
		||||
					font-size 0.9em
 | 
			
		||||
 | 
			
		||||
					> *
 | 
			
		||||
						color isDark ? #606984 : #c0c0c0
 | 
			
		||||
 | 
			
		||||
					> .mobile
 | 
			
		||||
						margin-right 6px
 | 
			
		||||
 | 
			
		||||
					> .visibility
 | 
			
		||||
						margin-left 6px
 | 
			
		||||
 | 
			
		||||
			> .body
 | 
			
		||||
				@media (min-width 700px)
 | 
			
		||||
					font-size 1.1em
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,7 @@ import * as XDraggable from 'vuedraggable';
 | 
			
		||||
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
 | 
			
		||||
import getKao from '../../../common/scripts/get-kao';
 | 
			
		||||
import parse from '../../../../../text/parse';
 | 
			
		||||
import { host } from '../../../config';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
@@ -123,6 +124,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
				// 自分は除外
 | 
			
		||||
				if (this.$store.state.i.username == x.username && x.host == null) return;
 | 
			
		||||
				if (this.$store.state.i.username == x.username && x.host == host) return;
 | 
			
		||||
 | 
			
		||||
				// 重複は除外
 | 
			
		||||
				if (this.text.indexOf(`${mention} `) != -1) return;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										81
									
								
								src/client/app/mobile/views/pages/tag.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/client/app/mobile/views/pages/tag.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
<template>
 | 
			
		||||
<mk-ui>
 | 
			
		||||
	<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span>
 | 
			
		||||
 | 
			
		||||
	<main>
 | 
			
		||||
		<p v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p>
 | 
			
		||||
		<mk-notes ref="timeline" :more="existMore ? more : null"/>
 | 
			
		||||
	</main>
 | 
			
		||||
</mk-ui>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import Progress from '../../../common/scripts/loading';
 | 
			
		||||
 | 
			
		||||
const limit = 20;
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			moreFetching: false,
 | 
			
		||||
			existMore: false,
 | 
			
		||||
			offset: 0,
 | 
			
		||||
			empty: false
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	watch: {
 | 
			
		||||
		$route: 'fetch'
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$nextTick(() => {
 | 
			
		||||
			this.fetch();
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetch() {
 | 
			
		||||
			this.fetching = true;
 | 
			
		||||
			Progress.start();
 | 
			
		||||
 | 
			
		||||
			(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
 | 
			
		||||
				(this as any).api('notes/search_by_tag', {
 | 
			
		||||
					limit: limit + 1,
 | 
			
		||||
					offset: this.offset,
 | 
			
		||||
					tag: this.$route.params.tag
 | 
			
		||||
				}).then(notes => {
 | 
			
		||||
					if (notes.length == 0) this.empty = true;
 | 
			
		||||
					if (notes.length == limit + 1) {
 | 
			
		||||
						notes.pop();
 | 
			
		||||
						this.existMore = true;
 | 
			
		||||
					}
 | 
			
		||||
					res(notes);
 | 
			
		||||
					this.fetching = false;
 | 
			
		||||
					Progress.done();
 | 
			
		||||
				}, rej);
 | 
			
		||||
			}));
 | 
			
		||||
		},
 | 
			
		||||
		more() {
 | 
			
		||||
			this.offset += limit;
 | 
			
		||||
 | 
			
		||||
			const promise = (this as any).api('notes/search_by_tag', {
 | 
			
		||||
				limit: limit + 1,
 | 
			
		||||
				offset: this.offset,
 | 
			
		||||
				tag: this.$route.params.tag
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			promise.then(notes => {
 | 
			
		||||
				if (notes.length == limit + 1) {
 | 
			
		||||
					notes.pop();
 | 
			
		||||
				} else {
 | 
			
		||||
					this.existMore = false;
 | 
			
		||||
				}
 | 
			
		||||
				notes.forEach(n => (this.$refs.timeline as any).append(n));
 | 
			
		||||
				this.moreFetching = false;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return promise;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -15,6 +15,8 @@
 | 
			
		||||
					<option value="rss">%i18n:common.widgets.rss%</option>
 | 
			
		||||
					<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
 | 
			
		||||
					<option value="slideshow">%i18n:common.widgets.slideshow%</option>
 | 
			
		||||
					<option value="hashtags">%i18n:common.widgets.hashtags%</option>
 | 
			
		||||
					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
 | 
			
		||||
					<option value="version">%i18n:common.widgets.version%</option>
 | 
			
		||||
					<option value="server">%i18n:common.widgets.server%</option>
 | 
			
		||||
					<option value="memo">%i18n:common.widgets.memo%</option>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,10 @@
 | 
			
		||||
// Detect an old browser
 | 
			
		||||
if (!('fetch' in window)) {
 | 
			
		||||
	alert(
 | 
			
		||||
		'お使いのブラウザが古いためMisskeyを動作させることができません。' +
 | 
			
		||||
		'お使いのブラウザ(またはOS)が古いためMisskeyを動作させることができません。' +
 | 
			
		||||
		'バージョンを最新のものに更新するか、別のブラウザをお試しください。' +
 | 
			
		||||
		'\n\n' +
 | 
			
		||||
		'Your browser seems outdated. ' +
 | 
			
		||||
		'Your browser (or your OS) seems outdated. ' +
 | 
			
		||||
		'To run Misskey, please update your browser to latest version or try other browsers.');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -180,6 +180,7 @@ export default (os: MiOS) => new Vuex.Store({
 | 
			
		||||
				removeDeckColumn(state, id) {
 | 
			
		||||
					state.deck.columns = state.deck.columns.filter(c => c.id != id);
 | 
			
		||||
					state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
 | 
			
		||||
					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
 | 
			
		||||
				},
 | 
			
		||||
 | 
			
		||||
				swapDeckColumn(state, x) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								src/daemons/hashtags-stats-child.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/daemons/hashtags-stats-child.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
import Note from '../models/note';
 | 
			
		||||
 | 
			
		||||
// 10分
 | 
			
		||||
const interval = 1000 * 60 * 10;
 | 
			
		||||
 | 
			
		||||
async function tick() {
 | 
			
		||||
	const res = await Note.aggregate([{
 | 
			
		||||
		$match: {
 | 
			
		||||
			createdAt: {
 | 
			
		||||
				$gt: new Date(Date.now() - interval)
 | 
			
		||||
			},
 | 
			
		||||
			tags: {
 | 
			
		||||
				$exists: true,
 | 
			
		||||
				$ne: []
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}, {
 | 
			
		||||
		$unwind: '$tags'
 | 
			
		||||
	}, {
 | 
			
		||||
		$group: {
 | 
			
		||||
			_id: '$tags',
 | 
			
		||||
			count: {
 | 
			
		||||
				$sum: 1
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}, {
 | 
			
		||||
		$group: {
 | 
			
		||||
			_id: null,
 | 
			
		||||
			tags: {
 | 
			
		||||
				$push: {
 | 
			
		||||
					tag: '$_id',
 | 
			
		||||
					count: '$count'
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}, {
 | 
			
		||||
		$project: {
 | 
			
		||||
			_id: false,
 | 
			
		||||
			tags: true
 | 
			
		||||
		}
 | 
			
		||||
	}]) as {
 | 
			
		||||
		tags: Array<{
 | 
			
		||||
			tag: string;
 | 
			
		||||
			count: number;
 | 
			
		||||
		}>
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const stats = res.tags
 | 
			
		||||
		.sort((a, b) => a.count - b.count)
 | 
			
		||||
		.map(tag => [tag.tag, tag.count])
 | 
			
		||||
		.slice(0, 10);
 | 
			
		||||
 | 
			
		||||
	console.log(stats);
 | 
			
		||||
 | 
			
		||||
	process.send(stats);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tick();
 | 
			
		||||
 | 
			
		||||
setInterval(tick, interval);
 | 
			
		||||
							
								
								
									
										20
									
								
								src/daemons/hashtags-stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/daemons/hashtags-stats.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import * as childProcess from 'child_process';
 | 
			
		||||
import Xev from 'xev';
 | 
			
		||||
 | 
			
		||||
const ev = new Xev();
 | 
			
		||||
 | 
			
		||||
export default function() {
 | 
			
		||||
	const log = [];
 | 
			
		||||
 | 
			
		||||
	const p = childProcess.fork(__dirname + '/hashtags-stats-child.js');
 | 
			
		||||
 | 
			
		||||
	p.on('message', stats => {
 | 
			
		||||
		ev.emit('hashtagsStats', stats);
 | 
			
		||||
		log.push(stats);
 | 
			
		||||
		if (log.length > 30) log.shift();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	ev.on('requestHashTagsStatsLog', id => {
 | 
			
		||||
		ev.emit('hashtagsStatsLog:' + id, log);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								src/daemons/notes-stats-child.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/daemons/notes-stats-child.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
import Note from '../models/note';
 | 
			
		||||
 | 
			
		||||
const interval = 5000;
 | 
			
		||||
 | 
			
		||||
async function tick() {
 | 
			
		||||
	const [all, local] = await Promise.all([Note.count({
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			$gte: new Date(Date.now() - interval)
 | 
			
		||||
		}
 | 
			
		||||
	}), Note.count({
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			$gte: new Date(Date.now() - interval)
 | 
			
		||||
		},
 | 
			
		||||
		'_user.host': null
 | 
			
		||||
	})]);
 | 
			
		||||
 | 
			
		||||
	const stats = {
 | 
			
		||||
		all, local
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	process.send(stats);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tick();
 | 
			
		||||
 | 
			
		||||
setInterval(tick, interval);
 | 
			
		||||
							
								
								
									
										20
									
								
								src/daemons/notes-stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/daemons/notes-stats.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import * as childProcess from 'child_process';
 | 
			
		||||
import Xev from 'xev';
 | 
			
		||||
 | 
			
		||||
const ev = new Xev();
 | 
			
		||||
 | 
			
		||||
export default function() {
 | 
			
		||||
	const log = [];
 | 
			
		||||
 | 
			
		||||
	const p = childProcess.fork(__dirname + '/notes-stats-child.js');
 | 
			
		||||
 | 
			
		||||
	p.on('message', stats => {
 | 
			
		||||
		ev.emit('notesStats', stats);
 | 
			
		||||
		log.push(stats);
 | 
			
		||||
		if (log.length > 100) log.shift();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	ev.on('requestNotesStatsLog', id => {
 | 
			
		||||
		ev.emit('notesStatsLog:' + id, log);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@@ -5,14 +5,22 @@ import Xev from 'xev';
 | 
			
		||||
 | 
			
		||||
const ev = new Xev();
 | 
			
		||||
 | 
			
		||||
const interval = 1000;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Report stats regularly
 | 
			
		||||
 * Report server stats regularly
 | 
			
		||||
 */
 | 
			
		||||
export default function() {
 | 
			
		||||
	setInterval(() => {
 | 
			
		||||
	const log = [];
 | 
			
		||||
 | 
			
		||||
	ev.on('requestServerStatsLog', id => {
 | 
			
		||||
		ev.emit('serverStatsLog:' + id, log);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	async function tick() {
 | 
			
		||||
		osUtils.cpuUsage(cpuUsage => {
 | 
			
		||||
			const disk = diskusage.checkSync(os.platform() == 'win32' ? 'c:' : '/');
 | 
			
		||||
			ev.emit('stats', {
 | 
			
		||||
			const stats = {
 | 
			
		||||
				cpu_usage: cpuUsage,
 | 
			
		||||
				mem: {
 | 
			
		||||
					total: os.totalmem(),
 | 
			
		||||
@@ -21,7 +29,14 @@ export default function() {
 | 
			
		||||
				disk,
 | 
			
		||||
				os_uptime: os.uptime(),
 | 
			
		||||
				process_uptime: process.uptime()
 | 
			
		||||
			});
 | 
			
		||||
			};
 | 
			
		||||
			ev.emit('serverStats', stats);
 | 
			
		||||
			log.push(stats);
 | 
			
		||||
			if (log.length > 50) log.shift();
 | 
			
		||||
		});
 | 
			
		||||
	}, 1000);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tick();
 | 
			
		||||
 | 
			
		||||
	setInterval(tick, interval);
 | 
			
		||||
}
 | 
			
		||||
@@ -17,7 +17,8 @@ import ProgressBar from './utils/cli/progressbar';
 | 
			
		||||
import EnvironmentInfo from './utils/environmentInfo';
 | 
			
		||||
import MachineInfo from './utils/machineInfo';
 | 
			
		||||
import DependencyInfo from './utils/dependencyInfo';
 | 
			
		||||
import stats from './utils/stats';
 | 
			
		||||
import serverStats from './daemons/server-stats';
 | 
			
		||||
import notesStats from './daemons/notes-stats';
 | 
			
		||||
 | 
			
		||||
import loadConfig from './config/load';
 | 
			
		||||
import { Config } from './config/types';
 | 
			
		||||
@@ -49,7 +50,8 @@ function main() {
 | 
			
		||||
		masterMain(opt);
 | 
			
		||||
 | 
			
		||||
		ev.mount();
 | 
			
		||||
		stats();
 | 
			
		||||
		serverStats();
 | 
			
		||||
		notesStats();
 | 
			
		||||
	} else {
 | 
			
		||||
		workerMain(opt);
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,10 @@ import Following from './following';
 | 
			
		||||
const Note = db.get<INote>('notes');
 | 
			
		||||
Note.createIndex('uri', { sparse: true, unique: true });
 | 
			
		||||
Note.createIndex('userId');
 | 
			
		||||
Note.createIndex('tags', { sparse: true });
 | 
			
		||||
Note.createIndex({
 | 
			
		||||
	createdAt: -1
 | 
			
		||||
});
 | 
			
		||||
export default Note;
 | 
			
		||||
 | 
			
		||||
export function isValidText(text: string): boolean {
 | 
			
		||||
@@ -44,6 +48,11 @@ export type INote = {
 | 
			
		||||
	repliesCount: number;
 | 
			
		||||
	reactionCounts: any;
 | 
			
		||||
	mentions: mongo.ObjectID[];
 | 
			
		||||
	mentionedRemoteUsers: Array<{
 | 
			
		||||
		uri: string;
 | 
			
		||||
		username: string;
 | 
			
		||||
		host: string;
 | 
			
		||||
	}>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * public ... 公開
 | 
			
		||||
@@ -285,7 +294,7 @@ export const pack = async (
 | 
			
		||||
 | 
			
		||||
		// Poll
 | 
			
		||||
		if (meId && _note.poll && !hide) {
 | 
			
		||||
			_note.poll = (async (poll) => {
 | 
			
		||||
			_note.poll = (async poll => {
 | 
			
		||||
				const vote = await PollVote
 | 
			
		||||
					.findOne({
 | 
			
		||||
						userId: meId,
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,8 @@ type IUserBase = {
 | 
			
		||||
	usernameLower: string;
 | 
			
		||||
	avatarId: mongo.ObjectID;
 | 
			
		||||
	bannerId: mongo.ObjectID;
 | 
			
		||||
	avatarUrl?: string;
 | 
			
		||||
	bannerUrl?: string;
 | 
			
		||||
	wallpaperId: mongo.ObjectID;
 | 
			
		||||
	data: any;
 | 
			
		||||
	description: string;
 | 
			
		||||
@@ -405,13 +407,17 @@ export const pack = (
 | 
			
		||||
		delete _user.publicKey;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_user.avatarUrl = _user.avatarId != null
 | 
			
		||||
		? `${config.drive_url}/${_user.avatarId}`
 | 
			
		||||
		: `${config.drive_url}/default-avatar.jpg`;
 | 
			
		||||
	if (_user.avatarUrl == null) {
 | 
			
		||||
		_user.avatarUrl = _user.avatarId != null
 | 
			
		||||
			? `${config.drive_url}/${_user.avatarId}`
 | 
			
		||||
			: `${config.drive_url}/default-avatar.jpg`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_user.bannerUrl = _user.bannerId != null
 | 
			
		||||
		? `${config.drive_url}/${_user.bannerId}`
 | 
			
		||||
		: null;
 | 
			
		||||
	if (_user.bannerUrl == null) {
 | 
			
		||||
		_user.bannerUrl = _user.bannerId != null
 | 
			
		||||
			? `${config.drive_url}/${_user.bannerId}`
 | 
			
		||||
			: null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_user.wallpaperUrl = _user.wallpaperId != null
 | 
			
		||||
		? `${config.drive_url}/${_user.wallpaperId}`
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,11 @@ const log = debug('misskey:activitypub');
 | 
			
		||||
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
 | 
			
		||||
	const uri = activity.id || activity;
 | 
			
		||||
 | 
			
		||||
	// アナウンサーが凍結されていたらスキップ
 | 
			
		||||
	if (actor.isSuspended) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (typeof uri !== 'string') {
 | 
			
		||||
		throw new Error('invalid announce');
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import webFinger from '../../webfinger';
 | 
			
		||||
import Resolver from '../resolver';
 | 
			
		||||
import { resolveImage } from './image';
 | 
			
		||||
import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
 | 
			
		||||
import { IDriveFile } from '../../../models/drive-file';
 | 
			
		||||
 | 
			
		||||
const log = debug('misskey:activitypub');
 | 
			
		||||
 | 
			
		||||
@@ -117,19 +118,33 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region アイコンとヘッダー画像をフェッチ
 | 
			
		||||
	const [avatarId, bannerId] = (await Promise.all([
 | 
			
		||||
	const [avatar, banner] = (await Promise.all<IDriveFile>([
 | 
			
		||||
		person.icon,
 | 
			
		||||
		person.image
 | 
			
		||||
	].map(img =>
 | 
			
		||||
		img == null
 | 
			
		||||
			? Promise.resolve(null)
 | 
			
		||||
			: resolveImage(user, img)
 | 
			
		||||
	))).map(file => file != null ? file._id : null);
 | 
			
		||||
	)));
 | 
			
		||||
 | 
			
		||||
	User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
 | 
			
		||||
	const avatarId = avatar ? avatar._id : null;
 | 
			
		||||
	const bannerId = banner ? banner._id : null;
 | 
			
		||||
	const avatarUrl = avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null;
 | 
			
		||||
	const bannerUrl = banner && banner.metadata.isMetaOnly ? banner.metadata.url : null;
 | 
			
		||||
 | 
			
		||||
	await User.update({ _id: user._id }, {
 | 
			
		||||
		$set: {
 | 
			
		||||
			avatarId,
 | 
			
		||||
			bannerId,
 | 
			
		||||
			avatarUrl,
 | 
			
		||||
			bannerUrl
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	user.avatarId = avatarId;
 | 
			
		||||
	user.bannerId = bannerId;
 | 
			
		||||
	user.avatarUrl = avatarUrl;
 | 
			
		||||
	user.bannerUrl = bannerUrl;
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	return user;
 | 
			
		||||
@@ -190,21 +205,23 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
 | 
			
		||||
	const summaryDOM = JSDOM.fragment(person.summary);
 | 
			
		||||
 | 
			
		||||
	// アイコンとヘッダー画像をフェッチ
 | 
			
		||||
	const [avatarId, bannerId] = (await Promise.all([
 | 
			
		||||
	const [avatar, banner] = (await Promise.all<IDriveFile>([
 | 
			
		||||
		person.icon,
 | 
			
		||||
		person.image
 | 
			
		||||
	].map(img =>
 | 
			
		||||
		img == null
 | 
			
		||||
			? Promise.resolve(null)
 | 
			
		||||
			: resolveImage(exist, img)
 | 
			
		||||
	))).map(file => file != null ? file._id : null);
 | 
			
		||||
	)));
 | 
			
		||||
 | 
			
		||||
	// Update user
 | 
			
		||||
	await User.update({ _id: exist._id }, {
 | 
			
		||||
		$set: {
 | 
			
		||||
			updatedAt: new Date(),
 | 
			
		||||
			avatarId,
 | 
			
		||||
			bannerId,
 | 
			
		||||
			avatarId: avatar ? avatar._id : null,
 | 
			
		||||
			bannerId: banner ? banner._id : null,
 | 
			
		||||
			avatarUrl: avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null,
 | 
			
		||||
			bannerUrl: banner && banner.metadata.isMetaOnly ? banner.metadata.url : null,
 | 
			
		||||
			description: summaryDOM.textContent,
 | 
			
		||||
			followersCount,
 | 
			
		||||
			followingCount,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import config from '../../../config';
 | 
			
		||||
 | 
			
		||||
export default tag => ({
 | 
			
		||||
export default (tag: string) => ({
 | 
			
		||||
	type: 'Hashtag',
 | 
			
		||||
	href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
 | 
			
		||||
	href: `${config.url}/tags/${encodeURIComponent(tag)}`,
 | 
			
		||||
	name: '#' + tag
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/remote/activitypub/renderer/mention.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/remote/activitypub/renderer/mention.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
export default (mention: {
 | 
			
		||||
	uri: string;
 | 
			
		||||
	username: string;
 | 
			
		||||
	host: string;
 | 
			
		||||
}) => ({
 | 
			
		||||
	type: 'Mention',
 | 
			
		||||
	href: mention.uri,
 | 
			
		||||
	name: `@${mention.username}@${mention.host}`
 | 
			
		||||
});
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import renderDocument from './document';
 | 
			
		||||
import renderHashtag from './hashtag';
 | 
			
		||||
import renderMention from './mention';
 | 
			
		||||
import config from '../../../config';
 | 
			
		||||
import DriveFile from '../../../models/drive-file';
 | 
			
		||||
import Note, { INote } from '../../../models/note';
 | 
			
		||||
@@ -45,6 +46,18 @@ export default async function renderNote(note: INote, dive = true) {
 | 
			
		||||
 | 
			
		||||
	const attributedTo = `${config.url}/users/${user._id}`;
 | 
			
		||||
 | 
			
		||||
	const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0
 | 
			
		||||
		? note.mentionedRemoteUsers.map(x => x.uri)
 | 
			
		||||
		: [];
 | 
			
		||||
 | 
			
		||||
	const cc = ['public', 'home', 'followers'].includes(note.visibility)
 | 
			
		||||
		? [`${attributedTo}/followers`].concat(mentions)
 | 
			
		||||
		: [];
 | 
			
		||||
 | 
			
		||||
	const hashtagTags = (note.tags || []).map(renderHashtag);
 | 
			
		||||
	const mentionTags = (note.mentionedRemoteUsers || []).map(renderMention);
 | 
			
		||||
	const tag = hashtagTags.concat(mentionTags)
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		id: `${config.url}/notes/${note._id}`,
 | 
			
		||||
		type: 'Note',
 | 
			
		||||
@@ -52,9 +65,9 @@ export default async function renderNote(note: INote, dive = true) {
 | 
			
		||||
		content: toHtml(note),
 | 
			
		||||
		published: note.createdAt.toISOString(),
 | 
			
		||||
		to: 'https://www.w3.org/ns/activitystreams#Public',
 | 
			
		||||
		cc: `${attributedTo}/followers`,
 | 
			
		||||
		cc,
 | 
			
		||||
		inReplyTo,
 | 
			
		||||
		attachment: (await promisedFiles).map(renderDocument),
 | 
			
		||||
		tag: (note.tags || []).map(renderHashtag)
 | 
			
		||||
		tag
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -525,6 +525,9 @@ const endpoints: Endpoint[] = [
 | 
			
		||||
	{
 | 
			
		||||
		name: 'notes/search'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		name: 'notes/search_by_tag'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		name: 'notes/timeline',
 | 
			
		||||
		withCredential: true,
 | 
			
		||||
@@ -625,6 +628,11 @@ const endpoints: Endpoint[] = [
 | 
			
		||||
		withCredential: true
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		name: 'hashtags/trend',
 | 
			
		||||
		withCredential: true
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		name: 'messaging/history',
 | 
			
		||||
		withCredential: true,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										136
									
								
								src/server/api/endpoints/hashtags/trend.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/server/api/endpoints/hashtags/trend.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
			
		||||
import Note from '../../../../models/note';
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
 | 
			
		||||
ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const rangeA = 1000 * 60 * 30; // 30分
 | 
			
		||||
const rangeB = 1000 * 60 * 120; // 2時間
 | 
			
		||||
const coefficient = 1.5; // 「n倍」の部分
 | 
			
		||||
const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
 | 
			
		||||
 | 
			
		||||
const max = 5;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get trends of hashtags
 | 
			
		||||
 */
 | 
			
		||||
module.exports = () => new Promise(async (res, rej) => {
 | 
			
		||||
	//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
 | 
			
		||||
	const data = await Note.aggregate([{
 | 
			
		||||
		$match: {
 | 
			
		||||
			createdAt: {
 | 
			
		||||
				$gt: new Date(Date.now() - rangeA)
 | 
			
		||||
			},
 | 
			
		||||
			tags: {
 | 
			
		||||
				$exists: true,
 | 
			
		||||
				$ne: []
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}, {
 | 
			
		||||
		$unwind: '$tags'
 | 
			
		||||
	}, {
 | 
			
		||||
		$group: {
 | 
			
		||||
			_id: { tags: '$tags', userId: '$userId' }
 | 
			
		||||
		}
 | 
			
		||||
	}]) as Array<{
 | 
			
		||||
		_id: {
 | 
			
		||||
			tags: string;
 | 
			
		||||
			userId: any;
 | 
			
		||||
		}
 | 
			
		||||
	}>;
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	if (data.length == 0) {
 | 
			
		||||
		return res([]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const tags = [];
 | 
			
		||||
 | 
			
		||||
	// カウント
 | 
			
		||||
	data.map(x => x._id).forEach(x => {
 | 
			
		||||
		const i = tags.findIndex(tag => tag.name == x.tags);
 | 
			
		||||
		if (i != -1) {
 | 
			
		||||
			tags[i].count++;
 | 
			
		||||
		} else {
 | 
			
		||||
			tags.push({
 | 
			
		||||
				name: x.tags,
 | 
			
		||||
				count: 1
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// 最低要求投稿者数を下回るならカットする
 | 
			
		||||
	const limitedTags = tags.filter(tag => tag.count >= requiredUsers);
 | 
			
		||||
 | 
			
		||||
	//#region 2. 1で取得したそれぞれのタグについて、「直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上」かどうかを判定する
 | 
			
		||||
	const hotsPromises = limitedTags.map(async tag => {
 | 
			
		||||
		const passedCount = (await Note.distinct('userId', {
 | 
			
		||||
			tags: tag.name,
 | 
			
		||||
			createdAt: {
 | 
			
		||||
				$lt: new Date(Date.now() - rangeA),
 | 
			
		||||
				$gt: new Date(Date.now() - rangeB)
 | 
			
		||||
			}
 | 
			
		||||
		}) as any).length;
 | 
			
		||||
 | 
			
		||||
		if (tag.count >= (passedCount * coefficient)) {
 | 
			
		||||
			return tag;
 | 
			
		||||
		} else {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	// タグを人気順に並べ替え
 | 
			
		||||
	let hots = (await Promise.all(hotsPromises))
 | 
			
		||||
		.filter(x => x != null)
 | 
			
		||||
		.sort((a, b) => b.count - a.count)
 | 
			
		||||
		.map(tag => tag.name)
 | 
			
		||||
		.slice(0, max);
 | 
			
		||||
 | 
			
		||||
	//#region 3. もし上記の方法でのトレンド抽出の結果、求められているタグ数に達しなければ「ただ単に現在投稿数が多いハッシュタグ」に切り替える
 | 
			
		||||
	if (hots.length < max) {
 | 
			
		||||
		hots = hots.concat(tags
 | 
			
		||||
			.filter(tag => hots.indexOf(tag.name) == -1)
 | 
			
		||||
			.sort((a, b) => b.count - a.count)
 | 
			
		||||
			.map(tag => tag.name)
 | 
			
		||||
			.slice(0, max - hots.length));
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
 | 
			
		||||
	const countPromises: Array<Promise<any[]>> = [];
 | 
			
		||||
 | 
			
		||||
	const range = 20;
 | 
			
		||||
 | 
			
		||||
	// 10分
 | 
			
		||||
	const interval = 1000 * 60 * 10;
 | 
			
		||||
 | 
			
		||||
	for (let i = 0; i < range; i++) {
 | 
			
		||||
		countPromises.push(Promise.all(hots.map(tag => Note.distinct('userId', {
 | 
			
		||||
			tags: tag,
 | 
			
		||||
			createdAt: {
 | 
			
		||||
				$lt: new Date(Date.now() - (interval * i)),
 | 
			
		||||
				$gt: new Date(Date.now() - (interval * (i + 1)))
 | 
			
		||||
			}
 | 
			
		||||
		}))));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const countsLog = await Promise.all(countPromises);
 | 
			
		||||
 | 
			
		||||
	const totalCounts: any = await Promise.all(hots.map(tag => Note.distinct('userId', {
 | 
			
		||||
		tags: tag,
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			$gt: new Date(Date.now() - (interval * range))
 | 
			
		||||
		}
 | 
			
		||||
	})));
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	const stats = hots.map((tag, i) => ({
 | 
			
		||||
		tag,
 | 
			
		||||
		chart: countsLog.map(counts => counts[i].length),
 | 
			
		||||
		usersCount: totalCounts[i].length
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	res(stats);
 | 
			
		||||
});
 | 
			
		||||
@@ -140,7 +140,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
 | 
			
		||||
	if (text === undefined && files === null && renote === null && poll === undefined) {
 | 
			
		||||
	if ((text === undefined || text === null) && files === null && renote === null && poll === undefined) {
 | 
			
		||||
		return rej('text, mediaIds, renoteId or poll is required');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										329
									
								
								src/server/api/endpoints/notes/search_by_tag.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										329
									
								
								src/server/api/endpoints/notes/search_by_tag.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,329 @@
 | 
			
		||||
import $ from 'cafy'; import ID from '../../../../cafy-id';
 | 
			
		||||
import Note from '../../../../models/note';
 | 
			
		||||
import User from '../../../../models/user';
 | 
			
		||||
import Mute from '../../../../models/mute';
 | 
			
		||||
import { getFriendIds } from '../../common/get-friends';
 | 
			
		||||
import { pack } from '../../../../models/note';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Search notes by tag
 | 
			
		||||
 */
 | 
			
		||||
module.exports = (params, me) => new Promise(async (res, rej) => {
 | 
			
		||||
	// Get 'tag' parameter
 | 
			
		||||
	const [tag, tagError] = $.str.get(params.tag);
 | 
			
		||||
	if (tagError) return rej('invalid tag param');
 | 
			
		||||
 | 
			
		||||
	// Get 'includeUserIds' parameter
 | 
			
		||||
	const [includeUserIds = [], includeUserIdsErr] = $.arr($.type(ID)).optional().get(params.includeUserIds);
 | 
			
		||||
	if (includeUserIdsErr) return rej('invalid includeUserIds param');
 | 
			
		||||
 | 
			
		||||
	// Get 'excludeUserIds' parameter
 | 
			
		||||
	const [excludeUserIds = [], excludeUserIdsErr] = $.arr($.type(ID)).optional().get(params.excludeUserIds);
 | 
			
		||||
	if (excludeUserIdsErr) return rej('invalid excludeUserIds param');
 | 
			
		||||
 | 
			
		||||
	// Get 'includeUserUsernames' parameter
 | 
			
		||||
	const [includeUserUsernames = [], includeUserUsernamesErr] = $.arr($.str).optional().get(params.includeUserUsernames);
 | 
			
		||||
	if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
 | 
			
		||||
 | 
			
		||||
	// Get 'excludeUserUsernames' parameter
 | 
			
		||||
	const [excludeUserUsernames = [], excludeUserUsernamesErr] = $.arr($.str).optional().get(params.excludeUserUsernames);
 | 
			
		||||
	if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param');
 | 
			
		||||
 | 
			
		||||
	// Get 'following' parameter
 | 
			
		||||
	const [following = null, followingErr] = $.bool.optional().nullable().get(params.following);
 | 
			
		||||
	if (followingErr) return rej('invalid following param');
 | 
			
		||||
 | 
			
		||||
	// Get 'mute' parameter
 | 
			
		||||
	const [mute = 'mute_all', muteErr] = $.str.optional().get(params.mute);
 | 
			
		||||
	if (muteErr) return rej('invalid mute param');
 | 
			
		||||
 | 
			
		||||
	// Get 'reply' parameter
 | 
			
		||||
	const [reply = null, replyErr] = $.bool.optional().nullable().get(params.reply);
 | 
			
		||||
	if (replyErr) return rej('invalid reply param');
 | 
			
		||||
 | 
			
		||||
	// Get 'renote' parameter
 | 
			
		||||
	const [renote = null, renoteErr] = $.bool.optional().nullable().get(params.renote);
 | 
			
		||||
	if (renoteErr) return rej('invalid renote param');
 | 
			
		||||
 | 
			
		||||
	// Get 'media' parameter
 | 
			
		||||
	const [media = null, mediaErr] = $.bool.optional().nullable().get(params.media);
 | 
			
		||||
	if (mediaErr) return rej('invalid media param');
 | 
			
		||||
 | 
			
		||||
	// Get 'poll' parameter
 | 
			
		||||
	const [poll = null, pollErr] = $.bool.optional().nullable().get(params.poll);
 | 
			
		||||
	if (pollErr) return rej('invalid poll param');
 | 
			
		||||
 | 
			
		||||
	// Get 'sinceDate' parameter
 | 
			
		||||
	const [sinceDate, sinceDateErr] = $.num.optional().get(params.sinceDate);
 | 
			
		||||
	if (sinceDateErr) throw 'invalid sinceDate param';
 | 
			
		||||
 | 
			
		||||
	// Get 'untilDate' parameter
 | 
			
		||||
	const [untilDate, untilDateErr] = $.num.optional().get(params.untilDate);
 | 
			
		||||
	if (untilDateErr) throw 'invalid untilDate param';
 | 
			
		||||
 | 
			
		||||
	// Get 'offset' parameter
 | 
			
		||||
	const [offset = 0, offsetErr] = $.num.optional().min(0).get(params.offset);
 | 
			
		||||
	if (offsetErr) return rej('invalid offset param');
 | 
			
		||||
 | 
			
		||||
	// Get 'limit' parameter
 | 
			
		||||
	const [limit = 10, limitErr] = $.num.optional().range(1, 30).get(params.limit);
 | 
			
		||||
	if (limitErr) return rej('invalid limit param');
 | 
			
		||||
 | 
			
		||||
	let includeUsers = includeUserIds;
 | 
			
		||||
	if (includeUserUsernames != null) {
 | 
			
		||||
		const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
 | 
			
		||||
			const _user = await User.findOne({
 | 
			
		||||
				usernameLower: username.toLowerCase()
 | 
			
		||||
			});
 | 
			
		||||
			return _user ? _user._id : null;
 | 
			
		||||
		}))).filter(id => id != null);
 | 
			
		||||
		includeUsers = includeUsers.concat(ids);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let excludeUsers = excludeUserIds;
 | 
			
		||||
	if (excludeUserUsernames != null) {
 | 
			
		||||
		const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
 | 
			
		||||
			const _user = await User.findOne({
 | 
			
		||||
				usernameLower: username.toLowerCase()
 | 
			
		||||
			});
 | 
			
		||||
			return _user ? _user._id : null;
 | 
			
		||||
		}))).filter(id => id != null);
 | 
			
		||||
		excludeUsers = excludeUsers.concat(ids);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	search(res, rej, me, tag, includeUsers, excludeUsers, following,
 | 
			
		||||
			mute, reply, renote, media, poll, sinceDate, untilDate, offset, limit);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function search(
 | 
			
		||||
	res, rej, me, tag, includeUserIds, excludeUserIds, following,
 | 
			
		||||
	mute, reply, renote, media, poll, sinceDate, untilDate, offset, max) {
 | 
			
		||||
 | 
			
		||||
	let q: any = {
 | 
			
		||||
		$and: [{
 | 
			
		||||
			tags: tag
 | 
			
		||||
		}]
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const push = x => q.$and.push(x);
 | 
			
		||||
 | 
			
		||||
	if (includeUserIds && includeUserIds.length != 0) {
 | 
			
		||||
		push({
 | 
			
		||||
			userId: {
 | 
			
		||||
				$in: includeUserIds
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	} else if (excludeUserIds && excludeUserIds.length != 0) {
 | 
			
		||||
		push({
 | 
			
		||||
			userId: {
 | 
			
		||||
				$nin: excludeUserIds
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (following != null && me != null) {
 | 
			
		||||
		const ids = await getFriendIds(me._id, false);
 | 
			
		||||
		push({
 | 
			
		||||
			userId: following ? {
 | 
			
		||||
				$in: ids
 | 
			
		||||
			} : {
 | 
			
		||||
				$nin: ids.concat(me._id)
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (me != null) {
 | 
			
		||||
		const mutes = await Mute.find({
 | 
			
		||||
			muterId: me._id,
 | 
			
		||||
			deletedAt: { $exists: false }
 | 
			
		||||
		});
 | 
			
		||||
		const mutedUserIds = mutes.map(m => m.muteeId);
 | 
			
		||||
 | 
			
		||||
		switch (mute) {
 | 
			
		||||
			case 'mute_all':
 | 
			
		||||
				push({
 | 
			
		||||
					userId: {
 | 
			
		||||
						$nin: mutedUserIds
 | 
			
		||||
					},
 | 
			
		||||
					'_reply.userId': {
 | 
			
		||||
						$nin: mutedUserIds
 | 
			
		||||
					},
 | 
			
		||||
					'_renote.userId': {
 | 
			
		||||
						$nin: mutedUserIds
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
			case 'mute_related':
 | 
			
		||||
				push({
 | 
			
		||||
					'_reply.userId': {
 | 
			
		||||
						$nin: mutedUserIds
 | 
			
		||||
					},
 | 
			
		||||
					'_renote.userId': {
 | 
			
		||||
						$nin: mutedUserIds
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
			case 'mute_direct':
 | 
			
		||||
				push({
 | 
			
		||||
					userId: {
 | 
			
		||||
						$nin: mutedUserIds
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
			case 'direct_only':
 | 
			
		||||
				push({
 | 
			
		||||
					userId: {
 | 
			
		||||
						$in: mutedUserIds
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
			case 'related_only':
 | 
			
		||||
				push({
 | 
			
		||||
					$or: [{
 | 
			
		||||
						'_reply.userId': {
 | 
			
		||||
							$in: mutedUserIds
 | 
			
		||||
						}
 | 
			
		||||
					}, {
 | 
			
		||||
						'_renote.userId': {
 | 
			
		||||
							$in: mutedUserIds
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
			case 'all_only':
 | 
			
		||||
				push({
 | 
			
		||||
					$or: [{
 | 
			
		||||
						userId: {
 | 
			
		||||
							$in: mutedUserIds
 | 
			
		||||
						}
 | 
			
		||||
					}, {
 | 
			
		||||
						'_reply.userId': {
 | 
			
		||||
							$in: mutedUserIds
 | 
			
		||||
						}
 | 
			
		||||
					}, {
 | 
			
		||||
						'_renote.userId': {
 | 
			
		||||
							$in: mutedUserIds
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (reply != null) {
 | 
			
		||||
		if (reply) {
 | 
			
		||||
			push({
 | 
			
		||||
				replyId: {
 | 
			
		||||
					$exists: true,
 | 
			
		||||
					$ne: null
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			push({
 | 
			
		||||
				$or: [{
 | 
			
		||||
					replyId: {
 | 
			
		||||
						$exists: false
 | 
			
		||||
					}
 | 
			
		||||
				}, {
 | 
			
		||||
					replyId: null
 | 
			
		||||
				}]
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (renote != null) {
 | 
			
		||||
		if (renote) {
 | 
			
		||||
			push({
 | 
			
		||||
				renoteId: {
 | 
			
		||||
					$exists: true,
 | 
			
		||||
					$ne: null
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			push({
 | 
			
		||||
				$or: [{
 | 
			
		||||
					renoteId: {
 | 
			
		||||
						$exists: false
 | 
			
		||||
					}
 | 
			
		||||
				}, {
 | 
			
		||||
					renoteId: null
 | 
			
		||||
				}]
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (media != null) {
 | 
			
		||||
		if (media) {
 | 
			
		||||
			push({
 | 
			
		||||
				mediaIds: {
 | 
			
		||||
					$exists: true,
 | 
			
		||||
					$ne: null
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			push({
 | 
			
		||||
				$or: [{
 | 
			
		||||
					mediaIds: {
 | 
			
		||||
						$exists: false
 | 
			
		||||
					}
 | 
			
		||||
				}, {
 | 
			
		||||
					mediaIds: null
 | 
			
		||||
				}]
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (poll != null) {
 | 
			
		||||
		if (poll) {
 | 
			
		||||
			push({
 | 
			
		||||
				poll: {
 | 
			
		||||
					$exists: true,
 | 
			
		||||
					$ne: null
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			push({
 | 
			
		||||
				$or: [{
 | 
			
		||||
					poll: {
 | 
			
		||||
						$exists: false
 | 
			
		||||
					}
 | 
			
		||||
				}, {
 | 
			
		||||
					poll: null
 | 
			
		||||
				}]
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (sinceDate) {
 | 
			
		||||
		push({
 | 
			
		||||
			createdAt: {
 | 
			
		||||
				$gt: new Date(sinceDate)
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (untilDate) {
 | 
			
		||||
		push({
 | 
			
		||||
			createdAt: {
 | 
			
		||||
				$lt: new Date(untilDate)
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (q.$and.length == 0) {
 | 
			
		||||
		q = {};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Search notes
 | 
			
		||||
	const notes = await Note
 | 
			
		||||
		.find(q, {
 | 
			
		||||
			sort: {
 | 
			
		||||
				_id: -1
 | 
			
		||||
			},
 | 
			
		||||
			limit: max,
 | 
			
		||||
			skip: offset
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	// Serialize
 | 
			
		||||
	res(await Promise.all(notes.map(note => pack(note, me))));
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,7 @@ import * as debug from 'debug';
 | 
			
		||||
 | 
			
		||||
import User, { IUser } from '../../../models/user';
 | 
			
		||||
import Mute from '../../../models/mute';
 | 
			
		||||
import { pack as packNote } from '../../../models/note';
 | 
			
		||||
import { pack as packNote, pack } from '../../../models/note';
 | 
			
		||||
import readNotification from '../common/read-notification';
 | 
			
		||||
import call from '../call';
 | 
			
		||||
import { IApp } from '../../../models/app';
 | 
			
		||||
@@ -48,6 +48,14 @@ export default async function(
 | 
			
		||||
					}
 | 
			
		||||
					//#endregion
 | 
			
		||||
 | 
			
		||||
					// Renoteなら再pack
 | 
			
		||||
					if (x.type == 'note' && x.body.renoteId != null) {
 | 
			
		||||
						x.body.renote = await pack(x.body.renoteId, user, {
 | 
			
		||||
							detail: true
 | 
			
		||||
						});
 | 
			
		||||
						data = JSON.stringify(x);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					connection.send(data);
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					connection.send(data);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import * as redis from 'redis';
 | 
			
		||||
 | 
			
		||||
import { IUser } from '../../../models/user';
 | 
			
		||||
import Mute from '../../../models/mute';
 | 
			
		||||
import { pack } from '../../../models/note';
 | 
			
		||||
 | 
			
		||||
export default async function(
 | 
			
		||||
	request: websocket.request,
 | 
			
		||||
@@ -31,6 +32,13 @@ export default async function(
 | 
			
		||||
		}
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
		// Renoteなら再pack
 | 
			
		||||
		if (note.renoteId != null) {
 | 
			
		||||
			note.renote = await pack(note.renoteId, user, {
 | 
			
		||||
				detail: true
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		connection.send(JSON.stringify({
 | 
			
		||||
			type: 'note',
 | 
			
		||||
			body: note
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								src/server/api/stream/notes-stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/server/api/stream/notes-stats.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import * as websocket from 'websocket';
 | 
			
		||||
import Xev from 'xev';
 | 
			
		||||
 | 
			
		||||
const ev = new Xev();
 | 
			
		||||
 | 
			
		||||
export default function(request: websocket.request, connection: websocket.connection): void {
 | 
			
		||||
	const onStats = stats => {
 | 
			
		||||
		connection.send(JSON.stringify({
 | 
			
		||||
			type: 'stats',
 | 
			
		||||
			body: stats
 | 
			
		||||
		}));
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	connection.on('message', async data => {
 | 
			
		||||
		const msg = JSON.parse(data.utf8Data);
 | 
			
		||||
 | 
			
		||||
		switch (msg.type) {
 | 
			
		||||
			case 'requestLog':
 | 
			
		||||
				ev.once('notesStatsLog:' + msg.id, statsLog => {
 | 
			
		||||
					connection.send(JSON.stringify({
 | 
			
		||||
						type: 'statsLog',
 | 
			
		||||
						body: statsLog
 | 
			
		||||
					}));
 | 
			
		||||
				});
 | 
			
		||||
				ev.emit('requestNotesStatsLog', msg.id);
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	ev.addListener('notesStats', onStats);
 | 
			
		||||
 | 
			
		||||
	connection.on('close', () => {
 | 
			
		||||
		ev.removeListener('notesStats', onStats);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								src/server/api/stream/server-stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/server/api/stream/server-stats.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import * as websocket from 'websocket';
 | 
			
		||||
import Xev from 'xev';
 | 
			
		||||
 | 
			
		||||
const ev = new Xev();
 | 
			
		||||
 | 
			
		||||
export default function(request: websocket.request, connection: websocket.connection): void {
 | 
			
		||||
	const onStats = stats => {
 | 
			
		||||
		connection.send(JSON.stringify({
 | 
			
		||||
			type: 'stats',
 | 
			
		||||
			body: stats
 | 
			
		||||
		}));
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	connection.on('message', async data => {
 | 
			
		||||
		const msg = JSON.parse(data.utf8Data);
 | 
			
		||||
 | 
			
		||||
		switch (msg.type) {
 | 
			
		||||
			case 'requestLog':
 | 
			
		||||
				ev.once('serverStatsLog:' + msg.id, statsLog => {
 | 
			
		||||
					connection.send(JSON.stringify({
 | 
			
		||||
						type: 'statsLog',
 | 
			
		||||
						body: statsLog
 | 
			
		||||
					}));
 | 
			
		||||
				});
 | 
			
		||||
				ev.emit('requestServerStatsLog', msg.id);
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	ev.addListener('serverStats', onStats);
 | 
			
		||||
 | 
			
		||||
	connection.on('close', () => {
 | 
			
		||||
		ev.removeListener('serverStats', onStats);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import * as websocket from 'websocket';
 | 
			
		||||
import Xev from 'xev';
 | 
			
		||||
 | 
			
		||||
const ev = new Xev();
 | 
			
		||||
 | 
			
		||||
export default function(request: websocket.request, connection: websocket.connection): void {
 | 
			
		||||
	const onStats = stats => {
 | 
			
		||||
		connection.send(JSON.stringify({
 | 
			
		||||
			type: 'stats',
 | 
			
		||||
			body: stats
 | 
			
		||||
		}));
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	ev.addListener('stats', onStats);
 | 
			
		||||
 | 
			
		||||
	connection.on('close', () => {
 | 
			
		||||
		ev.removeListener('stats', onStats);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@@ -12,7 +12,8 @@ import messagingStream from './stream/messaging';
 | 
			
		||||
import messagingIndexStream from './stream/messaging-index';
 | 
			
		||||
import othelloGameStream from './stream/othello-game';
 | 
			
		||||
import othelloStream from './stream/othello';
 | 
			
		||||
import serverStream from './stream/server';
 | 
			
		||||
import serverStatsStream from './stream/server-stats';
 | 
			
		||||
import notesStatsStream from './stream/notes-stats';
 | 
			
		||||
import requestsStream from './stream/requests';
 | 
			
		||||
import { ParsedUrlQuery } from 'querystring';
 | 
			
		||||
import authenticate from './authenticate';
 | 
			
		||||
@@ -28,8 +29,13 @@ module.exports = (server: http.Server) => {
 | 
			
		||||
	ws.on('request', async (request) => {
 | 
			
		||||
		const connection = request.accept();
 | 
			
		||||
 | 
			
		||||
		if (request.resourceURL.pathname === '/server') {
 | 
			
		||||
			serverStream(request, connection);
 | 
			
		||||
		if (request.resourceURL.pathname === '/server-stats') {
 | 
			
		||||
			serverStatsStream(request, connection);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (request.resourceURL.pathname === '/notes-stats') {
 | 
			
		||||
			notesStatsStream(request, connection);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -164,8 +164,8 @@ export default async function(
 | 
			
		||||
			'metadata.deletedAt': { $exists: false }
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (much !== null) {
 | 
			
		||||
			log('file with same hash is found');
 | 
			
		||||
		if (much) {
 | 
			
		||||
			log(`file with same hash is found: ${much._id}`);
 | 
			
		||||
			return much;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -94,7 +94,7 @@ export default async (user: IUser, data: {
 | 
			
		||||
	if (data.visibility == null) data.visibility = 'public';
 | 
			
		||||
	if (data.viaMobile == null) data.viaMobile = false;
 | 
			
		||||
 | 
			
		||||
	const tags = data.tags || [];
 | 
			
		||||
	let tags = data.tags || [];
 | 
			
		||||
 | 
			
		||||
	let tokens: any[] = null;
 | 
			
		||||
 | 
			
		||||
@@ -114,6 +114,8 @@ export default async (user: IUser, data: {
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tags = tags.filter(tag => tag.length <= 100);
 | 
			
		||||
 | 
			
		||||
	if (data.visibleUsers) {
 | 
			
		||||
		data.visibleUsers = data.visibleUsers.filter(x => x != null);
 | 
			
		||||
	}
 | 
			
		||||
@@ -202,6 +204,62 @@ export default async (user: IUser, data: {
 | 
			
		||||
		return packAp(content);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	//#region メンション
 | 
			
		||||
	if (data.text) {
 | 
			
		||||
		// TODO: Drop dupulicates
 | 
			
		||||
		const mentionTokens = tokens
 | 
			
		||||
			.filter(t => t.type == 'mention');
 | 
			
		||||
 | 
			
		||||
		// TODO: Drop dupulicates
 | 
			
		||||
		const mentionedUsers = (await Promise.all(mentionTokens.map(async m => {
 | 
			
		||||
			try {
 | 
			
		||||
				return await resolveUser(m.username, m.host);
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		}))).filter(x => x != null);
 | 
			
		||||
 | 
			
		||||
		// Append mentions data
 | 
			
		||||
		if (mentionedUsers.length > 0) {
 | 
			
		||||
			const set = {
 | 
			
		||||
				mentions: mentionedUsers.map(u => u._id),
 | 
			
		||||
				mentionedRemoteUsers: mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({
 | 
			
		||||
					uri: (u as IRemoteUser).uri,
 | 
			
		||||
					username: u.username,
 | 
			
		||||
					host: u.host
 | 
			
		||||
				}))
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			Note.update({ _id: note._id }, {
 | 
			
		||||
				$set: set
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			Object.assign(note, set);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
 | 
			
		||||
			event(u, 'mention', noteObj);
 | 
			
		||||
 | 
			
		||||
			// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
 | 
			
		||||
			if (data.reply && data.reply.userId.equals(u._id)) return;
 | 
			
		||||
			if (data.renote && data.renote.userId.equals(u._id)) return;
 | 
			
		||||
 | 
			
		||||
			// Create notification
 | 
			
		||||
			notify(u._id, user._id, 'mention', {
 | 
			
		||||
				noteId: note._id
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			nm.push(u._id, 'mention');
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (isLocalUser(user)) {
 | 
			
		||||
			mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
 | 
			
		||||
				deliver(user, await render(), (u as IRemoteUser).inbox);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	if (!silent) {
 | 
			
		||||
		if (isLocalUser(user)) {
 | 
			
		||||
			if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
 | 
			
		||||
@@ -285,55 +343,6 @@ export default async (user: IUser, data: {
 | 
			
		||||
	}
 | 
			
		||||
	//#endergion
 | 
			
		||||
 | 
			
		||||
	//#region メンション
 | 
			
		||||
	if (data.text) {
 | 
			
		||||
		// TODO: Drop dupulicates
 | 
			
		||||
		const mentions = tokens
 | 
			
		||||
			.filter(t => t.type == 'mention');
 | 
			
		||||
 | 
			
		||||
		let mentionedUsers = await Promise.all(mentions.map(async m => {
 | 
			
		||||
			try {
 | 
			
		||||
				return await resolveUser(m.username, m.host);
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		// TODO: Drop dupulicates
 | 
			
		||||
		mentionedUsers = mentionedUsers.filter(x => x != null);
 | 
			
		||||
 | 
			
		||||
		mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
 | 
			
		||||
			event(u, 'mention', noteObj);
 | 
			
		||||
 | 
			
		||||
			// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
 | 
			
		||||
			if (data.reply && data.reply.userId.equals(u._id)) return;
 | 
			
		||||
			if (data.renote && data.renote.userId.equals(u._id)) return;
 | 
			
		||||
 | 
			
		||||
			// Create notification
 | 
			
		||||
			notify(u._id, user._id, 'mention', {
 | 
			
		||||
				noteId: note._id
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			nm.push(u._id, 'mention');
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (isLocalUser(user)) {
 | 
			
		||||
			mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
 | 
			
		||||
				deliver(user, await render(), (u as IRemoteUser).inbox);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Append mentions data
 | 
			
		||||
		if (mentionedUsers.length > 0) {
 | 
			
		||||
			Note.update({ _id: note._id }, {
 | 
			
		||||
				$set: {
 | 
			
		||||
					mentions: mentionedUsers.map(u => u._id)
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	// If has in reply to note
 | 
			
		||||
	if (data.reply) {
 | 
			
		||||
		// Increment replies count
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ export default async function(user: IUser, note: INote) {
 | 
			
		||||
		$set: {
 | 
			
		||||
			deletedAt: new Date(),
 | 
			
		||||
			text: null,
 | 
			
		||||
			tags: [],
 | 
			
		||||
			mediaIds: [],
 | 
			
		||||
			poll: null
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,10 @@ const handlers = {
 | 
			
		||||
 | 
			
		||||
	hashtag({ document }, { hashtag }) {
 | 
			
		||||
		const a = document.createElement('a');
 | 
			
		||||
		a.href = '/search?q=#' + hashtag;
 | 
			
		||||
		a.textContent = hashtag;
 | 
			
		||||
		a.href = config.url + '/tags/' + hashtag;
 | 
			
		||||
		a.textContent = '#' + hashtag;
 | 
			
		||||
		a.setAttribute('rel', 'tag');
 | 
			
		||||
		document.body.appendChild(a);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	'inline-code'({ document }, { code }) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user