Compare commits
	
		
			76 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					278624f2c8 | ||
| 
						 | 
					899f42c070 | ||
| 
						 | 
					8ce1d4d6a3 | ||
| 
						 | 
					52225d703b | ||
| 
						 | 
					81739af7cb | ||
| 
						 | 
					25473222cc | ||
| 
						 | 
					0b7be70935 | ||
| 
						 | 
					818b71abd6 | ||
| 
						 | 
					25575e8510 | ||
| 
						 | 
					6c85adcf23 | ||
| 
						 | 
					5dc92d7a40 | ||
| 
						 | 
					4e2b966b80 | ||
| 
						 | 
					d34f8c3cb9 | ||
| 
						 | 
					9049ecb1cf | ||
| 
						 | 
					7bebea087c | ||
| 
						 | 
					1c79e30436 | ||
| 
						 | 
					1d7933349b | ||
| 
						 | 
					d002f67140 | ||
| 
						 | 
					da3447765b | ||
| 
						 | 
					cbf5663179 | ||
| 
						 | 
					b217fba235 | ||
| 
						 | 
					7f7e6d5aba | ||
| 
						 | 
					87c5a9d9a6 | ||
| 
						 | 
					8ca1fe3f0a | ||
| 
						 | 
					763ae8f1a6 | ||
| 
						 | 
					c65256d02b | ||
| 
						 | 
					bd2ac515d1 | ||
| 
						 | 
					681f372889 | ||
| 
						 | 
					c2eec272e6 | ||
| 
						 | 
					bd720491a9 | ||
| 
						 | 
					a408226509 | ||
| 
						 | 
					c015e99e6e | ||
| 
						 | 
					de47a17be7 | ||
| 
						 | 
					d38fc490ad | ||
| 
						 | 
					662167e792 | ||
| 
						 | 
					36c91f03d9 | ||
| 
						 | 
					33ccee26b5 | ||
| 
						 | 
					ed5cb991e3 | ||
| 
						 | 
					bea84ec2bf | ||
| 
						 | 
					08c176e549 | ||
| 
						 | 
					810ed50976 | ||
| 
						 | 
					2684541693 | ||
| 
						 | 
					a5b12bac54 | ||
| 
						 | 
					fea1b06e43 | ||
| 
						 | 
					182ca5d434 | ||
| 
						 | 
					facde9a75d | ||
| 
						 | 
					41385640b9 | ||
| 
						 | 
					7bad9db32e | ||
| 
						 | 
					af66f0a497 | ||
| 
						 | 
					95e1b80f41 | ||
| 
						 | 
					556e2eba95 | ||
| 
						 | 
					efe530cb17 | ||
| 
						 | 
					34e7c99283 | ||
| 
						 | 
					4157ea8bc3 | ||
| 
						 | 
					550517bbf3 | ||
| 
						 | 
					eb910cd8a1 | ||
| 
						 | 
					75131c4e8a | ||
| 
						 | 
					ee29ab95be | ||
| 
						 | 
					e97951fc51 | ||
| 
						 | 
					dfabdef60f | ||
| 
						 | 
					5a87763193 | ||
| 
						 | 
					6bb90f56fa | ||
| 
						 | 
					c883ae1350 | ||
| 
						 | 
					09e25e6a02 | ||
| 
						 | 
					bf5d43054b | ||
| 
						 | 
					63b3c65691 | ||
| 
						 | 
					f193da7f67 | ||
| 
						 | 
					40f38c2c0a | ||
| 
						 | 
					db439ef804 | ||
| 
						 | 
					56eb896a03 | ||
| 
						 | 
					68d43e43b6 | ||
| 
						 | 
					c60517e49a | ||
| 
						 | 
					3f59d261f2 | ||
| 
						 | 
					4068d220e5 | ||
| 
						 | 
					18968e7208 | ||
| 
						 | 
					38656103c0 | 
							
								
								
									
										58
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -5,6 +5,64 @@ If you encounter any problems with updating, please try the following:
 | 
			
		||||
1. `npm run clean` or `npm run cleanall`
 | 
			
		||||
2. Retry update (Don't forget `npm i`)
 | 
			
		||||
 | 
			
		||||
10.98.3
 | 
			
		||||
----------
 | 
			
		||||
* リアクションのカスタム絵文字の情報がNoteに添付されない問題を修正
 | 
			
		||||
* フォルダーの移動をするとき親フォルダーに自分自身を指定できてしまう問題を修正
 | 
			
		||||
* デザインの調整
 | 
			
		||||
 | 
			
		||||
10.98.2
 | 
			
		||||
----------
 | 
			
		||||
* 他のインスタンスから添付画像が見れない問題を修正
 | 
			
		||||
 | 
			
		||||
10.98.1
 | 
			
		||||
----------
 | 
			
		||||
* ドライブのファイルのサムネイルが表示されない問題を修正
 | 
			
		||||
* APでカスタム絵文字を送る時に常にimage/pngで送っている問題を修正
 | 
			
		||||
* いくらいじってもページリロードするとmisskeyのテーマがdark(future)になっちゃう問題を修正
 | 
			
		||||
 | 
			
		||||
10.98.0
 | 
			
		||||
----------
 | 
			
		||||
* ドライブのファイルダウンロード時に元のファイル名を尊重するように
 | 
			
		||||
* ドライブで画像以外のファイルを分かりやすく表示するように
 | 
			
		||||
* TwemojiのCDNを変更
 | 
			
		||||
* モバイルで通知の設定がない問題を修正
 | 
			
		||||
* デザインの調整
 | 
			
		||||
 | 
			
		||||
10.97.2
 | 
			
		||||
----------
 | 
			
		||||
* ビルド時に警告が出ないように修正
 | 
			
		||||
 | 
			
		||||
10.97.1
 | 
			
		||||
----------
 | 
			
		||||
* デザインの調整
 | 
			
		||||
 | 
			
		||||
10.97.0
 | 
			
		||||
----------
 | 
			
		||||
* リアクションに絵文字やカスタム絵文字を使えるように
 | 
			
		||||
* 不明なリアクションのフォールバックに star を使えるように
 | 
			
		||||
* デザインの調整
 | 
			
		||||
 | 
			
		||||
10.96.0
 | 
			
		||||
----------
 | 
			
		||||
* 連合ユーザーの投稿に対してActivityPubオブジェクトを要求されたら元のインスタンスにリダイレクトするように
 | 
			
		||||
* updatePersonを試行した時点でもlastFetchedAtを更新するように
 | 
			
		||||
* 管理画面でリモートインスタンスの登録日時を表示
 | 
			
		||||
* ユーザーサジェストが機能しなくなっていた問題を修正
 | 
			
		||||
* 最近使ったハッシュタグ表示が機能していない問題を修正
 | 
			
		||||
* バグ修正
 | 
			
		||||
* デザインの調整
 | 
			
		||||
 | 
			
		||||
10.95.0
 | 
			
		||||
----------
 | 
			
		||||
* ジョブを一覧できるように
 | 
			
		||||
* MFMでURLを明示する構文の追加
 | 
			
		||||
* Articleタイプのアクティビティを受け入れるように
 | 
			
		||||
* 凍結されたユーザーをサジェストしないように
 | 
			
		||||
* ファビコンが保存されないのを修正
 | 
			
		||||
* キューのジョブクリアの動作を修正
 | 
			
		||||
* デザインの調整
 | 
			
		||||
 | 
			
		||||
10.94.0
 | 
			
		||||
----------
 | 
			
		||||
* Faviconを設定できるように
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,9 @@ Convert な(na) to にゃ(nya)
 | 
			
		||||
Revert Nyaize
 | 
			
		||||
 | 
			
		||||
## Code style
 | 
			
		||||
### Use semicolon
 | 
			
		||||
To avoid ASI Hazard
 | 
			
		||||
 | 
			
		||||
### Don't use `export default`
 | 
			
		||||
Bad:
 | 
			
		||||
``` ts
 | 
			
		||||
 
 | 
			
		||||
@@ -101,6 +101,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
<!-- PATREON_START -->
 | 
			
		||||
<table><tr>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5888816/36da0f7c15954df0ab13f9abdf227f66/1?token-time=2145916800&token-hash=HGkZJ7s4bSaQVoOJ5q30mTWHTxDLiw1LuyaogKPLy24%3D" alt="Hiroshi Seki" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/1?token-time=2145916800&token-hash=WeuDzzz24cRXJogyIkU-mxARqkdyms-rcZKbO-GpGjw%3D" alt="weep" width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/12059069" alt="naga_rus" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/4?token-time=2145916800&token-hash=vZdDTTF-ahiKBjjgppS2ev4rkD8H7TTKkXXoxsucs6Y%3D" alt="Melilot" width="100"></td>
 | 
			
		||||
@@ -108,6 +109,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/3?token-time=2145916800&token-hash=LtV2lRi3L2jOWMLwccr9qWYfPrFlzIo2jYZHKzHEb6k%3D" alt="Xeltica" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=1FlxS9MEgmNGH_RHUVHbO5hIXB5I1z0lvA33CTvYvjA%3D" alt="gutfuckllc" width="100"></td>
 | 
			
		||||
</tr><tr>
 | 
			
		||||
<td><a href="https://www.patreon.com/rane_hs">Hiroshi Seki</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/weepjp">weep</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=12059069">naga_rus</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
 | 
			
		||||
@@ -154,7 +156,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
 | 
			
		||||
</tr></table>
 | 
			
		||||
 | 
			
		||||
**Last updated:** Tue, 12 Mar 2019 00:50:06 UTC
 | 
			
		||||
**Last updated:** Fri, 22 Mar 2019 09:29:06 UTC
 | 
			
		||||
<!-- PATREON_END -->
 | 
			
		||||
 | 
			
		||||
:four_leaf_clover: Copyright
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,12 @@
 | 
			
		||||
meta:
 | 
			
		||||
  lang: "Čeština"
 | 
			
		||||
common:
 | 
			
		||||
  misskey: "⭐ ve fediverse"
 | 
			
		||||
  about-title: "⭐ ve fediverse."
 | 
			
		||||
  about: "Děkujeme, že jste našli Misskey. Misskey je <b>decentralizovaná mikroblogovací platforma</b> zrozená na Zemi. Neboť existuje ve Fediverse (vesmíru, kde jsou organizovány různé sociální sítě), je vzájemně propojena s jinými sociálními sítěmi. Co takhle si chvilku odpočinout od ruchu a shonu města a ponořit se do nového internetu?"
 | 
			
		||||
  misskey: "⭐ ve fedivesmíru"
 | 
			
		||||
  about-title: "⭐ ve fedivesmíru."
 | 
			
		||||
  about: "Děkujeme, že jste našli Misskey. Misskey je <b>decentralizovaná mikroblogovací platforma</b> zrozená na Zemi. Neboť existuje ve fedivesmíru (vesmíru, kde jsou organizovány různé sociální sítě), je vzájemně propojena s jinými sociálními sítěmi. Co takhle si chvilku odpočinout od ruchu a shonu města a ponořit se do nového internetu?"
 | 
			
		||||
  intro:
 | 
			
		||||
    title: "Co je Misskey?"
 | 
			
		||||
    about: "Misskey je open-source <b>decentralizovaný mikroblogovací software</b>. Má sofistikované, zcela přizpůsobitelné uživatelské rozhraní, různé způsoby reagování na příspěvky, bezplatné úložiště souborů nabízející integrovaný management system, a další pokročilé vlastnosti. Misskey je navíc připojeno k systému sítí zvanému „fediverse“, který nám dovoluje komunikovat s uživateli na jiných sociálních sítí. Pokud například něco napíšete, nebude to posláno pouze uživatelů Misskey, ale také lidem na sítích Mastodon a Pleroma. Jen si představte, že planeta posílá jiné planetě rádiový signál, aby s ní komunikovala."
 | 
			
		||||
    about: "Misskey je open-source <b>decentralizovaný mikroblogovací software</b>. Má sofistikované, zcela přizpůsobitelné uživatelské rozhraní, různé způsoby reagování na příspěvky, bezplatné úložiště souborů nabízející integrovaný management system, a další pokročilé vlastnosti. Misskey je navíc připojeno k systému sítí zvanému „fedivesmír“ nebo „fediverse“, který nám dovoluje komunikovat s uživateli na jiných sociálních sítí. Pokud například něco napíšete, nebude to posláno pouze uživatelů Misskey, ale také lidem na sítích Mastodon a Pleroma. Jen si představte, že planeta posílá jiné planetě rádiový signál, aby s ní komunikovala."
 | 
			
		||||
    features: "Vlastnosti"
 | 
			
		||||
    rich-contents: "Příspěvky"
 | 
			
		||||
    rich-contents-desc: "Pouze napište svoje nápady, žhavá témata a cokoliv, co chcete sdílet. Můžete ozdobit svá slova, připojit vaše oblíbené obrázky, posílat soubory včetně videí či vytvořit hlasování – to je jen několik věcí, co můžete dělat s Misskey!"
 | 
			
		||||
@@ -300,10 +300,11 @@ common/views/pages/explore.vue:
 | 
			
		||||
  recently-updated-users: "Nedávno aktívni uživatelé"
 | 
			
		||||
  recently-registered-users: "Nedávno registrovaní uživatelé"
 | 
			
		||||
  popular-tags: "Populární tagy"
 | 
			
		||||
  federated: "Z fediverse"
 | 
			
		||||
  federated: "Z fedivesmíru"
 | 
			
		||||
  explore: "Prozkoumat {host}"
 | 
			
		||||
common/views/components/url-preview.vue:
 | 
			
		||||
  enable-player: "Otevřít v přehrávači"
 | 
			
		||||
  disable-player: "Zavřít přehrávač"
 | 
			
		||||
common/views/components/user-list.vue:
 | 
			
		||||
  no-users: "Žádní uživatelé"
 | 
			
		||||
common/views/components/games/reversi/reversi.vue:
 | 
			
		||||
@@ -835,7 +836,7 @@ admin/views/index.vue:
 | 
			
		||||
  emoji: "Emoji"
 | 
			
		||||
  moderators: "Moderátoři"
 | 
			
		||||
  users: "Uživatelé"
 | 
			
		||||
  federation: "Z fediversu"
 | 
			
		||||
  federation: "Federovaná"
 | 
			
		||||
  announcements: "Oznámení"
 | 
			
		||||
  hashtags: "Hashtagy"
 | 
			
		||||
  queue: "Fronta úloh"
 | 
			
		||||
@@ -847,9 +848,7 @@ admin/views/dashboard.vue:
 | 
			
		||||
  drive: "Disk"
 | 
			
		||||
  instances: "Instance"
 | 
			
		||||
  this-instance: "Tato instance"
 | 
			
		||||
  federated: "Z fediversu"
 | 
			
		||||
admin/views/queue.vue:
 | 
			
		||||
  operation: "Akce"
 | 
			
		||||
  federated: "Federovaná"
 | 
			
		||||
admin/views/abuse.vue:
 | 
			
		||||
  details: "Popis"
 | 
			
		||||
  remove-report: "Odstranit"
 | 
			
		||||
@@ -996,14 +995,15 @@ admin/views/announcements.vue:
 | 
			
		||||
admin/views/hashtags.vue:
 | 
			
		||||
  hided-tags: "Skryté tagy"
 | 
			
		||||
admin/views/federation.vue:
 | 
			
		||||
  federation: "Z fediversu"
 | 
			
		||||
  instance: "Instance"
 | 
			
		||||
  host: "Hostitel"
 | 
			
		||||
  notes: "Poznámky"
 | 
			
		||||
  users: "Uživatelé"
 | 
			
		||||
  caught-at: "Vytvořeno"
 | 
			
		||||
  status: "Status"
 | 
			
		||||
  latest-request-received-at: "Poslední požadavek přijat"
 | 
			
		||||
  block: "Blokován"
 | 
			
		||||
  instances: "Instance"
 | 
			
		||||
  instances: "Federovaná"
 | 
			
		||||
  states:
 | 
			
		||||
    all: "Všechny"
 | 
			
		||||
    blocked: "Blokován"
 | 
			
		||||
 
 | 
			
		||||
@@ -169,9 +169,9 @@ common:
 | 
			
		||||
    deck-column-align-flexible: "Flexible"
 | 
			
		||||
    deck-column-width: "Deck column width"
 | 
			
		||||
    deck-column-width-narrow: "Narrow"
 | 
			
		||||
    deck-column-width-narrower: "Somewhat narrow"
 | 
			
		||||
    deck-column-width-narrower: "Narrower"
 | 
			
		||||
    deck-column-width-normal: "Regular"
 | 
			
		||||
    deck-column-width-wider: "Somewhat wide"
 | 
			
		||||
    deck-column-width-wider: "Slightly wide"
 | 
			
		||||
    deck-column-width-wide: "Wide"
 | 
			
		||||
    use-shadow: "Use shadows in the UI"
 | 
			
		||||
    rounded-corners: "Round the corners of the UI"
 | 
			
		||||
@@ -1057,7 +1057,7 @@ admin/views/dashboard.vue:
 | 
			
		||||
  this-instance: "This instance"
 | 
			
		||||
  federated: "Federated"
 | 
			
		||||
admin/views/queue.vue:
 | 
			
		||||
  operation: "Action(s)"
 | 
			
		||||
  title: "Queue"
 | 
			
		||||
  remove-all-jobs: "Clear all queued jobs"
 | 
			
		||||
admin/views/abuse.vue:
 | 
			
		||||
  title: "Abuse"
 | 
			
		||||
@@ -1113,6 +1113,7 @@ admin/views/instance.vue:
 | 
			
		||||
  disable-local-timeline: "Disable the Local Timeline"
 | 
			
		||||
  disable-global-timeline: "Disable global timeline"
 | 
			
		||||
  disabling-timelines-info: "Even if you disable these timelines, the administrator as well as moderators can use them continually."
 | 
			
		||||
  enable-emoji-reaction: "Enable pictograms for reactions"
 | 
			
		||||
  invite: "Invite"
 | 
			
		||||
  save: "Save"
 | 
			
		||||
  saved: "Saved"
 | 
			
		||||
@@ -1275,12 +1276,13 @@ admin/views/announcements.vue:
 | 
			
		||||
admin/views/hashtags.vue:
 | 
			
		||||
  hided-tags: "Hidden Tags"
 | 
			
		||||
admin/views/federation.vue:
 | 
			
		||||
  federation: "Federation"
 | 
			
		||||
  instance: "Instance"
 | 
			
		||||
  host: "Host"
 | 
			
		||||
  notes: "Notes"
 | 
			
		||||
  users: "Users"
 | 
			
		||||
  following: "Following"
 | 
			
		||||
  followers: "Followers"
 | 
			
		||||
  caught-at: "Created at"
 | 
			
		||||
  status: "Statuses"
 | 
			
		||||
  latest-request-sent-at: "Time of last request sent"
 | 
			
		||||
  latest-request-received-at: "Last request received at"
 | 
			
		||||
@@ -1289,7 +1291,7 @@ admin/views/federation.vue:
 | 
			
		||||
  block: "Block"
 | 
			
		||||
  marked-as-closed: "Marked as closed"
 | 
			
		||||
  lookup: "Look up"
 | 
			
		||||
  instances: "Instances"
 | 
			
		||||
  instances: "Federated"
 | 
			
		||||
  instance-not-registered: "The instance has not been discovered"
 | 
			
		||||
  sort: "Sort by"
 | 
			
		||||
  sorts:
 | 
			
		||||
 
 | 
			
		||||
@@ -815,11 +815,11 @@ admin/views/announcements.vue:
 | 
			
		||||
  remove: "eliminar"
 | 
			
		||||
  add: "Agregar"
 | 
			
		||||
admin/views/federation.vue:
 | 
			
		||||
  instance: "Instancia"
 | 
			
		||||
  host: "Host"
 | 
			
		||||
  following: "Siguiendo"
 | 
			
		||||
  status: "Estado"
 | 
			
		||||
  block: "Bloquear"
 | 
			
		||||
  instances: "Instancia"
 | 
			
		||||
  states:
 | 
			
		||||
    all: "Todo"
 | 
			
		||||
    blocked: "Bloquear"
 | 
			
		||||
 
 | 
			
		||||
@@ -927,7 +927,6 @@ admin/views/dashboard.vue:
 | 
			
		||||
  this-instance: "Cette instance"
 | 
			
		||||
  federated: "Fédérées"
 | 
			
		||||
admin/views/queue.vue:
 | 
			
		||||
  operation: "Action(s)"
 | 
			
		||||
  remove-all-jobs: "Enlever toutes les tâches en attente"
 | 
			
		||||
admin/views/abuse.vue:
 | 
			
		||||
  title: "Abus"
 | 
			
		||||
@@ -1143,12 +1142,13 @@ admin/views/announcements.vue:
 | 
			
		||||
admin/views/hashtags.vue:
 | 
			
		||||
  hided-tags: "Tags cachés"
 | 
			
		||||
admin/views/federation.vue:
 | 
			
		||||
  federation: "Fédération"
 | 
			
		||||
  instance: "Instance"
 | 
			
		||||
  host: "Hôte"
 | 
			
		||||
  notes: "Notes"
 | 
			
		||||
  users: "Utilisateur·rice·s"
 | 
			
		||||
  following: "Abonnements"
 | 
			
		||||
  followers: "Abonné·e·s"
 | 
			
		||||
  caught-at: "Créé le"
 | 
			
		||||
  status: "Statuts"
 | 
			
		||||
  latest-request-sent-at: "Dernière requête envoyée"
 | 
			
		||||
  latest-request-received-at: "Dernière requête reçue"
 | 
			
		||||
@@ -1156,7 +1156,7 @@ admin/views/federation.vue:
 | 
			
		||||
  block: "Bloquer"
 | 
			
		||||
  marked-as-closed: "Marquées comme fermées"
 | 
			
		||||
  lookup: "Recherche"
 | 
			
		||||
  instances: "Instances"
 | 
			
		||||
  instances: "Fédérées"
 | 
			
		||||
  sort: "Trier par"
 | 
			
		||||
  sorts:
 | 
			
		||||
    caughtAtAsc: "Date d’inscription (Ascendant)"
 | 
			
		||||
 
 | 
			
		||||
@@ -925,12 +925,6 @@ desktop/views/input-dialog.vue:
 | 
			
		||||
  cancel: "キャンセル"
 | 
			
		||||
  ok: "決定"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/messaging-room-window.vue:
 | 
			
		||||
  title: "メッセージ:"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/messaging-window.vue:
 | 
			
		||||
  title: "メッセージ"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/note-detail.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
@@ -1100,7 +1094,7 @@ desktop/views/components/timeline.vue:
 | 
			
		||||
  hybrid: "ソーシャル"
 | 
			
		||||
  global: "グローバル"
 | 
			
		||||
  mentions: "あなた宛て"
 | 
			
		||||
  messages: "メッセージ"
 | 
			
		||||
  messages: "ダイレクト投稿"
 | 
			
		||||
  list: "リスト"
 | 
			
		||||
  hashtag: "ハッシュタグ"
 | 
			
		||||
  add-tag-timeline: "ハッシュタグを追加"
 | 
			
		||||
@@ -1238,6 +1232,8 @@ admin/views/instance.vue:
 | 
			
		||||
  disable-local-timeline: "ローカルタイムラインを無効にする"
 | 
			
		||||
  disable-global-timeline: "グローバルタイムラインを無効にする"
 | 
			
		||||
  disabling-timelines-info: "これらのタイムラインを無効にしても、管理者およびモデレーターは引き続き利用できます。"
 | 
			
		||||
  enable-emoji-reaction: "リアクションに絵文字を使えるようにする"
 | 
			
		||||
  use-star-for-reaction-fallback: "不明なリアクションのフォールバックに star を使う"
 | 
			
		||||
  invite: "招待"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
  saved: "保存しました"
 | 
			
		||||
@@ -1408,12 +1404,13 @@ admin/views/hashtags.vue:
 | 
			
		||||
  hided-tags: "Hidden Tags"
 | 
			
		||||
 | 
			
		||||
admin/views/federation.vue:
 | 
			
		||||
  federation: "連合"
 | 
			
		||||
  instance: "インスタンス"
 | 
			
		||||
  host: "ホスト"
 | 
			
		||||
  notes: "投稿"
 | 
			
		||||
  users: "ユーザー"
 | 
			
		||||
  following: "フォロー中"
 | 
			
		||||
  followers: "フォロワー"
 | 
			
		||||
  caught-at: "登録日時"
 | 
			
		||||
  status: "ステータス"
 | 
			
		||||
  latest-request-sent-at: "直近のリクエスト送信"
 | 
			
		||||
  latest-request-received-at: "直近のリクエスト受信"
 | 
			
		||||
@@ -1422,7 +1419,7 @@ admin/views/federation.vue:
 | 
			
		||||
  block: "ブロック"
 | 
			
		||||
  marked-as-closed: "閉鎖されているとマーク"
 | 
			
		||||
  lookup: "照会"
 | 
			
		||||
  instances: "インスタンス"
 | 
			
		||||
  instances: "連合"
 | 
			
		||||
  instance-not-registered: "そのインスタンスは登録されていません"
 | 
			
		||||
  sort: "ソート"
 | 
			
		||||
  sorts:
 | 
			
		||||
@@ -1532,9 +1529,6 @@ desktop/views/pages/user/user.timeline.vue:
 | 
			
		||||
  with-media: "メディア"
 | 
			
		||||
  my-posts: "私の投稿"
 | 
			
		||||
 | 
			
		||||
desktop/views/widgets/messaging.vue:
 | 
			
		||||
  title: "メッセージ"
 | 
			
		||||
 | 
			
		||||
desktop/views/widgets/notifications.vue:
 | 
			
		||||
  title: "通知"
 | 
			
		||||
 | 
			
		||||
@@ -1682,7 +1676,7 @@ mobile/views/pages/home.vue:
 | 
			
		||||
  hybrid: "ソーシャル"
 | 
			
		||||
  global: "グローバル"
 | 
			
		||||
  mentions: "あなた宛て"
 | 
			
		||||
  messages: "メッセージ"
 | 
			
		||||
  messages: "ダイレクト投稿"
 | 
			
		||||
 | 
			
		||||
mobile/views/pages/tag.vue:
 | 
			
		||||
  no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"
 | 
			
		||||
 
 | 
			
		||||
@@ -860,8 +860,6 @@ admin/views/dashboard.vue:
 | 
			
		||||
  instances: "インスタンス"
 | 
			
		||||
  this-instance: "ワイのインスタンス"
 | 
			
		||||
  federated: "連合"
 | 
			
		||||
admin/views/queue.vue:
 | 
			
		||||
  operation: "操作"
 | 
			
		||||
admin/views/abuse.vue:
 | 
			
		||||
  details: "もっと"
 | 
			
		||||
  remove-report: "削除"
 | 
			
		||||
@@ -990,7 +988,7 @@ admin/views/announcements.vue:
 | 
			
		||||
  add: "増やす"
 | 
			
		||||
  saved: "保存したで!"
 | 
			
		||||
admin/views/federation.vue:
 | 
			
		||||
  federation: "連合"
 | 
			
		||||
  instance: "インスタンス"
 | 
			
		||||
  host: "ホスト"
 | 
			
		||||
  notes: "投稿"
 | 
			
		||||
  users: "ユーザー"
 | 
			
		||||
@@ -999,7 +997,7 @@ admin/views/federation.vue:
 | 
			
		||||
  status: "ステータス"
 | 
			
		||||
  block: "ブロック"
 | 
			
		||||
  lookup: "照会"
 | 
			
		||||
  instances: "インスタンス"
 | 
			
		||||
  instances: "連合"
 | 
			
		||||
  states:
 | 
			
		||||
    all: "すべて"
 | 
			
		||||
    blocked: "ブロック"
 | 
			
		||||
 
 | 
			
		||||
@@ -314,6 +314,7 @@ common/views/pages/explore.vue:
 | 
			
		||||
  users-info: "현재 {users} 사용자가 등록되어 있습니다"
 | 
			
		||||
common/views/components/url-preview.vue:
 | 
			
		||||
  enable-player: "플레이어 열기"
 | 
			
		||||
  disable-player: "플레이어 닫기"
 | 
			
		||||
common/views/components/user-list.vue:
 | 
			
		||||
  no-users: "사용자가 없습니다"
 | 
			
		||||
common/views/components/games/reversi/reversi.vue:
 | 
			
		||||
@@ -1056,7 +1057,7 @@ admin/views/dashboard.vue:
 | 
			
		||||
  this-instance: "이 인스턴스"
 | 
			
		||||
  federated: "연합"
 | 
			
		||||
admin/views/queue.vue:
 | 
			
		||||
  operation: "동작"
 | 
			
		||||
  title: "큐"
 | 
			
		||||
  remove-all-jobs: "모든 작업 제거"
 | 
			
		||||
admin/views/abuse.vue:
 | 
			
		||||
  title: "스팸 신고"
 | 
			
		||||
@@ -1274,12 +1275,13 @@ admin/views/announcements.vue:
 | 
			
		||||
admin/views/hashtags.vue:
 | 
			
		||||
  hided-tags: "Hidden Tags"
 | 
			
		||||
admin/views/federation.vue:
 | 
			
		||||
  federation: "연합"
 | 
			
		||||
  instance: "인스턴스"
 | 
			
		||||
  host: "호스트"
 | 
			
		||||
  notes: "글"
 | 
			
		||||
  users: "사용자"
 | 
			
		||||
  following: "팔로우 중"
 | 
			
		||||
  followers: "팔로워"
 | 
			
		||||
  caught-at: "등록 날짜"
 | 
			
		||||
  status: "상태"
 | 
			
		||||
  latest-request-sent-at: "마지막으로 요청을 전송한 시간"
 | 
			
		||||
  latest-request-received-at: "마지막으로 요청을 받은 시간"
 | 
			
		||||
@@ -1288,7 +1290,7 @@ admin/views/federation.vue:
 | 
			
		||||
  block: "차단"
 | 
			
		||||
  marked-as-closed: "폐쇄된 것으로 표시"
 | 
			
		||||
  lookup: "조회"
 | 
			
		||||
  instances: "인스턴스"
 | 
			
		||||
  instances: "연합"
 | 
			
		||||
  instance-not-registered: "해당 인스턴스가 등록되어 있지 않습니다"
 | 
			
		||||
  sort: "정렬"
 | 
			
		||||
  sorts:
 | 
			
		||||
 
 | 
			
		||||
@@ -121,12 +121,17 @@ common:
 | 
			
		||||
    other: "Inne"
 | 
			
		||||
    appearance: "Wygląd"
 | 
			
		||||
    behavior: "Zachowanie"
 | 
			
		||||
    fetch-on-scroll: "Automatycznie ładuj po przeciągnięciu w dół"
 | 
			
		||||
    note-visibility: "Widoczność wpisów"
 | 
			
		||||
    web-search-engine: "Wyszukiwarka internetowa"
 | 
			
		||||
    line-width: "Szerokości linii"
 | 
			
		||||
    line-width-thin: "Cienka"
 | 
			
		||||
    line-width-normal: "Normalna"
 | 
			
		||||
    line-width-thick: "Gruba"
 | 
			
		||||
    font-size: "Rozmiar tekstu"
 | 
			
		||||
    font-size-x-small: "Małe"
 | 
			
		||||
    font-size-medium: "Normalna"
 | 
			
		||||
    font-size-large: "Trochę duży"
 | 
			
		||||
    font-size-x-large: "Duży"
 | 
			
		||||
    deck-column-align-center: "Po środku"
 | 
			
		||||
    deck-column-align-left: "Z lewej"
 | 
			
		||||
@@ -137,7 +142,13 @@ common:
 | 
			
		||||
    deck-column-width-normal: "Normalna"
 | 
			
		||||
    deck-column-width-wider: "Trochę szerokie"
 | 
			
		||||
    deck-column-width-wide: "Szeroka"
 | 
			
		||||
    wallpaper: "Tapeta"
 | 
			
		||||
    choose-wallpaper: "Wybierz tapetę"
 | 
			
		||||
    timeline: "Oś czasu"
 | 
			
		||||
    sound: "Dźwięk"
 | 
			
		||||
    test: "Test"
 | 
			
		||||
    update: "Aktualizacja Misskey"
 | 
			
		||||
    version: "Wersja:"
 | 
			
		||||
    navbar-position-left: "Z lewej"
 | 
			
		||||
  search: "Szukaj"
 | 
			
		||||
  delete: "Usuń"
 | 
			
		||||
@@ -936,13 +947,14 @@ admin/views/announcements.vue:
 | 
			
		||||
    are-you-sure: "Usunąć \"$1\"?"
 | 
			
		||||
    removed: "Usunięto"
 | 
			
		||||
admin/views/federation.vue:
 | 
			
		||||
  instance: "Instancja"
 | 
			
		||||
  notes: "Wpis"
 | 
			
		||||
  users: "Użytkownicy"
 | 
			
		||||
  following: "Śledzisz"
 | 
			
		||||
  followers: "Śledzący"
 | 
			
		||||
  caught-at: "Utworzono"
 | 
			
		||||
  status: "Stan"
 | 
			
		||||
  block: "Zablokuj"
 | 
			
		||||
  instances: "Instancja"
 | 
			
		||||
  sort: "Sortuj"
 | 
			
		||||
  states:
 | 
			
		||||
    all: "Wszyscy"
 | 
			
		||||
 
 | 
			
		||||
@@ -314,6 +314,7 @@ common/views/pages/explore.vue:
 | 
			
		||||
  users-info: "当前有{users}个注册用户"
 | 
			
		||||
common/views/components/url-preview.vue:
 | 
			
		||||
  enable-player: "打开播放器"
 | 
			
		||||
  disable-player: "关闭播放器"
 | 
			
		||||
common/views/components/user-list.vue:
 | 
			
		||||
  no-users: "无用户"
 | 
			
		||||
common/views/components/games/reversi/reversi.vue:
 | 
			
		||||
@@ -1056,7 +1057,7 @@ admin/views/dashboard.vue:
 | 
			
		||||
  this-instance: "此实例"
 | 
			
		||||
  federated: "联合"
 | 
			
		||||
admin/views/queue.vue:
 | 
			
		||||
  operation: "操作"
 | 
			
		||||
  title: "队列"
 | 
			
		||||
  remove-all-jobs: "清除所有作业"
 | 
			
		||||
admin/views/abuse.vue:
 | 
			
		||||
  title: "举报垃圾信息"
 | 
			
		||||
@@ -1274,12 +1275,13 @@ admin/views/announcements.vue:
 | 
			
		||||
admin/views/hashtags.vue:
 | 
			
		||||
  hided-tags: "隐藏标签"
 | 
			
		||||
admin/views/federation.vue:
 | 
			
		||||
  federation: "联合"
 | 
			
		||||
  instance: "例"
 | 
			
		||||
  host: "主机名"
 | 
			
		||||
  notes: "帖子"
 | 
			
		||||
  users: "用户"
 | 
			
		||||
  following: "正在关注"
 | 
			
		||||
  followers: "关注者"
 | 
			
		||||
  caught-at: "注册日期"
 | 
			
		||||
  status: "状态"
 | 
			
		||||
  latest-request-sent-at: "上次发送的请求"
 | 
			
		||||
  latest-request-received-at: "上次收到的请求"
 | 
			
		||||
@@ -1288,7 +1290,7 @@ admin/views/federation.vue:
 | 
			
		||||
  block: "拉黑"
 | 
			
		||||
  marked-as-closed: "标记为已关闭"
 | 
			
		||||
  lookup: "查询"
 | 
			
		||||
  instances: "实例"
 | 
			
		||||
  instances: "联合"
 | 
			
		||||
  instance-not-registered: "实例未注册"
 | 
			
		||||
  sort: "排序"
 | 
			
		||||
  sorts:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"author": "syuilo <i@syuilo.com>",
 | 
			
		||||
	"version": "10.94.0",
 | 
			
		||||
	"version": "10.98.3",
 | 
			
		||||
	"codename": "nighthike",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
@@ -77,6 +77,7 @@
 | 
			
		||||
		"@types/qrcode": "1.3.0",
 | 
			
		||||
		"@types/ratelimiter": "2.1.28",
 | 
			
		||||
		"@types/redis": "2.8.10",
 | 
			
		||||
		"@types/rename": "1.0.1",
 | 
			
		||||
		"@types/request": "2.48.1",
 | 
			
		||||
		"@types/request-promise-native": "1.0.15",
 | 
			
		||||
		"@types/request-stats": "3.0.0",
 | 
			
		||||
@@ -107,6 +108,7 @@
 | 
			
		||||
		"chai-http": "4.2.1",
 | 
			
		||||
		"chalk": "2.4.2",
 | 
			
		||||
		"commander": "2.19.0",
 | 
			
		||||
		"content-disposition": "0.5.3",
 | 
			
		||||
		"crc-32": "1.2.0",
 | 
			
		||||
		"css-loader": "2.1.1",
 | 
			
		||||
		"cssnano": "4.1.10",
 | 
			
		||||
@@ -193,6 +195,7 @@
 | 
			
		||||
		"recaptcha-promise": "0.1.3",
 | 
			
		||||
		"reconnecting-websocket": "4.1.10",
 | 
			
		||||
		"redis": "2.8.0",
 | 
			
		||||
		"rename": "1.0.4",
 | 
			
		||||
		"request": "2.88.0",
 | 
			
		||||
		"request-promise-native": "1.0.7",
 | 
			
		||||
		"request-stats": "3.0.0",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								src/@types/deepcopy.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								src/@types/deepcopy.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,17 +1,19 @@
 | 
			
		||||
declare module 'deepcopy';
 | 
			
		||||
 | 
			
		||||
declare namespace deepcopy {
 | 
			
		||||
declare module 'deepcopy' {
 | 
			
		||||
	type DeepcopyCustomizerValueType = 'Object';
 | 
			
		||||
 | 
			
		||||
	type DeepcopyCustomizer<T> = (
 | 
			
		||||
		value: T,
 | 
			
		||||
		valueType: DeepcopyCustomizerValueType) => T;
 | 
			
		||||
 | 
			
		||||
	interface DeepcopyOptions<T> {
 | 
			
		||||
	interface IDeepcopyOptions<T> {
 | 
			
		||||
		customizer: DeepcopyCustomizer<T>;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export function deepcopy<T>(
 | 
			
		||||
	function deepcopy<T>(
 | 
			
		||||
		value: T,
 | 
			
		||||
		options?: DeepcopyOptions<T> | DeepcopyCustomizer<T>): T;
 | 
			
		||||
		options?: IDeepcopyOptions<T> | DeepcopyCustomizer<T>): T;
 | 
			
		||||
 | 
			
		||||
	namespace deepcopy {} // Hack
 | 
			
		||||
 | 
			
		||||
	export = deepcopy;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								src/@types/koa-slow.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/@types/koa-slow.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -8,7 +8,7 @@ declare module 'koa-slow' {
 | 
			
		||||
 | 
			
		||||
	function slow(options?: ISlowOptions): Middleware;
 | 
			
		||||
 | 
			
		||||
	namespace slow { } // Hack
 | 
			
		||||
	namespace slow {} // Hack
 | 
			
		||||
 | 
			
		||||
	export = slow;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -181,7 +181,12 @@ export default Vue.extend({
 | 
			
		||||
				},
 | 
			
		||||
				grid: {
 | 
			
		||||
					clipMarkers: false,
 | 
			
		||||
					borderColor: 'rgba(0, 0, 0, 0.1)'
 | 
			
		||||
					borderColor: 'rgba(0, 0, 0, 0.1)',
 | 
			
		||||
					xaxis: {
 | 
			
		||||
						lines: {
 | 
			
		||||
							show: true,
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				stroke: {
 | 
			
		||||
					curve: 'straight',
 | 
			
		||||
@@ -240,6 +245,7 @@ export default Vue.extend({
 | 
			
		||||
		federationInstancesChart(total: boolean): any {
 | 
			
		||||
			return {
 | 
			
		||||
				series: [{
 | 
			
		||||
					name: 'Instances',
 | 
			
		||||
					data: this.format(total
 | 
			
		||||
						? this.stats.federation.instance.total
 | 
			
		||||
						: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,43 +1,58 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<ui-card>
 | 
			
		||||
		<template #title><fa :icon="faTerminal"/> {{ $t('federation') }}</template>
 | 
			
		||||
		<template #title><fa :icon="faTerminal"/> {{ $t('instance') }}</template>
 | 
			
		||||
		<section class="fit-top">
 | 
			
		||||
			<ui-input class="target" v-model="target" type="text" @enter="showInstance()">
 | 
			
		||||
				<span>{{ $t('host') }}</span>
 | 
			
		||||
				<template #prefix><fa :icon="faServer"/></template>
 | 
			
		||||
			</ui-input>
 | 
			
		||||
			<ui-button @click="showInstance()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
 | 
			
		||||
 | 
			
		||||
			<div class="instance" v-if="instance">
 | 
			
		||||
				<ui-input :value="instance.host" type="text" readonly>
 | 
			
		||||
					<span>{{ $t('host') }}</span>
 | 
			
		||||
				</ui-input>
 | 
			
		||||
				<ui-horizon-group inputs>
 | 
			
		||||
					<ui-input :value="instance.host" type="text" readonly>
 | 
			
		||||
						<span>{{ $t('host') }}</span>
 | 
			
		||||
						<template #prefix><fa :icon="faServer"/></template>
 | 
			
		||||
					</ui-input>
 | 
			
		||||
					<ui-input :value="instance.caughtAt | date" type="text" readonly>
 | 
			
		||||
						<span>{{ $t('caught-at') }}</span>
 | 
			
		||||
						<template #prefix><fa :icon="faCrosshairs"/></template>
 | 
			
		||||
					</ui-input>
 | 
			
		||||
				</ui-horizon-group>
 | 
			
		||||
				<ui-horizon-group inputs>
 | 
			
		||||
					<ui-input :value="instance.notesCount | number" type="text" readonly>
 | 
			
		||||
						<span>{{ $t('notes') }}</span>
 | 
			
		||||
						<template #prefix><fa :icon="faEnvelopeOpenText"/></template>
 | 
			
		||||
					</ui-input>
 | 
			
		||||
					<ui-input :value="instance.usersCount | number" type="text" readonly>
 | 
			
		||||
						<span>{{ $t('users') }}</span>
 | 
			
		||||
						<template #prefix><fa :icon="faUsers"/></template>
 | 
			
		||||
					</ui-input>
 | 
			
		||||
				</ui-horizon-group>
 | 
			
		||||
				<ui-horizon-group inputs>
 | 
			
		||||
					<ui-input :value="instance.followingCount | number" type="text" readonly>
 | 
			
		||||
						<span>{{ $t('following') }}</span>
 | 
			
		||||
						<template #prefix><fa :icon="faCaretDown"/></template>
 | 
			
		||||
					</ui-input>
 | 
			
		||||
					<ui-input :value="instance.followersCount | number" type="text" readonly>
 | 
			
		||||
						<span>{{ $t('followers') }}</span>
 | 
			
		||||
						<template #prefix><fa :icon="faCaretUp"/></template>
 | 
			
		||||
					</ui-input>
 | 
			
		||||
				</ui-horizon-group>
 | 
			
		||||
				<ui-horizon-group inputs>
 | 
			
		||||
					<ui-input :value="instance.latestRequestSentAt" type="text" readonly>
 | 
			
		||||
					<ui-input :value="instance.latestRequestSentAt | date" type="text" readonly>
 | 
			
		||||
						<span>{{ $t('latest-request-sent-at') }}</span>
 | 
			
		||||
						<template #prefix><fa :icon="faPaperPlane"/></template>
 | 
			
		||||
					</ui-input>
 | 
			
		||||
					<ui-input :value="instance.latestStatus" type="text" readonly>
 | 
			
		||||
						<span>{{ $t('status') }}</span>
 | 
			
		||||
						<template #prefix><fa :icon="faTrafficLight"/></template>
 | 
			
		||||
					</ui-input>
 | 
			
		||||
				</ui-horizon-group>
 | 
			
		||||
				<ui-input :value="instance.latestRequestReceivedAt" type="text" readonly>
 | 
			
		||||
				<ui-input :value="instance.latestRequestReceivedAt | date" type="text" readonly>
 | 
			
		||||
					<span>{{ $t('latest-request-received-at') }}</span>
 | 
			
		||||
					<template #prefix><fa :icon="faInbox"/></template>
 | 
			
		||||
				</ui-input>
 | 
			
		||||
				<ui-switch v-model="instance.isBlocked" @change="updateInstance()">{{ $t('block') }}</ui-switch>
 | 
			
		||||
				<ui-switch v-model="instance.isMarkedAsClosed" @change="updateInstance()">{{ $t('marked-as-closed') }}</ui-switch>
 | 
			
		||||
@@ -133,7 +148,8 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../i18n';
 | 
			
		||||
import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import ApexCharts from 'apexcharts';
 | 
			
		||||
import * as tinycolor from 'tinycolor2';
 | 
			
		||||
 | 
			
		||||
@@ -144,19 +160,23 @@ const negate = arr => arr.map(x => -x);
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('admin/views/federation.vue'),
 | 
			
		||||
 | 
			
		||||
	filters: {
 | 
			
		||||
		date: v => v ? new Date(v).toLocaleString() : 'N/A'
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			instance: null,
 | 
			
		||||
			target: null,
 | 
			
		||||
			sort: '+lastCommunicatedAt',
 | 
			
		||||
			state: 'all',
 | 
			
		||||
			limit: 50,
 | 
			
		||||
			limit: 100,
 | 
			
		||||
			instances: [],
 | 
			
		||||
			chart: null,
 | 
			
		||||
			chartSrc: 'requests',
 | 
			
		||||
			chartSpan: 'hour',
 | 
			
		||||
			chartInstance: null,
 | 
			
		||||
			faGlobe, faTerminal, faSearch, faMinusCircle, faServer
 | 
			
		||||
			faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,8 @@
 | 
			
		||||
			<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
 | 
			
		||||
			<ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch>
 | 
			
		||||
			<ui-info>{{ $t('disabling-timelines-info') }}</ui-info>
 | 
			
		||||
			<ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch>
 | 
			
		||||
			<ui-switch v-model="useStarForReactionFallback">{{ $t('use-star-for-reaction-fallback') }}</ui-switch>
 | 
			
		||||
		</section>
 | 
			
		||||
		<section class="fit-bottom">
 | 
			
		||||
			<header><fa icon="cloud"/> {{ $t('drive-config') }}</header>
 | 
			
		||||
@@ -155,6 +157,8 @@ export default Vue.extend({
 | 
			
		||||
			disableRegistration: false,
 | 
			
		||||
			disableLocalTimeline: false,
 | 
			
		||||
			disableGlobalTimeline: false,
 | 
			
		||||
			enableEmojiReaction: true,
 | 
			
		||||
			useStarForReactionFallback: false,
 | 
			
		||||
			mascotImageUrl: null,
 | 
			
		||||
			bannerUrl: null,
 | 
			
		||||
			errorImageUrl: null,
 | 
			
		||||
@@ -206,6 +210,8 @@ export default Vue.extend({
 | 
			
		||||
			this.disableRegistration = meta.disableRegistration;
 | 
			
		||||
			this.disableLocalTimeline = meta.disableLocalTimeline;
 | 
			
		||||
			this.disableGlobalTimeline = meta.disableGlobalTimeline;
 | 
			
		||||
			this.enableEmojiReaction = meta.enableEmojiReaction;
 | 
			
		||||
			this.useStarForReactionFallback = meta.useStarForReactionFallback;
 | 
			
		||||
			this.mascotImageUrl = meta.mascotImageUrl;
 | 
			
		||||
			this.bannerUrl = meta.bannerUrl;
 | 
			
		||||
			this.errorImageUrl = meta.errorImageUrl;
 | 
			
		||||
@@ -267,10 +273,12 @@ export default Vue.extend({
 | 
			
		||||
				disableRegistration: this.disableRegistration,
 | 
			
		||||
				disableLocalTimeline: this.disableLocalTimeline,
 | 
			
		||||
				disableGlobalTimeline: this.disableGlobalTimeline,
 | 
			
		||||
				enableEmojiReaction: this.enableEmojiReaction,
 | 
			
		||||
				useStarForReactionFallback: this.useStarForReactionFallback,
 | 
			
		||||
				mascotImageUrl: this.mascotImageUrl,
 | 
			
		||||
				bannerUrl: this.bannerUrl,
 | 
			
		||||
				errorImageUrl: this.errorImageUrl,
 | 
			
		||||
				iconImageUrl: this.iconImageUrl,
 | 
			
		||||
				iconUrl: this.iconUrl,
 | 
			
		||||
				name: this.name,
 | 
			
		||||
				description: this.description,
 | 
			
		||||
				langs: this.languages.split(' '),
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
		<template #title><fa :icon="faStream"/> {{ $t('logs') }}</template>
 | 
			
		||||
		<section class="fit-top">
 | 
			
		||||
			<ui-horizon-group inputs>
 | 
			
		||||
				<ui-input v-model="domain" debounce>
 | 
			
		||||
				<ui-input v-model="domain" :debounce="true">
 | 
			
		||||
					<span>{{ $t('domain') }}</span>
 | 
			
		||||
				</ui-input>
 | 
			
		||||
				<ui-select v-model="level">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,15 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<ui-card>
 | 
			
		||||
		<template #title><fa :icon="faTasks"/> {{ $t('title') }}</template>
 | 
			
		||||
		<template #title><fa :icon="faChartBar"/> {{ $t('title') }}</template>
 | 
			
		||||
		<section class="wptihjuy">
 | 
			
		||||
			<header><fa :icon="faPaperPlane"/> Deliver</header>
 | 
			
		||||
			<ui-info warn v-if="latestStats && latestStats.deliver.waiting > 0">The queue is jammed.</ui-info>
 | 
			
		||||
			<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
 | 
			
		||||
				<ui-input :value="latestStats.deliver.activeSincePrevTick | number" type="text" readonly>
 | 
			
		||||
					<span>Process</span>
 | 
			
		||||
					<template #prefix><fa :icon="fasPlayCircle"/></template>
 | 
			
		||||
					<template #suffix>jobs/s</template>
 | 
			
		||||
					<template #suffix>jobs/tick</template>
 | 
			
		||||
				</ui-input>
 | 
			
		||||
				<ui-input :value="latestStats.deliver.active | number" type="text" readonly>
 | 
			
		||||
					<span>Active</span>
 | 
			
		||||
@@ -30,11 +31,12 @@
 | 
			
		||||
		</section>
 | 
			
		||||
		<section class="wptihjuy">
 | 
			
		||||
			<header><fa :icon="faInbox"/> Inbox</header>
 | 
			
		||||
			<ui-info warn v-if="latestStats && latestStats.inbox.waiting > 0">The queue is jammed.</ui-info>
 | 
			
		||||
			<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
 | 
			
		||||
				<ui-input :value="latestStats.inbox.activeSincePrevTick | number" type="text" readonly>
 | 
			
		||||
					<span>Process</span>
 | 
			
		||||
					<template #prefix><fa :icon="fasPlayCircle"/></template>
 | 
			
		||||
					<template #suffix>jobs/s</template>
 | 
			
		||||
					<template #suffix>jobs/tick</template>
 | 
			
		||||
				</ui-input>
 | 
			
		||||
				<ui-input :value="latestStats.inbox.active | number" type="text" readonly>
 | 
			
		||||
					<span>Active</span>
 | 
			
		||||
@@ -58,6 +60,35 @@
 | 
			
		||||
			<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
 | 
			
		||||
		</section>
 | 
			
		||||
	</ui-card>
 | 
			
		||||
 | 
			
		||||
	<ui-card>
 | 
			
		||||
		<template #title><fa :icon="faTasks"/> {{ $t('jobs') }}</template>
 | 
			
		||||
		<section class="fit-top">
 | 
			
		||||
			<ui-horizon-group inputs>
 | 
			
		||||
				<ui-select v-model="domain">
 | 
			
		||||
					<template #label>{{ $t('queue') }}</template>
 | 
			
		||||
					<option value="deliver">{{ $t('domains.deliver') }}</option>
 | 
			
		||||
					<option value="inbox">{{ $t('domains.inbox') }}</option>
 | 
			
		||||
				</ui-select>
 | 
			
		||||
				<ui-select v-model="state">
 | 
			
		||||
					<template #label>{{ $t('state') }}</template>
 | 
			
		||||
					<option value="delayed">{{ $t('states.delayed') }}</option>
 | 
			
		||||
				</ui-select>
 | 
			
		||||
			</ui-horizon-group>
 | 
			
		||||
			<sequential-entrance animation="entranceFromTop" delay="25">
 | 
			
		||||
				<div class="xvvuvgsv" v-for="job in jobs">
 | 
			
		||||
					<b>{{ job.id }}</b>
 | 
			
		||||
					<template v-if="domain === 'deliver'">
 | 
			
		||||
						<span>{{ job.data.to }}</span>
 | 
			
		||||
					</template>
 | 
			
		||||
					<template v-if="domain === 'inbox'">
 | 
			
		||||
						<span>{{ job.activity.id }}</span>
 | 
			
		||||
					</template>
 | 
			
		||||
				</div>
 | 
			
		||||
			</sequential-entrance>
 | 
			
		||||
			<ui-info v-if="jobs.length == jobsLimit">{{ $t('result-is-truncated', { n: jobsLimit }) }}</ui-info>
 | 
			
		||||
		</section>
 | 
			
		||||
	</ui-card>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -67,9 +98,9 @@ import i18n from '../../i18n';
 | 
			
		||||
import ApexCharts from 'apexcharts';
 | 
			
		||||
import * as tinycolor from 'tinycolor2';
 | 
			
		||||
import { faTasks, faInbox, faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle, faChartBar } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
 | 
			
		||||
const limit = 150;
 | 
			
		||||
const limit = 200;
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('admin/views/queue.vue'),
 | 
			
		||||
@@ -79,7 +110,11 @@ export default Vue.extend({
 | 
			
		||||
			stats: [],
 | 
			
		||||
			deliverChart: null,
 | 
			
		||||
			inboxChart: null,
 | 
			
		||||
			faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle
 | 
			
		||||
			jobs: [],
 | 
			
		||||
			jobsLimit: 50,
 | 
			
		||||
			domain: 'deliver',
 | 
			
		||||
			state: 'delayed',
 | 
			
		||||
			faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle, faChartBar
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@@ -125,12 +160,26 @@ export default Vue.extend({
 | 
			
		||||
				type: 'line',
 | 
			
		||||
				data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
 | 
			
		||||
			}]);
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		domain() {
 | 
			
		||||
			this.jobs = [];
 | 
			
		||||
			this.fetchJobs();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		state() {
 | 
			
		||||
			this.jobs = [];
 | 
			
		||||
			this.fetchJobs();
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		const chartOpts = {
 | 
			
		||||
		this.fetchJobs();
 | 
			
		||||
 | 
			
		||||
		const chartOpts = id => ({
 | 
			
		||||
			chart: {
 | 
			
		||||
				id,
 | 
			
		||||
				group: 'queue',
 | 
			
		||||
				type: 'area',
 | 
			
		||||
				height: 200,
 | 
			
		||||
				animations: {
 | 
			
		||||
@@ -150,7 +199,12 @@ export default Vue.extend({
 | 
			
		||||
			},
 | 
			
		||||
			grid: {
 | 
			
		||||
				clipMarkers: false,
 | 
			
		||||
				borderColor: 'rgba(0, 0, 0, 0.1)'
 | 
			
		||||
				borderColor: 'rgba(0, 0, 0, 0.1)',
 | 
			
		||||
				xaxis: {
 | 
			
		||||
					lines: {
 | 
			
		||||
						show: true,
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			stroke: {
 | 
			
		||||
				curve: 'straight',
 | 
			
		||||
@@ -179,10 +233,10 @@ export default Vue.extend({
 | 
			
		||||
				show: false,
 | 
			
		||||
				min: 0,
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts);
 | 
			
		||||
		this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts);
 | 
			
		||||
		this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts('a'));
 | 
			
		||||
		this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts('b'));
 | 
			
		||||
 | 
			
		||||
		this.inboxChart.render();
 | 
			
		||||
		this.deliverChart.render();
 | 
			
		||||
@@ -229,7 +283,17 @@ export default Vue.extend({
 | 
			
		||||
			for (const stats of statsLog.reverse()) {
 | 
			
		||||
				this.onStats(stats);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fetchJobs() {
 | 
			
		||||
			this.$root.api('admin/queue/jobs', {
 | 
			
		||||
				domain: this.domain,
 | 
			
		||||
				state: this.state,
 | 
			
		||||
				limit: this.jobsLimit
 | 
			
		||||
			}).then(jobs => {
 | 
			
		||||
				this.jobs = jobs;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -238,5 +302,10 @@ export default Vue.extend({
 | 
			
		||||
.wptihjuy
 | 
			
		||||
	> .chart
 | 
			
		||||
		min-height 200px !important
 | 
			
		||||
		margin 0 -8px
 | 
			
		||||
 | 
			
		||||
.xvvuvgsv
 | 
			
		||||
	> b
 | 
			
		||||
		margin-right 16px
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,7 @@ export default Vue.extend({
 | 
			
		||||
				},
 | 
			
		||||
				plotOptions: {
 | 
			
		||||
					bar: {
 | 
			
		||||
						columnWidth: '90%'
 | 
			
		||||
						columnWidth: '80%'
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				grid: {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import * as emojilib from 'emojilib';
 | 
			
		||||
import contains from '../../../common/scripts/contains';
 | 
			
		||||
import { twemojiBase } from '../../../../../misc/twemoji-base';
 | 
			
		||||
 | 
			
		||||
type EmojiDef = {
 | 
			
		||||
	emoji: string;
 | 
			
		||||
@@ -54,7 +55,7 @@ const emjdb: EmojiDef[] = lib.map((x: any) => ({
 | 
			
		||||
	emoji: x[1].char,
 | 
			
		||||
	name: x[0],
 | 
			
		||||
	aliasOf: null,
 | 
			
		||||
	url: `https://twemoji.maxcdn.com/2/svg/${char2file(x[1].char)}.svg`
 | 
			
		||||
	url: `${twemojiBase}/2/svg/${char2file(x[1].char)}.svg`
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
for (const x of lib as any) {
 | 
			
		||||
@@ -64,7 +65,7 @@ for (const x of lib as any) {
 | 
			
		||||
				emoji: x[1].char,
 | 
			
		||||
				name: k,
 | 
			
		||||
				aliasOf: x[0],
 | 
			
		||||
				url: `https://twemoji.maxcdn.com/2/svg/${char2file(x[1].char)}.svg`
 | 
			
		||||
				url: `${twemojiBase}/2/svg/${char2file(x[1].char)}.svg`
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										177
									
								
								src/client/app/common/views/components/drive-file-thumbnail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								src/client/app/common/views/components/drive-file-thumbnail.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,177 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="zdjebgpv" :class="{ detail }" ref="thumbnail" :style="`background-color: ${ background }`">
 | 
			
		||||
	<img
 | 
			
		||||
		:src="file.url"
 | 
			
		||||
		:alt="file.name"
 | 
			
		||||
		:title="file.name"
 | 
			
		||||
		v-if="detail && is === 'image'"/>
 | 
			
		||||
	<video
 | 
			
		||||
		:src="file.url"
 | 
			
		||||
		ref="volumectrl"
 | 
			
		||||
		preload="metadata"
 | 
			
		||||
		controls
 | 
			
		||||
		v-else-if="detail && is === 'video'"/>
 | 
			
		||||
	<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/>
 | 
			
		||||
	<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
 | 
			
		||||
	<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
 | 
			
		||||
 | 
			
		||||
	<audio
 | 
			
		||||
		:src="file.url"
 | 
			
		||||
		ref="volumectrl"
 | 
			
		||||
		preload="metadata"
 | 
			
		||||
		controls
 | 
			
		||||
		v-else-if="detail && is === 'audio'"/>
 | 
			
		||||
	<fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
 | 
			
		||||
 | 
			
		||||
	<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
 | 
			
		||||
	<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
 | 
			
		||||
	<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
 | 
			
		||||
	<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
 | 
			
		||||
	<fa :icon="faFile" class="icon" v-else/>
 | 
			
		||||
 | 
			
		||||
	<fa :icon="faFilm" class="icon-sub" v-if="!detail && isThumbnailAvailable && is === 'video'"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import anime from 'animejs';
 | 
			
		||||
import {
 | 
			
		||||
	faFile,
 | 
			
		||||
	faFileAlt,
 | 
			
		||||
	faFileImage,
 | 
			
		||||
	faMusic,
 | 
			
		||||
	faFileVideo,
 | 
			
		||||
	faFileCsv,
 | 
			
		||||
	faFilePdf,
 | 
			
		||||
	faFileArchive,
 | 
			
		||||
	faFilm
 | 
			
		||||
	} from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: {
 | 
			
		||||
		file: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		fit: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		detail: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			isContextmenuShowing: false,
 | 
			
		||||
			isDragging: false,
 | 
			
		||||
 | 
			
		||||
			faFile,
 | 
			
		||||
			faFileAlt,
 | 
			
		||||
			faFileImage,
 | 
			
		||||
			faMusic,
 | 
			
		||||
			faFileVideo,
 | 
			
		||||
			faFileCsv,
 | 
			
		||||
			faFilePdf,
 | 
			
		||||
			faFileArchive,
 | 
			
		||||
			faFilm
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
 | 
			
		||||
			if (this.file.type.startsWith('image/')) return 'image';
 | 
			
		||||
			if (this.file.type.startsWith('video/')) return 'video';
 | 
			
		||||
			if (this.file.type === 'audio/midi') return 'midi';
 | 
			
		||||
			if (this.file.type.startsWith('audio/')) return 'audio';
 | 
			
		||||
			if (this.file.type.endsWith('/csv')) return 'csv';
 | 
			
		||||
			if (this.file.type.endsWith('/pdf')) return 'pdf';
 | 
			
		||||
			if (this.file.type.startsWith('text/')) return 'textfile';
 | 
			
		||||
			if ([
 | 
			
		||||
					"application/zip",
 | 
			
		||||
					"application/x-cpio",
 | 
			
		||||
					"application/x-bzip",
 | 
			
		||||
					"application/x-bzip2",
 | 
			
		||||
					"application/java-archive",
 | 
			
		||||
					"application/x-rar-compressed",
 | 
			
		||||
					"application/x-tar",
 | 
			
		||||
					"application/gzip",
 | 
			
		||||
					"application/x-7z-compressed"
 | 
			
		||||
				].some(e => e === this.file.type)) return 'archive';
 | 
			
		||||
			return 'unknown';
 | 
			
		||||
		},
 | 
			
		||||
		isThumbnailAvailable(): boolean {
 | 
			
		||||
			return this.file.thumbnailUrl
 | 
			
		||||
				? this.file.thumbnailUrl.endsWith('?thumbnail')
 | 
			
		||||
					? (this.is === 'image' || this.is === 'video')
 | 
			
		||||
					: true
 | 
			
		||||
				: false;
 | 
			
		||||
		},
 | 
			
		||||
		background(): string {
 | 
			
		||||
			return this.file.properties.avgColor && this.file.properties.avgColor.length == 3
 | 
			
		||||
				? `rgb(${this.file.properties.avgColor.join(',')})`
 | 
			
		||||
				: 'transparent';
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		const audioTag = this.$refs.volumectrl as HTMLAudioElement;
 | 
			
		||||
		if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		onThumbnailLoaded() {
 | 
			
		||||
			if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) {
 | 
			
		||||
				anime({
 | 
			
		||||
					targets: this.$refs.thumbnail,
 | 
			
		||||
					backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,
 | 
			
		||||
					duration: 100,
 | 
			
		||||
					easing: 'linear'
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		volumechange() {
 | 
			
		||||
			const audioTag = this.$refs.volumectrl as HTMLAudioElement;
 | 
			
		||||
			this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.zdjebgpv
 | 
			
		||||
	display flex
 | 
			
		||||
 | 
			
		||||
	> img,
 | 
			
		||||
	> .icon
 | 
			
		||||
		pointer-events none
 | 
			
		||||
 | 
			
		||||
	> img
 | 
			
		||||
		height 100%
 | 
			
		||||
		width 100%
 | 
			
		||||
		margin auto
 | 
			
		||||
		object-fit cover
 | 
			
		||||
 | 
			
		||||
	> .icon
 | 
			
		||||
		height 65%
 | 
			
		||||
		width 65%
 | 
			
		||||
		margin auto
 | 
			
		||||
 | 
			
		||||
	> video,
 | 
			
		||||
	> audio
 | 
			
		||||
		width 100%
 | 
			
		||||
 | 
			
		||||
	> .icon-sub
 | 
			
		||||
		position absolute
 | 
			
		||||
		width 30%
 | 
			
		||||
		height auto
 | 
			
		||||
		margin 0
 | 
			
		||||
		right 4%
 | 
			
		||||
		bottom 4%
 | 
			
		||||
 | 
			
		||||
	&.detail
 | 
			
		||||
		> .icon
 | 
			
		||||
			height 100px
 | 
			
		||||
			margin 16px auto
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -10,6 +10,7 @@ import Vue from 'vue';
 | 
			
		||||
// スクリプトサイズがデカい
 | 
			
		||||
//import { lib } from 'emojilib';
 | 
			
		||||
import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
 | 
			
		||||
import { twemojiBase } from '../../../../../misc/twemoji-base';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -29,7 +30,11 @@ export default Vue.extend({
 | 
			
		||||
		customEmojis: {
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: () => []
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
		isReaction: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			default: false
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
@@ -46,7 +51,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		useOsDefaultEmojis(): boolean {
 | 
			
		||||
			return this.$store.state.device.useOsDefaultEmojis;
 | 
			
		||||
			return this.$store.state.device.useOsDefaultEmojis && !this.isReaction;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@@ -73,7 +78,7 @@ export default Vue.extend({
 | 
			
		||||
			if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
 | 
			
		||||
			codes = codes.filter(x => x && x.length);
 | 
			
		||||
 | 
			
		||||
			this.url = `https://twemoji.maxcdn.com/2/svg/${codes.join('-')}.svg`;
 | 
			
		||||
			this.url = `${twemojiBase}/2/svg/${codes.join('-')}.svg`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<span class="mk-reaction-icon">
 | 
			
		||||
	<img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" :alt="$t('@.reactions.like')">
 | 
			
		||||
	<img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" :alt="$t('@.reactions.love')">
 | 
			
		||||
	<img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" :alt="$t('@.reactions.laugh')">
 | 
			
		||||
	<img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" :alt="$t('@.reactions.hmm')">
 | 
			
		||||
	<img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" :alt="$t('@.reactions.surprise')">
 | 
			
		||||
	<img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" :alt="$t('@.reactions.congrats')">
 | 
			
		||||
	<img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" :alt="$t('@.reactions.angry')">
 | 
			
		||||
	<img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" :alt="$t('@.reactions.confused')">
 | 
			
		||||
	<img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" :alt="$t('@.reactions.rip')">
 | 
			
		||||
	<template v-if="reaction == 'pudding'">
 | 
			
		||||
		<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" :alt="$t('@.reactions.pudding')">
 | 
			
		||||
		<img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" :alt="$t('@.reactions.pudding')">
 | 
			
		||||
	</template>
 | 
			
		||||
</span>
 | 
			
		||||
<mk-emoji :emoji="str.startsWith(':') ? null : str" :name="str.startsWith(':') ? str.substr(1, str.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true"/>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
@@ -21,7 +7,35 @@ import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n(),
 | 
			
		||||
	props: ['reaction']
 | 
			
		||||
	props: {
 | 
			
		||||
		reaction: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			customEmojis: (this.$root.getMetaSync() || { emojis: [] }).emojis || []
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		str(): any {
 | 
			
		||||
			switch (this.reaction) {
 | 
			
		||||
				case 'like': return '👍';
 | 
			
		||||
				case 'love': return '❤';
 | 
			
		||||
				case 'laugh': return '😆';
 | 
			
		||||
				case 'hmm': return '🤔';
 | 
			
		||||
				case 'surprise': return '😮';
 | 
			
		||||
				case 'congrats': return '🎉';
 | 
			
		||||
				case 'angry': return '💢';
 | 
			
		||||
				case 'confused': return '😥';
 | 
			
		||||
				case 'rip': return '😇';
 | 
			
		||||
				case 'pudding': return (this.$store.getters.isSignedIn && this.$store.state.settings.iLikeSushi) ? '🍣' : '🍮';
 | 
			
		||||
				case 'star': return '⭐';
 | 
			
		||||
				default: return this.reaction;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
	<div class="backdrop" ref="backdrop" @click="close"></div>
 | 
			
		||||
	<div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover">
 | 
			
		||||
		<p v-if="!$root.isMobile">{{ title }}</p>
 | 
			
		||||
		<div ref="buttons" :class="{ showFocus }">
 | 
			
		||||
		<div class="buttons" ref="buttons" :class="{ showFocus }">
 | 
			
		||||
			<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" :title="$t('@.reactions.like')" v-particle><mk-reaction-icon reaction="like"/></button>
 | 
			
		||||
			<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" :title="$t('@.reactions.love')" v-particle><mk-reaction-icon reaction="love"/></button>
 | 
			
		||||
			<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" :title="$t('@.reactions.laugh')" v-particle><mk-reaction-icon reaction="laugh"/></button>
 | 
			
		||||
@@ -15,6 +15,9 @@
 | 
			
		||||
			<button @click="react('rip')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="9" :title="$t('@.reactions.rip')" v-particle><mk-reaction-icon reaction="rip"/></button>
 | 
			
		||||
			<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="10" :title="$t('@.reactions.pudding')" v-particle><mk-reaction-icon reaction="pudding"/></button>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="enableEmojiReaction" class="text">
 | 
			
		||||
			<input v-model="text" placeholder="または絵文字を入力" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -23,6 +26,7 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import anime from 'animejs';
 | 
			
		||||
import { emojiRegex } from '../../../../../misc/emoji-regex';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('common/views/components/reaction-picker.vue'),
 | 
			
		||||
@@ -56,6 +60,8 @@ export default Vue.extend({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('choose-reaction'),
 | 
			
		||||
			text: null,
 | 
			
		||||
			enableEmojiReaction: false,
 | 
			
		||||
			focus: null
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
@@ -94,6 +100,10 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$root.getMeta().then(meta => {
 | 
			
		||||
			this.enableEmojiReaction = meta.enableEmojiReaction;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.$nextTick(() => {
 | 
			
		||||
			this.focus = 0;
 | 
			
		||||
 | 
			
		||||
@@ -143,6 +153,17 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reactText() {
 | 
			
		||||
			if (!this.text) return;
 | 
			
		||||
			this.react(this.text);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		tryReactText() {
 | 
			
		||||
			if (!this.text) return;
 | 
			
		||||
			if (!this.text.match(emojiRegex)) return;
 | 
			
		||||
			this.reactText();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onMouseover(e) {
 | 
			
		||||
			this.title = e.target.title;
 | 
			
		||||
		},
 | 
			
		||||
@@ -256,9 +277,9 @@ export default Vue.extend({
 | 
			
		||||
			color var(--popupFg)
 | 
			
		||||
			border-bottom solid var(--lineWidth) var(--faceDivider)
 | 
			
		||||
 | 
			
		||||
		> div
 | 
			
		||||
			padding 4px
 | 
			
		||||
			width 240px
 | 
			
		||||
		> .buttons
 | 
			
		||||
			padding 4px 4px 8px 4px
 | 
			
		||||
			width 216px
 | 
			
		||||
			text-align center
 | 
			
		||||
 | 
			
		||||
			&.showFocus
 | 
			
		||||
@@ -283,6 +304,9 @@ export default Vue.extend({
 | 
			
		||||
				font-size 24px
 | 
			
		||||
				border-radius 2px
 | 
			
		||||
 | 
			
		||||
				> *
 | 
			
		||||
					height 1em
 | 
			
		||||
 | 
			
		||||
				&:hover
 | 
			
		||||
					background var(--reactionPickerButtonHoverBg)
 | 
			
		||||
 | 
			
		||||
@@ -290,4 +314,29 @@ export default Vue.extend({
 | 
			
		||||
					background var(--primary)
 | 
			
		||||
					box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
 | 
			
		||||
 | 
			
		||||
		> .text
 | 
			
		||||
			width 216px
 | 
			
		||||
			padding 0 8px 8px 8px
 | 
			
		||||
 | 
			
		||||
			> input
 | 
			
		||||
				width 100%
 | 
			
		||||
				padding 10px
 | 
			
		||||
				margin 0
 | 
			
		||||
				text-align center
 | 
			
		||||
				font-size 16px
 | 
			
		||||
				color var(--desktopPostFormTextareaFg)
 | 
			
		||||
				background var(--desktopPostFormTextareaBg)
 | 
			
		||||
				outline none
 | 
			
		||||
				border solid 1px var(--primaryAlpha01)
 | 
			
		||||
				border-radius 4px
 | 
			
		||||
				transition border-color .2s ease
 | 
			
		||||
 | 
			
		||||
				&:hover
 | 
			
		||||
					border-color var(--primaryAlpha02)
 | 
			
		||||
					transition border-color .1s ease
 | 
			
		||||
 | 
			
		||||
				&:focus
 | 
			
		||||
					border-color var(--primaryAlpha05)
 | 
			
		||||
					transition border-color 0s ease
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -136,12 +136,8 @@ export default Vue.extend({
 | 
			
		||||
		&:hover
 | 
			
		||||
			background var(--reactionViewerButtonHoverBg)
 | 
			
		||||
 | 
			
		||||
	> .mk-reaction-icon
 | 
			
		||||
		font-size 1.4em
 | 
			
		||||
 | 
			
		||||
	> span
 | 
			
		||||
		font-size 1.1em
 | 
			
		||||
		line-height 32px
 | 
			
		||||
		vertical-align middle
 | 
			
		||||
		color var(--text)
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -93,12 +93,17 @@ export default Vue.extend({
 | 
			
		||||
					},
 | 
			
		||||
					plotOptions: {
 | 
			
		||||
						bar: {
 | 
			
		||||
							columnWidth: '90%'
 | 
			
		||||
							columnWidth: '80%'
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					grid: {
 | 
			
		||||
						clipMarkers: false,
 | 
			
		||||
						borderColor: 'rgba(0, 0, 0, 0.1)'
 | 
			
		||||
						borderColor: 'rgba(0, 0, 0, 0.1)',
 | 
			
		||||
						xaxis: {
 | 
			
		||||
							lines: {
 | 
			
		||||
								show: true,
 | 
			
		||||
							}
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					tooltip: {
 | 
			
		||||
						shared: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -159,7 +159,7 @@
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<template v-if="page == null || page == 'notification'">
 | 
			
		||||
		<x-notification v-show="page == 'notification'"/>
 | 
			
		||||
		<x-notification/>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<template v-if="page == null || page == 'drive'">
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,14 @@
 | 
			
		||||
	<ol v-if="uploads.length > 0">
 | 
			
		||||
		<li v-for="ctx in uploads" :key="ctx.id">
 | 
			
		||||
			<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
 | 
			
		||||
			<p class="name"><fa icon="spinner" pulse/>{{ ctx.name }}</p>
 | 
			
		||||
			<p class="status">
 | 
			
		||||
				<span class="initing" v-if="ctx.progress == undefined">{{ $t('waiting') }}<mk-ellipsis/></span>
 | 
			
		||||
				<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
 | 
			
		||||
				<span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span>
 | 
			
		||||
			</p>
 | 
			
		||||
			<div class="top">
 | 
			
		||||
				<p class="name"><fa icon="spinner" pulse/>{{ ctx.name }}</p>
 | 
			
		||||
				<p class="status">
 | 
			
		||||
					<span class="initing" v-if="ctx.progress == undefined">{{ $t('waiting') }}<mk-ellipsis/></span>
 | 
			
		||||
					<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
 | 
			
		||||
					<span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span>
 | 
			
		||||
				</p>
 | 
			
		||||
			</div>
 | 
			
		||||
			<progress v-if="ctx.progress != undefined && ctx.progress.value != ctx.progress.max" :value="ctx.progress.value" :max="ctx.progress.max"></progress>
 | 
			
		||||
			<div class="progress initing" v-if="ctx.progress == undefined"></div>
 | 
			
		||||
			<div class="progress waiting" v-if="ctx.progress != undefined && ctx.progress.value == ctx.progress.max"></div>
 | 
			
		||||
@@ -116,12 +118,17 @@ export default Vue.extend({
 | 
			
		||||
		list-style none
 | 
			
		||||
 | 
			
		||||
		> li
 | 
			
		||||
			display block
 | 
			
		||||
			display grid
 | 
			
		||||
			margin 8px 0 0 0
 | 
			
		||||
			padding 0
 | 
			
		||||
			height 36px
 | 
			
		||||
			width: 100%
 | 
			
		||||
			box-shadow 0 -1px 0 var(--primaryAlpha01)
 | 
			
		||||
			border-top solid 8px transparent
 | 
			
		||||
			grid-template-columns 36px calc(100% - 44px)
 | 
			
		||||
			grid-template-rows 1fr 8px
 | 
			
		||||
			column-gap 8px
 | 
			
		||||
			box-sizing content-box
 | 
			
		||||
 | 
			
		||||
			&:first-child
 | 
			
		||||
				margin 0
 | 
			
		||||
@@ -130,68 +137,62 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
			> .img
 | 
			
		||||
				display block
 | 
			
		||||
				position absolute
 | 
			
		||||
				top 0
 | 
			
		||||
				left 0
 | 
			
		||||
				width 36px
 | 
			
		||||
				height 36px
 | 
			
		||||
				background-size cover
 | 
			
		||||
				background-position center center
 | 
			
		||||
				grid-column 1 / 2
 | 
			
		||||
				grid-row 1 / 3
 | 
			
		||||
 | 
			
		||||
			> .name
 | 
			
		||||
				display block
 | 
			
		||||
				position absolute
 | 
			
		||||
				top 0
 | 
			
		||||
				left 44px
 | 
			
		||||
				margin 0
 | 
			
		||||
				padding 0
 | 
			
		||||
				max-width 256px
 | 
			
		||||
				font-size 0.8em
 | 
			
		||||
				color var(--primaryAlpha07)
 | 
			
		||||
				white-space nowrap
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
				overflow hidden
 | 
			
		||||
 | 
			
		||||
				> [data-icon]
 | 
			
		||||
					margin-right 4px
 | 
			
		||||
 | 
			
		||||
			> .status
 | 
			
		||||
				display block
 | 
			
		||||
				position absolute
 | 
			
		||||
				top 0
 | 
			
		||||
				right 0
 | 
			
		||||
				margin 0
 | 
			
		||||
				padding 0
 | 
			
		||||
				font-size 0.8em
 | 
			
		||||
 | 
			
		||||
				> .initing
 | 
			
		||||
					color var(--primaryAlpha05)
 | 
			
		||||
 | 
			
		||||
				> .kb
 | 
			
		||||
					color var(--primaryAlpha05)
 | 
			
		||||
 | 
			
		||||
				> .percentage
 | 
			
		||||
					display inline-block
 | 
			
		||||
					width 48px
 | 
			
		||||
					text-align right
 | 
			
		||||
			> .top
 | 
			
		||||
				display flex
 | 
			
		||||
				grid-column 2 / 3
 | 
			
		||||
				grid-row 1 / 2
 | 
			
		||||
 | 
			
		||||
				> .name
 | 
			
		||||
					display block
 | 
			
		||||
					padding 0 8px 0 0
 | 
			
		||||
					margin 0
 | 
			
		||||
					font-size 0.8em
 | 
			
		||||
					color var(--primaryAlpha07)
 | 
			
		||||
					white-space nowrap
 | 
			
		||||
					text-overflow ellipsis
 | 
			
		||||
					overflow hidden
 | 
			
		||||
					flex-shrink 1
 | 
			
		||||
 | 
			
		||||
					&:after
 | 
			
		||||
						content '%'
 | 
			
		||||
					> [data-icon]
 | 
			
		||||
						margin-right 4px
 | 
			
		||||
 | 
			
		||||
				> .status
 | 
			
		||||
					display block
 | 
			
		||||
					margin 0 0 0 auto
 | 
			
		||||
					padding 0
 | 
			
		||||
					font-size 0.8em
 | 
			
		||||
					flex-shrink 0
 | 
			
		||||
 | 
			
		||||
					> .initing
 | 
			
		||||
						color var(--primaryAlpha05)
 | 
			
		||||
 | 
			
		||||
					> .kb
 | 
			
		||||
						color var(--primaryAlpha05)
 | 
			
		||||
 | 
			
		||||
					> .percentage
 | 
			
		||||
						display inline-block
 | 
			
		||||
						width 48px
 | 
			
		||||
						text-align right
 | 
			
		||||
 | 
			
		||||
						color var(--primaryAlpha07)
 | 
			
		||||
 | 
			
		||||
						&:after
 | 
			
		||||
							content '%'
 | 
			
		||||
 | 
			
		||||
			> progress
 | 
			
		||||
				display block
 | 
			
		||||
				position absolute
 | 
			
		||||
				bottom 0
 | 
			
		||||
				right 0
 | 
			
		||||
				margin 0
 | 
			
		||||
				width calc(100% - 44px)
 | 
			
		||||
				height 8px
 | 
			
		||||
				background transparent
 | 
			
		||||
				border none
 | 
			
		||||
				border-radius 4px
 | 
			
		||||
				overflow hidden
 | 
			
		||||
				grid-column 2 / 3
 | 
			
		||||
				grid-row 2 / 3
 | 
			
		||||
				z-index 2
 | 
			
		||||
 | 
			
		||||
				&::-webkit-progress-value
 | 
			
		||||
					background var(--primary)
 | 
			
		||||
@@ -201,12 +202,6 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
			> .progress
 | 
			
		||||
				display block
 | 
			
		||||
				position absolute
 | 
			
		||||
				bottom 0
 | 
			
		||||
				right 0
 | 
			
		||||
				margin 0
 | 
			
		||||
				width calc(100% - 44px)
 | 
			
		||||
				height 8px
 | 
			
		||||
				border none
 | 
			
		||||
				border-radius 4px
 | 
			
		||||
				background linear-gradient(
 | 
			
		||||
@@ -221,6 +216,9 @@ export default Vue.extend({
 | 
			
		||||
				)
 | 
			
		||||
				background-size 32px 32px
 | 
			
		||||
				animation bg 1.5s linear infinite
 | 
			
		||||
				grid-column 2 / 3
 | 
			
		||||
				grid-row 2 / 3
 | 
			
		||||
				z-index 1
 | 
			
		||||
 | 
			
		||||
				&.initing
 | 
			
		||||
					opacity 0.3
 | 
			
		||||
 
 | 
			
		||||
@@ -172,7 +172,7 @@ export default Vue.extend({
 | 
			
		||||
					},
 | 
			
		||||
					plotOptions: {
 | 
			
		||||
						bar: {
 | 
			
		||||
							columnWidth: '90%'
 | 
			
		||||
							columnWidth: '80%'
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					grid: {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
				<p @click="click(item)"><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}</p>
 | 
			
		||||
			</template>
 | 
			
		||||
			<template v-else-if="item.type == 'link'">
 | 
			
		||||
				<a :href="item.href" :target="item.target" @click="click(item)"><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}</a>
 | 
			
		||||
				<a :href="item.href" :target="item.target" @click="click(item)" :download="item.download"><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}</a>
 | 
			
		||||
			</template>
 | 
			
		||||
			<template v-else-if="item.type == 'nest'">
 | 
			
		||||
				<p><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}...<span class="caret"><fa icon="caret-right"/></span></p>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
		<template #header><fa icon="crop"/>{{ title }}</template>
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<vue-cropper ref="cropper"
 | 
			
		||||
				:src="image.url"
 | 
			
		||||
				:src="imageUrl"
 | 
			
		||||
				:view-mode="1"
 | 
			
		||||
				:aspect-ratio="aspectRatio"
 | 
			
		||||
				:container-style="{ width: '100%', 'max-height': '400px' }"
 | 
			
		||||
@@ -21,6 +21,7 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import VueCropper from 'vue-cropperjs';
 | 
			
		||||
import * as url from '../../../../../prelude/url';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('desktop/views/components/crop-window.vue'),
 | 
			
		||||
@@ -41,6 +42,13 @@ export default Vue.extend({
 | 
			
		||||
			required: true
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		imageUrl() {
 | 
			
		||||
			return `/proxy/?${url.query({
 | 
			
		||||
				url: this.image.url
 | 
			
		||||
			})}`;
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		ok() {
 | 
			
		||||
			(this.$refs.cropper as any).getCroppedCanvas().toBlob(blob => {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,9 @@
 | 
			
		||||
		<img src="/assets/label-red.svg"/>
 | 
			
		||||
		<p>{{ $t('nsfw') }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
 | 
			
		||||
		<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded"/>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<x-file-thumbnail class="thumbnail" :file="file" fit="contain"/>
 | 
			
		||||
 | 
			
		||||
	<p class="name">
 | 
			
		||||
		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
 | 
			
		||||
		<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
 | 
			
		||||
@@ -34,14 +34,18 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import anime from 'animejs';
 | 
			
		||||
import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
 | 
			
		||||
import updateAvatar from '../../api/update-avatar';
 | 
			
		||||
import updateBanner from '../../api/update-banner';
 | 
			
		||||
import { appendQuery } from '../../../../../prelude/url';
 | 
			
		||||
import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('desktop/views/components/drive.file.vue'),
 | 
			
		||||
	props: ['file'],
 | 
			
		||||
	components: {
 | 
			
		||||
		XFileThumbnail
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			isContextmenuShowing: false,
 | 
			
		||||
@@ -57,11 +61,6 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
		title(): string {
 | 
			
		||||
			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
 | 
			
		||||
		},
 | 
			
		||||
		background(): string {
 | 
			
		||||
			return this.file.properties.avgColor && this.file.properties.avgColor.length == 3
 | 
			
		||||
				? `rgb(${this.file.properties.avgColor.join(',')})`
 | 
			
		||||
				: 'transparent';
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
@@ -88,9 +87,10 @@ export default Vue.extend({
 | 
			
		||||
				action: this.copyUrl
 | 
			
		||||
			}, {
 | 
			
		||||
				type: 'link',
 | 
			
		||||
				href: `${this.file.url}?download`,
 | 
			
		||||
				href: appendQuery(this.file.url, 'download'),
 | 
			
		||||
				text: this.$t('contextmenu.download'),
 | 
			
		||||
				icon: 'download',
 | 
			
		||||
				download: this.file.name
 | 
			
		||||
			}, null, {
 | 
			
		||||
				type: 'item',
 | 
			
		||||
				text: this.$t('@.delete'),
 | 
			
		||||
@@ -205,7 +205,7 @@ export default Vue.extend({
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.gvfdktuvdgwhmztnuekzkswkjygptfcv
 | 
			
		||||
	padding 8px 0 0 0
 | 
			
		||||
	height 180px
 | 
			
		||||
	min-height 180px
 | 
			
		||||
	border-radius 4px
 | 
			
		||||
 | 
			
		||||
	&, *
 | 
			
		||||
@@ -254,6 +254,9 @@ export default Vue.extend({
 | 
			
		||||
		> .name
 | 
			
		||||
			color var(--primaryForeground)
 | 
			
		||||
 | 
			
		||||
		> .thumbnail
 | 
			
		||||
			color var(--primaryForeground)
 | 
			
		||||
 | 
			
		||||
	&[data-is-contextmenu-showing]
 | 
			
		||||
		&:after
 | 
			
		||||
			content ""
 | 
			
		||||
@@ -319,18 +322,7 @@ export default Vue.extend({
 | 
			
		||||
		width 128px
 | 
			
		||||
		height 128px
 | 
			
		||||
		margin auto
 | 
			
		||||
 | 
			
		||||
		> img
 | 
			
		||||
			display block
 | 
			
		||||
			position absolute
 | 
			
		||||
			top 0
 | 
			
		||||
			left 0
 | 
			
		||||
			right 0
 | 
			
		||||
			bottom 0
 | 
			
		||||
			margin auto
 | 
			
		||||
			max-width 128px
 | 
			
		||||
			max-height 128px
 | 
			
		||||
			pointer-events none
 | 
			
		||||
		color var(--driveFileIcon)
 | 
			
		||||
 | 
			
		||||
	> .name
 | 
			
		||||
		display block
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
 | 
			
		||||
	<template #header><fa icon="comments"/> {{ $t('title') }} <mk-user-name :user="user"/></template>
 | 
			
		||||
	<template #header><fa icon="comments"/> {{ $t('@.messaging') }}: <mk-user-name :user="user"/></template>
 | 
			
		||||
	<x-messaging-room :user="user" :class="$style.content"/>
 | 
			
		||||
</mk-window>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -12,7 +12,7 @@ import { url } from '../../../config';
 | 
			
		||||
import getAcct from '../../../../../misc/acct/render';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('desktop/views/components/messaging-room-window.vue'),
 | 
			
		||||
	i18n: i18n(),
 | 
			
		||||
	components: {
 | 
			
		||||
		XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default)
 | 
			
		||||
	},
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
 | 
			
		||||
	<template #header :class="$style.header"><fa icon="comments"/>{{ $t('title') }}</template>
 | 
			
		||||
	<template #header :class="$style.header"><fa icon="comments"/>{{ $t('@.messaging') }}</template>
 | 
			
		||||
	<x-messaging :class="$style.content" @navigate="navigate"/>
 | 
			
		||||
</mk-window>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -11,7 +11,7 @@ import i18n from '../../../i18n';
 | 
			
		||||
import MkMessagingRoomWindow from './messaging-room-window.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('desktop/views/components/messaging-window.vue'),
 | 
			
		||||
	i18n: i18n(),
 | 
			
		||||
	components: {
 | 
			
		||||
		XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default)
 | 
			
		||||
	},
 | 
			
		||||
 
 | 
			
		||||
@@ -480,7 +480,7 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (this.text && this.text != '') {
 | 
			
		||||
				const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
 | 
			
		||||
				const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag);
 | 
			
		||||
				const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
 | 
			
		||||
				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import parseAcct from '../../../../../misc/acct/parse';
 | 
			
		||||
import getUserName from '../../../../../misc/get-user-name';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('.vue'),
 | 
			
		||||
	i18n: i18n(),
 | 
			
		||||
	components: {
 | 
			
		||||
		XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default)
 | 
			
		||||
	},
 | 
			
		||||
@@ -51,7 +51,7 @@ export default Vue.extend({
 | 
			
		||||
				this.user = user;
 | 
			
		||||
				this.fetching = false;
 | 
			
		||||
 | 
			
		||||
				document.title = `メッセージ: ${getUserName(this.user)}`;
 | 
			
		||||
				document.title = this.$t('@.messaging') + ': ' + getUserName(this.user);
 | 
			
		||||
 | 
			
		||||
				Progress.done();
 | 
			
		||||
			});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mkw-messaging">
 | 
			
		||||
	<ui-container :show-header="props.design == 0">
 | 
			
		||||
		<template #header><fa icon="comments"/>{{ $t('title') }}</template>
 | 
			
		||||
		<template #header><fa icon="comments"/>{{ $t('@.messaging') }}</template>
 | 
			
		||||
		<template #func><button @click="add"><fa icon="plus"/></button></template>
 | 
			
		||||
 | 
			
		||||
		<x-messaging ref="index" compact @navigate="navigate"/>
 | 
			
		||||
@@ -21,7 +21,7 @@ export default define({
 | 
			
		||||
		design: 0
 | 
			
		||||
	})
 | 
			
		||||
}).extend({
 | 
			
		||||
	i18n: i18n('desktop/views/widgets/messaging.vue'),
 | 
			
		||||
	i18n: i18n(''),
 | 
			
		||||
	components: {
 | 
			
		||||
		XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default)
 | 
			
		||||
	},
 | 
			
		||||
 
 | 
			
		||||
@@ -16,11 +16,11 @@ import App from './app.vue';
 | 
			
		||||
import checkForUpdate from './common/scripts/check-for-update';
 | 
			
		||||
import MiOS from './mios';
 | 
			
		||||
import { version, codename, lang, locale } from './config';
 | 
			
		||||
import { builtinThemes, applyTheme, darkTheme } from './theme';
 | 
			
		||||
import { builtinThemes, applyTheme, futureTheme } from './theme';
 | 
			
		||||
import Dialog from './common/views/components/dialog.vue';
 | 
			
		||||
 | 
			
		||||
if (localStorage.getItem('theme') == null) {
 | 
			
		||||
	applyTheme(darkTheme);
 | 
			
		||||
	applyTheme(futureTheme);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#region FontAwesome
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="pyvicwrksnfyhpfgkjwqknuururpaztw">
 | 
			
		||||
	<div class="preview">
 | 
			
		||||
		<img v-if="kind == 'image'" ref="img"
 | 
			
		||||
			:src="file.url"
 | 
			
		||||
			:alt="file.name"
 | 
			
		||||
			:title="file.name"
 | 
			
		||||
			:style="style">
 | 
			
		||||
		<x-file-thumbnail class="preview" :file="file" fit="cover" :detail="true"/>
 | 
			
		||||
		<template v-if="kind != 'image'"><fa icon="file"/></template>
 | 
			
		||||
		<footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height">
 | 
			
		||||
			<span class="size">
 | 
			
		||||
@@ -38,7 +34,7 @@
 | 
			
		||||
	<div class="menu">
 | 
			
		||||
		<div>
 | 
			
		||||
			<ui-input readonly :value="file.url">URL</ui-input>
 | 
			
		||||
			<ui-button link :href="`${file.url}?download`" :download="file.name"><fa icon="download"/> {{ $t('download') }}</ui-button>
 | 
			
		||||
			<ui-button link :href="dlUrl" :download="file.name"><fa icon="download"/> {{ $t('download') }}</ui-button>
 | 
			
		||||
			<ui-button @click="rename"><fa icon="pencil-alt"/> {{ $t('rename') }}</ui-button>
 | 
			
		||||
			<ui-button @click="move"><fa :icon="['far', 'folder-open']"/> {{ $t('move') }}</ui-button>
 | 
			
		||||
			<ui-button @click="toggleSensitive" v-if="file.isSensitive"><fa :icon="['far', 'eye']"/> {{ $t('unmark-as-sensitive') }}</ui-button>
 | 
			
		||||
@@ -61,11 +57,17 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import { gcd } from '../../../../../prelude/math';
 | 
			
		||||
import { appendQuery } from '../../../../../prelude/url';
 | 
			
		||||
import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('mobile/views/components/drive.file-detail.vue'),
 | 
			
		||||
	props: ['file'],
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		XFileThumbnail
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			gcd,
 | 
			
		||||
@@ -86,6 +88,10 @@ export default Vue.extend({
 | 
			
		||||
			return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? {
 | 
			
		||||
				'background-color': `rgb(${ this.file.properties.avgColor.join(',') })`
 | 
			
		||||
			} : {};
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		dlUrl(): string {
 | 
			
		||||
			return appendQuery(this.file.url, 'download');
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@@ -142,12 +148,13 @@ export default Vue.extend({
 | 
			
		||||
		padding 8px
 | 
			
		||||
		background var(--bg)
 | 
			
		||||
 | 
			
		||||
		> img
 | 
			
		||||
			display block
 | 
			
		||||
		> .preview
 | 
			
		||||
			max-width 100%
 | 
			
		||||
			max-height 300px
 | 
			
		||||
			margin 0 auto
 | 
			
		||||
			box-shadow 1px 1px 4px rgba(#000, 0.2)
 | 
			
		||||
			overflow hidden
 | 
			
		||||
			color var(--driveFileIcon)
 | 
			
		||||
 | 
			
		||||
		> footer
 | 
			
		||||
			padding 8px 8px 0 8px
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
<a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected">
 | 
			
		||||
	<div class="container">
 | 
			
		||||
		<div class="thumbnail" :style="thumbnail"></div>
 | 
			
		||||
		<x-file-thumbnail class="thumbnail" :file="file" fit="cover"/>
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<p class="name">
 | 
			
		||||
				<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
 | 
			
		||||
@@ -26,9 +26,14 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('mobile/views/components/drive.file.vue'),
 | 
			
		||||
	props: ['file'],
 | 
			
		||||
	components: {
 | 
			
		||||
		XFileThumbnail
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			isSelected: false
 | 
			
		||||
@@ -37,12 +42,6 @@ export default Vue.extend({
 | 
			
		||||
	computed: {
 | 
			
		||||
		browser(): any {
 | 
			
		||||
			return this.$parent;
 | 
			
		||||
		},
 | 
			
		||||
		thumbnail(): any {
 | 
			
		||||
			return {
 | 
			
		||||
				'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
 | 
			
		||||
				'background-image': `url(${this.file.thumbnailUrl})`
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	created() {
 | 
			
		||||
@@ -74,9 +73,12 @@ export default Vue.extend({
 | 
			
		||||
		pointer-events none
 | 
			
		||||
 | 
			
		||||
	> .container
 | 
			
		||||
		display grid
 | 
			
		||||
		max-width 500px
 | 
			
		||||
		margin 0 auto
 | 
			
		||||
		padding 16px
 | 
			
		||||
		grid-template-columns 64px 1fr
 | 
			
		||||
		grid-column-gap 10px
 | 
			
		||||
 | 
			
		||||
		&:after
 | 
			
		||||
			content ""
 | 
			
		||||
@@ -84,18 +86,13 @@ export default Vue.extend({
 | 
			
		||||
			clear both
 | 
			
		||||
 | 
			
		||||
		> .thumbnail
 | 
			
		||||
			display block
 | 
			
		||||
			float left
 | 
			
		||||
			width 64px
 | 
			
		||||
			height 64px
 | 
			
		||||
			background-size cover
 | 
			
		||||
			background-position center center
 | 
			
		||||
			color var(--driveFileIcon)
 | 
			
		||||
 | 
			
		||||
		> .body
 | 
			
		||||
			display block
 | 
			
		||||
			float left
 | 
			
		||||
			width calc(100% - 74px)
 | 
			
		||||
			margin-left 10px
 | 
			
		||||
			word-break break-all
 | 
			
		||||
 | 
			
		||||
			> .name
 | 
			
		||||
				display block
 | 
			
		||||
@@ -104,8 +101,7 @@ export default Vue.extend({
 | 
			
		||||
				font-size 0.9em
 | 
			
		||||
				font-weight bold
 | 
			
		||||
				color var(--text)
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
				overflow-wrap break-word
 | 
			
		||||
				word-break break-word
 | 
			
		||||
 | 
			
		||||
				> .ext
 | 
			
		||||
					opacity 0.5
 | 
			
		||||
@@ -154,6 +150,6 @@ export default Vue.extend({
 | 
			
		||||
		background var(--primary)
 | 
			
		||||
 | 
			
		||||
		&, *
 | 
			
		||||
			color #fff !important
 | 
			
		||||
			color var(--primaryForeground) !important
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -367,7 +367,7 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (this.text && this.text != '') {
 | 
			
		||||
				const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
 | 
			
		||||
				const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag);
 | 
			
		||||
				const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
 | 
			
		||||
				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ const defaultDeviceSettings = {
 | 
			
		||||
	roundedCorners: true,
 | 
			
		||||
	reduceMotion: false,
 | 
			
		||||
	darkmode: true,
 | 
			
		||||
	darkTheme: 'dark',
 | 
			
		||||
	darkTheme: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2',
 | 
			
		||||
	lightTheme: 'light',
 | 
			
		||||
	lineWidth: 1,
 | 
			
		||||
	fontSize: 0,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,26 +10,26 @@ export type Theme = {
 | 
			
		||||
	props: { [key: string]: string };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const lightTheme: Theme = require('../theme/light.json5');
 | 
			
		||||
export const darkTheme: Theme = require('../theme/dark.json5');
 | 
			
		||||
export const pinkTheme: Theme = require('../theme/pink.json5');
 | 
			
		||||
export const blackTheme: Theme = require('../theme/black.json5');
 | 
			
		||||
export const halloweenTheme: Theme = require('../theme/halloween.json5');
 | 
			
		||||
export const cafeTheme: Theme = require('../theme/cafe.json5');
 | 
			
		||||
export const japaneseSushiSetTheme: Theme = require('../theme/japanese-sushi-set.json5');
 | 
			
		||||
export const gruvboxDarkTheme: Theme = require('../theme/gruvbox-dark.json5');
 | 
			
		||||
export const monokaiTheme: Theme = require('../theme/monokai.json5');
 | 
			
		||||
export const colorfulTheme: Theme = require('../theme/colorful.json5');
 | 
			
		||||
export const rainyTheme: Theme = require('../theme/rainy.json5');
 | 
			
		||||
export const mauveTheme: Theme = require('../theme/mauve.json5');
 | 
			
		||||
export const grayTheme: Theme = require('../theme/gray.json5');
 | 
			
		||||
export const tweetDeckTheme: Theme = require('../theme/tweet-deck.json5');
 | 
			
		||||
export const lightTheme: Theme = require('../themes/light.json5');
 | 
			
		||||
export const darkTheme: Theme = require('../themes/dark.json5');
 | 
			
		||||
export const lavenderTheme: Theme = require('../themes/lavender.json5');
 | 
			
		||||
export const futureTheme: Theme = require('../themes/future.json5');
 | 
			
		||||
export const halloweenTheme: Theme = require('../themes/halloween.json5');
 | 
			
		||||
export const cafeTheme: Theme = require('../themes/cafe.json5');
 | 
			
		||||
export const japaneseSushiSetTheme: Theme = require('../themes/japanese-sushi-set.json5');
 | 
			
		||||
export const gruvboxDarkTheme: Theme = require('../themes/gruvbox-dark.json5');
 | 
			
		||||
export const monokaiTheme: Theme = require('../themes/monokai.json5');
 | 
			
		||||
export const colorfulTheme: Theme = require('../themes/colorful.json5');
 | 
			
		||||
export const rainyTheme: Theme = require('../themes/rainy.json5');
 | 
			
		||||
export const mauveTheme: Theme = require('../themes/mauve.json5');
 | 
			
		||||
export const grayTheme: Theme = require('../themes/gray.json5');
 | 
			
		||||
export const tweetDeckTheme: Theme = require('../themes/tweet-deck.json5');
 | 
			
		||||
 | 
			
		||||
export const builtinThemes = [
 | 
			
		||||
	lightTheme,
 | 
			
		||||
	darkTheme,
 | 
			
		||||
	pinkTheme,
 | 
			
		||||
	blackTheme,
 | 
			
		||||
	lavenderTheme,
 | 
			
		||||
	futureTheme,
 | 
			
		||||
	halloweenTheme,
 | 
			
		||||
	cafeTheme,
 | 
			
		||||
	japaneseSushiSetTheme,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	id: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2',
 | 
			
		||||
 | 
			
		||||
	name: 'Future',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
 | 
			
		||||
	base: 'dark',
 | 
			
		||||
 | 
			
		||||
	vars: {
 | 
			
		||||
		primary: 'rgb(94, 158, 185)',
 | 
			
		||||
		secondary: 'rgb(22, 24, 30)',
 | 
			
		||||
		text: 'rgb(214, 218, 224)',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		renoteGradient: '#0a2d3c',
 | 
			
		||||
		renoteText: '$primary',
 | 
			
		||||
		quoteBorder: '$primary',
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
@@ -153,6 +153,8 @@
 | 
			
		||||
		messagingRoomMessageBg: '$secondary',
 | 
			
		||||
		messagingRoomMessageFg: '#fff',
 | 
			
		||||
 | 
			
		||||
		driveFileIcon: '$text',
 | 
			
		||||
 | 
			
		||||
		formButtonBorder: 'rgba(255, 255, 255, 0.1)',
 | 
			
		||||
		formButtonHoverBg: ':alpha<0.2<$primary',
 | 
			
		||||
		formButtonHoverBorder: ':alpha<0.5<$primary',
 | 
			
		||||
							
								
								
									
										39
									
								
								src/client/themes/future.json5
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/client/themes/future.json5
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
{
 | 
			
		||||
	id: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2',
 | 
			
		||||
 | 
			
		||||
	name: 'Future',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	desc: 'Sci-fi flavored',
 | 
			
		||||
 | 
			
		||||
	base: 'dark',
 | 
			
		||||
 | 
			
		||||
	vars: {
 | 
			
		||||
		c0: '#0e0e0e',
 | 
			
		||||
		c1: 'rgb(255, 105, 78)',
 | 
			
		||||
		c2: 'rgb(99, 197, 210)',
 | 
			
		||||
		c4: 'rgb(253, 254, 214)',
 | 
			
		||||
		c3: 'rgb(204, 254, 253)',
 | 
			
		||||
		primary: '$c1',
 | 
			
		||||
		secondary: '#191919',
 | 
			
		||||
		text: '$c3',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		bg: '$c0',
 | 
			
		||||
		noteText: '$c4',
 | 
			
		||||
		noteHeaderAcct: ':alpha<0.65<$c4',
 | 
			
		||||
		noteHeaderInfo: ':alpha<0.5<$c4',
 | 
			
		||||
		subNoteText: ':alpha<0.7<$c4',
 | 
			
		||||
		renoteGradient: 'rgba(0, 0, 0, 0)',
 | 
			
		||||
		renoteText: '$c2',
 | 
			
		||||
		quoteBorder: '$c2',
 | 
			
		||||
		mfmHashtag: '$c1',
 | 
			
		||||
		mfmUrl: '$c2',
 | 
			
		||||
		mfmLink: '$c2',
 | 
			
		||||
		mfmMention: '$c1',
 | 
			
		||||
		mfmMentionForeground: '#fff',
 | 
			
		||||
		notificationIndicator: '$c2',
 | 
			
		||||
		link: '$c2',
 | 
			
		||||
		desktopHeaderBg: '$secondary',
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
@@ -153,6 +153,8 @@
 | 
			
		||||
		messagingRoomMessageBg: '#eee',
 | 
			
		||||
		messagingRoomMessageFg: '#333',
 | 
			
		||||
 | 
			
		||||
		driveFileIcon: '$text',
 | 
			
		||||
 | 
			
		||||
		formButtonBorder: 'rgba(0, 0, 0, 0.1)',
 | 
			
		||||
		formButtonHoverBg: ':alpha<0.12<$primary',
 | 
			
		||||
		formButtonHoverBorder: ':alpha<0.3<$primary',
 | 
			
		||||
@@ -179,7 +181,7 @@
 | 
			
		||||
		desktopTimelineSrcHover: ':darken<7<$text',
 | 
			
		||||
		desktopWindowTitle: '$text',
 | 
			
		||||
		desktopWindowShadow: 'rgba(0, 0, 0, 0.2)',
 | 
			
		||||
		desktopDriveBg: '#fff',
 | 
			
		||||
		desktopDriveBg: '@bg',
 | 
			
		||||
		desktopDriveFolderBg: ':lighten<31<$primary',
 | 
			
		||||
		desktopDriveFolderHoverBg: ':lighten<27<$primary',
 | 
			
		||||
		desktopDriveFolderActiveBg: ':lighten<25<$primary',
 | 
			
		||||
@@ -4,7 +4,7 @@ import { deliverQueue, inboxQueue } from '../queue';
 | 
			
		||||
 | 
			
		||||
const ev = new Xev();
 | 
			
		||||
 | 
			
		||||
const interval = 1000;
 | 
			
		||||
const interval = 3000;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Report queue stats regularly
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
 | 
			
		||||
import { URL } from 'url';
 | 
			
		||||
import { urlRegex } from './prelude';
 | 
			
		||||
 | 
			
		||||
export function fromHtml(html: string): string {
 | 
			
		||||
	if (html == null) return null;
 | 
			
		||||
@@ -14,7 +15,7 @@ export function fromHtml(html: string): string {
 | 
			
		||||
 | 
			
		||||
	return text.trim();
 | 
			
		||||
 | 
			
		||||
	function getText(node: any) {
 | 
			
		||||
	function getText(node: any): string {
 | 
			
		||||
		if (node.nodeName == '#text') return node.value;
 | 
			
		||||
 | 
			
		||||
		if (node.childNodes) {
 | 
			
		||||
@@ -38,10 +39,11 @@ export function fromHtml(html: string): string {
 | 
			
		||||
				const txt = getText(node);
 | 
			
		||||
				const rel = node.attrs.find((x: any) => x.name == 'rel');
 | 
			
		||||
				const href = node.attrs.find((x: any) => x.name == 'href');
 | 
			
		||||
				const isHashtag = rel && rel.value.match('tag') !== null;
 | 
			
		||||
 | 
			
		||||
				// ハッシュタグ / hrefがない / txtがURL
 | 
			
		||||
				if ((rel && rel.value.match('tag') !== null) || !href || href.value == txt) {
 | 
			
		||||
					text += txt;
 | 
			
		||||
				if (isHashtag || !href || href.value == txt) {
 | 
			
		||||
					text += isHashtag || txt.match(urlRegex) ? txt : `<${txt}>`;
 | 
			
		||||
				// メンション
 | 
			
		||||
				} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
 | 
			
		||||
					const part = txt.split('@');
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
import * as A from '../prelude/array';
 | 
			
		||||
import * as S from '../prelude/string';
 | 
			
		||||
import { MfmForest, MfmTree } from './types';
 | 
			
		||||
import { MfmForest, MfmTree } from './prelude';
 | 
			
		||||
import { createTree, createLeaf } from '../prelude/tree';
 | 
			
		||||
 | 
			
		||||
function isEmptyTextTree(t: MfmTree): boolean {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { mfmLanguage } from './language';
 | 
			
		||||
import { MfmForest } from './types';
 | 
			
		||||
import { MfmForest } from './prelude';
 | 
			
		||||
import { normalize } from './normalize';
 | 
			
		||||
 | 
			
		||||
export function parse(source: string): MfmForest {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,3 +35,5 @@ export function createLeaf(type: string, props: any): MfmTree {
 | 
			
		||||
export function createTree(type: string, children: MfmForest, props: any): MfmTree {
 | 
			
		||||
	return T.createTree({ type, props }, children);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const urlRegex = /^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/;
 | 
			
		||||
@@ -2,7 +2,7 @@ import { JSDOM } from 'jsdom';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { INote } from '../models/note';
 | 
			
		||||
import { intersperse } from '../prelude/array';
 | 
			
		||||
import { MfmForest, MfmTree } from './types';
 | 
			
		||||
import { MfmForest, MfmTree } from './prelude';
 | 
			
		||||
 | 
			
		||||
export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) {
 | 
			
		||||
	if (tokens == null) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								src/misc/content-disposition.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/misc/content-disposition.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
const cd = require('content-disposition');
 | 
			
		||||
 | 
			
		||||
export function contentDisposition(type: 'inline' | 'attachment', filename: string): string {
 | 
			
		||||
	const fallback = filename.replace(/[^\w.-]/g, '_');
 | 
			
		||||
	return cd(filename, { type, fallback });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/misc/create-temp.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/misc/create-temp.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
 | 
			
		||||
export function createTemp(): Promise<[string, any]> {
 | 
			
		||||
	return new Promise<[string, any]>((res, rej) => {
 | 
			
		||||
		tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								src/misc/detect-mine.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/misc/detect-mine.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import fileType from 'file-type';
 | 
			
		||||
import checkSvg from '../misc/check-svg';
 | 
			
		||||
 | 
			
		||||
export async function detectMine(path: string) {
 | 
			
		||||
	return new Promise<[string, string]>((res, rej) => {
 | 
			
		||||
		const readable = fs.createReadStream(path);
 | 
			
		||||
		readable
 | 
			
		||||
			.on('error', rej)
 | 
			
		||||
			.once('data', (buffer: Buffer) => {
 | 
			
		||||
				readable.destroy();
 | 
			
		||||
				const type = fileType(buffer);
 | 
			
		||||
				if (type) {
 | 
			
		||||
					if (type.mime == 'application/xml' && checkSvg(path)) {
 | 
			
		||||
						res(['image/svg+xml', 'svg']);
 | 
			
		||||
					} else {
 | 
			
		||||
						res([type.mime, type.ext]);
 | 
			
		||||
					}
 | 
			
		||||
				} else if (checkSvg(path)) {
 | 
			
		||||
					res(['image/svg+xml', 'svg']);
 | 
			
		||||
				} else {
 | 
			
		||||
					// 種類が同定できなかったら application/octet-stream にする
 | 
			
		||||
					res(['application/octet-stream', null]);
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.on('end', () => {
 | 
			
		||||
				// maybe 0 bytes
 | 
			
		||||
				res(['application/octet-stream', null]);
 | 
			
		||||
			});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/misc/detect-url-mine.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/misc/detect-url-mine.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import { createTemp } from './create-temp';
 | 
			
		||||
import { downloadUrl } from './donwload-url';
 | 
			
		||||
import { detectMine } from './detect-mine';
 | 
			
		||||
 | 
			
		||||
export async function detectUrlMine(url: string) {
 | 
			
		||||
	const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
	try {
 | 
			
		||||
		await downloadUrl(url, path);
 | 
			
		||||
		const [type] = await detectMine(path);
 | 
			
		||||
		return type;
 | 
			
		||||
	} finally {
 | 
			
		||||
		cleanup();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								src/misc/donwload-url.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/misc/donwload-url.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as URL from 'url';
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import chalk from 'chalk';
 | 
			
		||||
import Logger from '../services/logger';
 | 
			
		||||
 | 
			
		||||
export async function downloadUrl(url: string, path: string) {
 | 
			
		||||
	const logger = new Logger('download-url');
 | 
			
		||||
 | 
			
		||||
	await new Promise((res, rej) => {
 | 
			
		||||
		logger.info(`Downloading ${chalk.cyan(url)} ...`);
 | 
			
		||||
 | 
			
		||||
		const writable = fs.createWriteStream(path);
 | 
			
		||||
 | 
			
		||||
		writable.on('finish', () => {
 | 
			
		||||
			logger.succ(`Download finished: ${chalk.cyan(url)}`);
 | 
			
		||||
			res();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		writable.on('error', error => {
 | 
			
		||||
			logger.error(`Download failed: ${chalk.cyan(url)}: ${error}`, {
 | 
			
		||||
				url: url,
 | 
			
		||||
				e: error
 | 
			
		||||
			});
 | 
			
		||||
			rej(error);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const requestUrl = URL.parse(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url;
 | 
			
		||||
 | 
			
		||||
		const req = request({
 | 
			
		||||
			url: requestUrl,
 | 
			
		||||
			proxy: config.proxy,
 | 
			
		||||
			timeout: 10 * 1000,
 | 
			
		||||
			headers: {
 | 
			
		||||
				'User-Agent': config.userAgent
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		req.pipe(writable);
 | 
			
		||||
 | 
			
		||||
		req.on('response', response => {
 | 
			
		||||
			if (response.statusCode !== 200) {
 | 
			
		||||
				logger.error(`Got ${response.statusCode} (${url})`);
 | 
			
		||||
				writable.close();
 | 
			
		||||
				rej(response.statusCode);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		req.on('error', error => {
 | 
			
		||||
			logger.error(`Failed to start download: ${chalk.cyan(url)}: ${error}`, {
 | 
			
		||||
				url: url,
 | 
			
		||||
				e: error
 | 
			
		||||
			});
 | 
			
		||||
			writable.close();
 | 
			
		||||
			rej(error);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		logger.succ(`Downloaded to: ${path}`);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@@ -1,79 +1,25 @@
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as util from 'util';
 | 
			
		||||
import chalk from 'chalk';
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
import Logger from '../services/logger';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { createTemp } from './create-temp';
 | 
			
		||||
import { downloadUrl } from './donwload-url';
 | 
			
		||||
 | 
			
		||||
const logger = new Logger('download-text-file');
 | 
			
		||||
 | 
			
		||||
export async function downloadTextFile(url: string): Promise<string> {
 | 
			
		||||
	// Create temp file
 | 
			
		||||
	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
 | 
			
		||||
		tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
	logger.info(`Temp file is ${path}`);
 | 
			
		||||
 | 
			
		||||
	// write content at URL to temp file
 | 
			
		||||
	await new Promise((res, rej) => {
 | 
			
		||||
		logger.info(`Downloading ${chalk.cyan(url)} ...`);
 | 
			
		||||
	try {
 | 
			
		||||
		// write content at URL to temp file
 | 
			
		||||
		await downloadUrl(url, path);
 | 
			
		||||
 | 
			
		||||
		const writable = fs.createWriteStream(path);
 | 
			
		||||
		const text = await util.promisify(fs.readFile)(path, 'utf8');
 | 
			
		||||
 | 
			
		||||
		writable.on('finish', () => {
 | 
			
		||||
			logger.succ(`Download finished: ${chalk.cyan(url)}`);
 | 
			
		||||
			res();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		writable.on('error', error => {
 | 
			
		||||
			logger.error(`Download failed: ${chalk.cyan(url)}: ${error}`, {
 | 
			
		||||
				url: url,
 | 
			
		||||
				e: error
 | 
			
		||||
			});
 | 
			
		||||
			rej(error);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const requestUrl = new URL(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url;
 | 
			
		||||
 | 
			
		||||
		const req = request({
 | 
			
		||||
			url: requestUrl,
 | 
			
		||||
			proxy: config.proxy,
 | 
			
		||||
			timeout: 10 * 1000,
 | 
			
		||||
			headers: {
 | 
			
		||||
				'User-Agent': config.userAgent
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		req.pipe(writable);
 | 
			
		||||
 | 
			
		||||
		req.on('response', response => {
 | 
			
		||||
			if (response.statusCode !== 200) {
 | 
			
		||||
				logger.error(`Got ${response.statusCode} (${url})`);
 | 
			
		||||
				writable.close();
 | 
			
		||||
				rej(response.statusCode);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		req.on('error', error => {
 | 
			
		||||
			logger.error(`Failed to start download: ${chalk.cyan(url)}: ${error}`, {
 | 
			
		||||
				url: url,
 | 
			
		||||
				e: error
 | 
			
		||||
			});
 | 
			
		||||
			writable.close();
 | 
			
		||||
			rej(error);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Downloaded to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const text = await util.promisify(fs.readFile)(path, 'utf8');
 | 
			
		||||
 | 
			
		||||
	cleanup();
 | 
			
		||||
 | 
			
		||||
	return text;
 | 
			
		||||
		return text;
 | 
			
		||||
	} finally {
 | 
			
		||||
		cleanup();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								src/misc/emoji-regex.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/misc/emoji-regex.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
import { EmojiNode, MfmForest } from '../mfm/types';
 | 
			
		||||
import { EmojiNode, MfmForest } from '../mfm/prelude';
 | 
			
		||||
import { preorderF } from '../prelude/tree';
 | 
			
		||||
import { unique } from '../prelude/array';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { HashtagNode, MfmForest } from '../mfm/types';
 | 
			
		||||
import { HashtagNode, MfmForest } from '../mfm/prelude';
 | 
			
		||||
import { preorderF } from '../prelude/tree';
 | 
			
		||||
import { unique } from '../prelude/array';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
// test is located in test/extract-mentions
 | 
			
		||||
 | 
			
		||||
import { MentionNode, MfmForest } from '../mfm/types';
 | 
			
		||||
import { MentionNode, MfmForest } from '../mfm/prelude';
 | 
			
		||||
import { preorderF } from '../prelude/tree';
 | 
			
		||||
 | 
			
		||||
export default function(mfmForest: MfmForest): MentionNode['props'][] {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ const defaultMeta: any = {
 | 
			
		||||
		originalUsersCount: 0
 | 
			
		||||
	},
 | 
			
		||||
	maxNoteTextLength: 1000,
 | 
			
		||||
	enableEmojiReaction: true,
 | 
			
		||||
	enableTwitterIntegration: false,
 | 
			
		||||
	enableGithubIntegration: false,
 | 
			
		||||
	enableDiscordIntegration: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ export default function(reaction: string): string {
 | 
			
		||||
		case 'confused': return '😥';
 | 
			
		||||
		case 'rip': return '😇';
 | 
			
		||||
		case 'pudding': return '🍮';
 | 
			
		||||
		default: return '';
 | 
			
		||||
		case 'star': return '⭐';
 | 
			
		||||
		default: return reaction;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								src/misc/reaction-lib.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/misc/reaction-lib.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
import Emoji from '../models/emoji';
 | 
			
		||||
import { emojiRegex } from './emoji-regex';
 | 
			
		||||
import fetchMeta from './fetch-meta';
 | 
			
		||||
 | 
			
		||||
const basic10: Record<string, string> = {
 | 
			
		||||
	'👍': 'like',
 | 
			
		||||
	'❤': 'love',	// ここに記述する場合は異体字セレクタを入れない
 | 
			
		||||
	'😆': 'laugh',
 | 
			
		||||
	'🤔': 'hmm',
 | 
			
		||||
	'😮': 'surprise',
 | 
			
		||||
	'🎉': 'congrats',
 | 
			
		||||
	'💢': 'angry',
 | 
			
		||||
	'😥': 'confused',
 | 
			
		||||
	'😇': 'rip',
 | 
			
		||||
	'🍮': 'pudding',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function getFallbackReaction(): Promise<string> {
 | 
			
		||||
	const meta = await fetchMeta();
 | 
			
		||||
	return  meta.useStarForReactionFallback ? 'star' : 'like';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function toDbReaction(reaction: string, enableEmoji = true): Promise<string> {
 | 
			
		||||
	if (reaction == null) return await getFallbackReaction();
 | 
			
		||||
 | 
			
		||||
	// 既存の文字列リアクションはそのまま
 | 
			
		||||
	if (Object.values(basic10).includes(reaction)) return reaction;
 | 
			
		||||
 | 
			
		||||
	if (!enableEmoji) return await getFallbackReaction();
 | 
			
		||||
 | 
			
		||||
	// Unicode絵文字
 | 
			
		||||
	const match = emojiRegex.exec(reaction);
 | 
			
		||||
	if (match) {
 | 
			
		||||
		// 合字を含む1つの絵文字
 | 
			
		||||
		const unicode = match[0];
 | 
			
		||||
 | 
			
		||||
		// 異体字セレクタ除去後の絵文字
 | 
			
		||||
		const normalized = unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
 | 
			
		||||
 | 
			
		||||
		// Unicodeプリンは寿司化不能とするため文字列化しない
 | 
			
		||||
		if (normalized === '🍮') return normalized;
 | 
			
		||||
 | 
			
		||||
		// プリン以外の既存のリアクションは文字列化する
 | 
			
		||||
		if (basic10[normalized]) return basic10[normalized];
 | 
			
		||||
 | 
			
		||||
		// それ以外はUnicodeのまま
 | 
			
		||||
		return normalized;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const custom = reaction.match(/^:([\w+-]+):$/);
 | 
			
		||||
	if (custom) {
 | 
			
		||||
		const emoji = await Emoji.findOne({
 | 
			
		||||
			host: null,
 | 
			
		||||
			name: custom[1],
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (emoji) return reaction;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return await getFallbackReaction();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								src/misc/twemoji-base.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/misc/twemoji-base.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
export const twemojiBase = 'https://cdn.jsdelivr.net/npm/twemoji@11.3.0';
 | 
			
		||||
// https://cdn.jsdelivr.net/npm/twemoji@11.3.0
 | 
			
		||||
// https://cdnjs.cloudflare.com/ajax/libs/twemoji/11.3.0
 | 
			
		||||
// https://twemoji.maxcdn.com
 | 
			
		||||
@@ -17,4 +17,5 @@ export type IEmoji = {
 | 
			
		||||
	updatedAt?: Date;
 | 
			
		||||
	/** AP object id */
 | 
			
		||||
	uri?: string;
 | 
			
		||||
	type?: string;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -194,6 +194,8 @@ export type IMeta = {
 | 
			
		||||
	disableRegistration?: boolean;
 | 
			
		||||
	disableLocalTimeline?: boolean;
 | 
			
		||||
	disableGlobalTimeline?: boolean;
 | 
			
		||||
	enableEmojiReaction?: boolean;
 | 
			
		||||
	useStarForReactionFallback?: boolean;
 | 
			
		||||
	hidedTags?: string[];
 | 
			
		||||
	mascotImageUrl?: string;
 | 
			
		||||
	bannerUrl?: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
import * as mongo from 'mongodb';
 | 
			
		||||
import $ from 'cafy';
 | 
			
		||||
import * as deepcopy from 'deepcopy';
 | 
			
		||||
import db from '../db/mongodb';
 | 
			
		||||
import isObjectId from '../misc/is-objectid';
 | 
			
		||||
import Reaction from './note-reaction';
 | 
			
		||||
import { pack as packUser } from './user';
 | 
			
		||||
 | 
			
		||||
const NoteReaction = db.get<INoteReaction>('noteReactions');
 | 
			
		||||
@@ -20,19 +18,6 @@ export interface INoteReaction {
 | 
			
		||||
	reaction: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const validateReaction = $.str.or([
 | 
			
		||||
	'like',
 | 
			
		||||
	'love',
 | 
			
		||||
	'laugh',
 | 
			
		||||
	'hmm',
 | 
			
		||||
	'surprise',
 | 
			
		||||
	'congrats',
 | 
			
		||||
	'angry',
 | 
			
		||||
	'confused',
 | 
			
		||||
	'rip',
 | 
			
		||||
	'pudding'
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Pack a reaction for API response
 | 
			
		||||
 */
 | 
			
		||||
@@ -44,11 +29,11 @@ export const pack = (
 | 
			
		||||
 | 
			
		||||
	// Populate the reaction if 'reaction' is ID
 | 
			
		||||
	if (isObjectId(reaction)) {
 | 
			
		||||
		_reaction = await Reaction.findOne({
 | 
			
		||||
		_reaction = await NoteReaction.findOne({
 | 
			
		||||
			_id: reaction
 | 
			
		||||
		});
 | 
			
		||||
	} else if (typeof reaction === 'string') {
 | 
			
		||||
		_reaction = await Reaction.findOne({
 | 
			
		||||
		_reaction = await NoteReaction.findOne({
 | 
			
		||||
			_id: new mongo.ObjectID(reaction)
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,11 +7,12 @@ import { length } from 'stringz';
 | 
			
		||||
import { IUser, pack as packUser } from './user';
 | 
			
		||||
import { pack as packApp } from './app';
 | 
			
		||||
import PollVote from './poll-vote';
 | 
			
		||||
import Reaction from './note-reaction';
 | 
			
		||||
import NoteReaction from './note-reaction';
 | 
			
		||||
import { packMany as packFileMany, IDriveFile } from './drive-file';
 | 
			
		||||
import Following from './following';
 | 
			
		||||
import Emoji from './emoji';
 | 
			
		||||
import { dbLogger } from '../db/logger';
 | 
			
		||||
import { unique, concat } from '../prelude/array';
 | 
			
		||||
 | 
			
		||||
const Note = db.get<INote>('notes');
 | 
			
		||||
Note.createIndex('uri', { sparse: true, unique: true });
 | 
			
		||||
@@ -41,6 +42,7 @@ export type INote = {
 | 
			
		||||
	replyId: mongo.ObjectID;
 | 
			
		||||
	renoteId: mongo.ObjectID;
 | 
			
		||||
	poll: IPoll;
 | 
			
		||||
	name?: string;
 | 
			
		||||
	text: string;
 | 
			
		||||
	tags: string[];
 | 
			
		||||
	tagsLower: string[];
 | 
			
		||||
@@ -241,6 +243,11 @@ export const pack = async (
 | 
			
		||||
 | 
			
		||||
	const id = _note._id;
 | 
			
		||||
 | 
			
		||||
	// Some counts
 | 
			
		||||
	_note.renoteCount = _note.renoteCount || 0;
 | 
			
		||||
	_note.repliesCount = _note.repliesCount || 0;
 | 
			
		||||
	_note.reactionCounts = _note.reactionCounts || {};
 | 
			
		||||
 | 
			
		||||
	// _note._userを消す前か、_note.userを解決した後でないとホストがわからない
 | 
			
		||||
	if (_note._user) {
 | 
			
		||||
		const host = _note._user.host;
 | 
			
		||||
@@ -252,6 +259,8 @@ export const pack = async (
 | 
			
		||||
				fields: { _id: false }
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			_note.emojis = unique(concat([_note.emojis, Object.keys(_note.reactionCounts).map(x => x.replace(/:/g, ''))]));
 | 
			
		||||
 | 
			
		||||
			_note.emojis = Emoji.find({
 | 
			
		||||
				name: { $in: _note.emojis },
 | 
			
		||||
				host: host
 | 
			
		||||
@@ -289,11 +298,6 @@ export const pack = async (
 | 
			
		||||
	// Populate files
 | 
			
		||||
	_note.files = packFileMany(_note.fileIds || []);
 | 
			
		||||
 | 
			
		||||
	// Some counts
 | 
			
		||||
	_note.renoteCount = _note.renoteCount || 0;
 | 
			
		||||
	_note.repliesCount = _note.repliesCount || 0;
 | 
			
		||||
	_note.reactionCounts = _note.reactionCounts || {};
 | 
			
		||||
 | 
			
		||||
	// 後方互換性のため
 | 
			
		||||
	_note.mediaIds = _note.fileIds;
 | 
			
		||||
	_note.media = _note.files;
 | 
			
		||||
@@ -353,7 +357,7 @@ export const pack = async (
 | 
			
		||||
		if (meId) {
 | 
			
		||||
			// Fetch my reaction
 | 
			
		||||
			_note.myReaction = (async () => {
 | 
			
		||||
				const reaction = await Reaction
 | 
			
		||||
				const reaction = await NoteReaction
 | 
			
		||||
					.findOne({
 | 
			
		||||
						userId: meId,
 | 
			
		||||
						noteId: id,
 | 
			
		||||
@@ -391,6 +395,10 @@ export const pack = async (
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	if (_note.name) {
 | 
			
		||||
		_note.text = `【${_note.name}】\n${_note.text}`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (_note.user.isCat && _note.text) {
 | 
			
		||||
		_note.text = (_note.text
 | 
			
		||||
			// ja-JP
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ User.createIndex('createdAt');
 | 
			
		||||
User.createIndex('updatedAt');
 | 
			
		||||
User.createIndex('followersCount');
 | 
			
		||||
User.createIndex('tags');
 | 
			
		||||
User.createIndex('isSuspended');
 | 
			
		||||
User.createIndex('username');
 | 
			
		||||
User.createIndex('usernameLower');
 | 
			
		||||
User.createIndex('host');
 | 
			
		||||
 
 | 
			
		||||
@@ -5,3 +5,7 @@ export function query(obj: {}): string {
 | 
			
		||||
		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
 | 
			
		||||
		.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function appendQuery(url: string, query: string): string {
 | 
			
		||||
	return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -178,10 +178,10 @@ export function destroy() {
 | 
			
		||||
	deliverQueue.once('cleaned', (jobs, status) => {
 | 
			
		||||
		deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
 | 
			
		||||
	});
 | 
			
		||||
	deliverQueue.clean(0, 'wait');
 | 
			
		||||
	deliverQueue.clean(0, 'delayed');
 | 
			
		||||
 | 
			
		||||
	inboxQueue.once('cleaned', (jobs, status) => {
 | 
			
		||||
		inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
 | 
			
		||||
	});
 | 
			
		||||
	inboxQueue.clean(0, 'wait');
 | 
			
		||||
	inboxQueue.clean(0, 'delayed');
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,10 +24,8 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
 | 
			
		||||
 | 
			
		||||
	switch (object.type) {
 | 
			
		||||
	case 'Note':
 | 
			
		||||
		announceNote(resolver, actor, activity, object as INote);
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
	case 'Question':
 | 
			
		||||
	case 'Article':
 | 
			
		||||
		announceNote(resolver, actor, activity, object as INote);
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,10 +29,8 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
	case 'Note':
 | 
			
		||||
		createNote(resolver, actor, object);
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
	case 'Question':
 | 
			
		||||
	case 'Article':
 | 
			
		||||
		createNote(resolver, actor, object);
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,8 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
 | 
			
		||||
 | 
			
		||||
	switch (object.type) {
 | 
			
		||||
	case 'Note':
 | 
			
		||||
		deleteNote(actor, uri);
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
	case 'Question':
 | 
			
		||||
	case 'Article':
 | 
			
		||||
		deleteNote(actor, uri);
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ import Note from '../../../models/note';
 | 
			
		||||
import { IRemoteUser } from '../../../models/user';
 | 
			
		||||
import { ILike } from '../type';
 | 
			
		||||
import create from '../../../services/note/reaction/create';
 | 
			
		||||
import { validateReaction } from '../../../models/note-reaction';
 | 
			
		||||
 | 
			
		||||
export default async (actor: IRemoteUser, activity: ILike) => {
 | 
			
		||||
	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
 | 
			
		||||
@@ -18,12 +17,5 @@ export default async (actor: IRemoteUser, activity: ILike) => {
 | 
			
		||||
		throw new Error();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let reaction = 'like';
 | 
			
		||||
 | 
			
		||||
	// 他のMisskeyインスタンスからのリアクション
 | 
			
		||||
	if (activity._misskey_reaction && validateReaction.ok(activity._misskey_reaction)) {
 | 
			
		||||
		reaction = activity._misskey_reaction;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	await create(actor, note, reaction);
 | 
			
		||||
	await create(actor, note, activity._misskey_reaction);
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
import { remoteLogger } from "../logger";
 | 
			
		||||
import { remoteLogger } from '../logger';
 | 
			
		||||
 | 
			
		||||
export const apLogger = remoteLogger.createSubLogger('ap', 'magenta');
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 | 
			
		||||
 | 
			
		||||
	const object: any = await resolver.resolve(value);
 | 
			
		||||
 | 
			
		||||
	if (!object || !['Note', 'Question'].includes(object.type)) {
 | 
			
		||||
	if (!object || !['Note', 'Question', 'Article'].includes(object.type)) {
 | 
			
		||||
		logger.error(`invalid note: ${value}`, {
 | 
			
		||||
			resolver: {
 | 
			
		||||
				history: resolver.getHistory()
 | 
			
		||||
@@ -199,6 +199,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 | 
			
		||||
		files,
 | 
			
		||||
		reply,
 | 
			
		||||
		renote: quote,
 | 
			
		||||
		name: note.name,
 | 
			
		||||
		cw,
 | 
			
		||||
		text,
 | 
			
		||||
		viaMobile: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -294,6 +294,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
 | 
			
		||||
	await User.update({ _id: exist._id }, {
 | 
			
		||||
		$set: {
 | 
			
		||||
			lastFetchedAt: new Date(),
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (resolver == null) resolver = new Resolver();
 | 
			
		||||
 | 
			
		||||
	const object = hint || await resolver.resolve(uri) as any;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export default (emoji: IEmoji) => ({
 | 
			
		||||
	updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString,
 | 
			
		||||
	icon: {
 | 
			
		||||
		type: 'Image',
 | 
			
		||||
		mediaType: 'image/png',	//Mei-TODO
 | 
			
		||||
		mediaType: emoji.type || 'image/png',
 | 
			
		||||
		url: emoji.url
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import Following from './activitypub/following';
 | 
			
		||||
import Featured from './activitypub/featured';
 | 
			
		||||
import renderQuestion from '../remote/activitypub/renderer/question';
 | 
			
		||||
import { inbox as processInbox } from '../queue';
 | 
			
		||||
import { isSelfHost } from '../misc/convert-host';
 | 
			
		||||
 | 
			
		||||
// Init router
 | 
			
		||||
const router = new Router();
 | 
			
		||||
@@ -79,6 +80,16 @@ router.get('/notes/:note', async (ctx, next) => {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// リモートだったらリダイレクト
 | 
			
		||||
	if (note._user.host != null) {
 | 
			
		||||
		if (note.uri == null || isSelfHost(note._user.host)) {
 | 
			
		||||
			ctx.status = 500;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		ctx.redirect(note.uri);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.body = renderActivity(await renderNote(note, false));
 | 
			
		||||
	ctx.set('Cache-Control', 'public, max-age=180');
 | 
			
		||||
	setResponseType(ctx);
 | 
			
		||||
@@ -93,6 +104,7 @@ router.get('/notes/:note/activity', async ctx => {
 | 
			
		||||
 | 
			
		||||
	const note = await Note.findOne({
 | 
			
		||||
		_id: new ObjectID(ctx.params.note),
 | 
			
		||||
		'_user.host': null,
 | 
			
		||||
		visibility: { $in: ['public', 'home'] },
 | 
			
		||||
		localOnly: { $ne: true }
 | 
			
		||||
	});
 | 
			
		||||
@@ -116,6 +128,7 @@ router.get('/questions/:question', async (ctx, next) => {
 | 
			
		||||
 | 
			
		||||
	const poll = await Note.findOne({
 | 
			
		||||
		_id: new ObjectID(ctx.params.question),
 | 
			
		||||
		'_user.host': null,
 | 
			
		||||
		visibility: { $in: ['public', 'home'] },
 | 
			
		||||
		localOnly: { $ne: true },
 | 
			
		||||
		poll: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import $ from 'cafy';
 | 
			
		||||
import Emoji from '../../../../../models/emoji';
 | 
			
		||||
import define from '../../../define';
 | 
			
		||||
import { detectUrlMine } from '../../../../../misc/detect-url-mine';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	desc: {
 | 
			
		||||
@@ -29,12 +30,15 @@ export const meta = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default define(meta, async (ps) => {
 | 
			
		||||
	const type = await detectUrlMine(ps.url);
 | 
			
		||||
 | 
			
		||||
	const emoji = await Emoji.insert({
 | 
			
		||||
		updatedAt: new Date(),
 | 
			
		||||
		name: ps.name,
 | 
			
		||||
		host: null,
 | 
			
		||||
		aliases: ps.aliases,
 | 
			
		||||
		url: ps.url
 | 
			
		||||
		url: ps.url,
 | 
			
		||||
		type,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import $ from 'cafy';
 | 
			
		||||
import Emoji from '../../../../../models/emoji';
 | 
			
		||||
import define from '../../../define';
 | 
			
		||||
import ID from '../../../../../misc/cafy-id';
 | 
			
		||||
import { detectUrlMine } from '../../../../../misc/detect-url-mine';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	desc: {
 | 
			
		||||
@@ -39,12 +40,15 @@ export default define(meta, async (ps) => {
 | 
			
		||||
 | 
			
		||||
	if (emoji == null) throw new Error('emoji not found');
 | 
			
		||||
 | 
			
		||||
	const type = await detectUrlMine(ps.url);
 | 
			
		||||
 | 
			
		||||
	await Emoji.update({ _id: emoji._id }, {
 | 
			
		||||
		$set: {
 | 
			
		||||
			updatedAt: new Date(),
 | 
			
		||||
			name: ps.name,
 | 
			
		||||
			aliases: ps.aliases,
 | 
			
		||||
			url: ps.url
 | 
			
		||||
			url: ps.url,
 | 
			
		||||
			type,
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								src/server/api/endpoints/admin/queue/jobs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/server/api/endpoints/admin/queue/jobs.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import $ from 'cafy';
 | 
			
		||||
import define from '../../../define';
 | 
			
		||||
import { deliverQueue, inboxQueue } from '../../../../../queue';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requireModerator: true,
 | 
			
		||||
 | 
			
		||||
	params: {
 | 
			
		||||
		domain: {
 | 
			
		||||
			validator: $.str,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		state: {
 | 
			
		||||
			validator: $.str,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		limit: {
 | 
			
		||||
			validator: $.optional.num,
 | 
			
		||||
			default: 50
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default define(meta, async (ps) => {
 | 
			
		||||
	const queue =
 | 
			
		||||
		ps.domain === 'deliver' ? deliverQueue :
 | 
			
		||||
		ps.domain === 'inbox' ? inboxQueue :
 | 
			
		||||
		null;
 | 
			
		||||
 | 
			
		||||
	const jobs = await queue.getJobs([ps.state], 0, ps.limit);
 | 
			
		||||
 | 
			
		||||
	return jobs.map(job => ({
 | 
			
		||||
		id: job.id,
 | 
			
		||||
		data: job.data,
 | 
			
		||||
		attempts: job.attemptsMade,
 | 
			
		||||
	}));
 | 
			
		||||
});
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user