Compare commits
	
		
			71 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 71bada97df | ||
|   | 62509edcbe | ||
|   | f97cdfaa20 | ||
|   | 67ec10e86d | ||
|   | 7d599a68ea | ||
|   | 7ccff732b8 | ||
|   | 7587c896d5 | ||
|   | 91297f1ab3 | ||
|   | d872a16fe0 | ||
|   | 60aa35adf8 | ||
|   | 5035b66773 | ||
|   | fa9da8ecab | ||
|   | 1f9bca7188 | ||
|   | ffa5bdeb50 | ||
|   | e6bfb7398e | ||
|   | 6def0c776f | ||
|   | 24bae9eaed | ||
|   | fb5175a283 | ||
|   | 6e49437154 | ||
|   | 2511ed56ac | ||
|   | c4bfc99cf5 | ||
|   | 4efe38440d | ||
|   | 4a5f2c3c40 | ||
|   | 109738ccb9 | ||
|   | 433dbe179d | ||
|   | b21b33831a | ||
|   | 020cc471da | ||
|   | 43b47c4494 | ||
|   | 8751d91794 | ||
|   | 374b276f5c | ||
|   | 6138a74231 | ||
|   | 25438c4d64 | ||
|   | ae6ce19886 | ||
|   | e17a9bfd6f | ||
|   | dc2055f5bc | ||
|   | afeb8058b1 | ||
|   | 9299f99ac3 | ||
|   | 858fc7ebcc | ||
|   | 35089c65d3 | ||
|   | 643ca42829 | ||
|   | 935dc4fe33 | ||
|   | 3a9e74feb1 | ||
|   | 92e66fbf0c | ||
|   | a50515f569 | ||
|   | 2f8f47acea | ||
|   | dcb296db93 | ||
|   | 0bdae9ede7 | ||
|   | 11290c2a0f | ||
|   | 428b8f8669 | ||
|   | 7ced10f84e | ||
|   | 8ac54139c9 | ||
|   | 32afe77a26 | ||
|   | 6db8e33662 | ||
|   | 569561f247 | ||
|   | d132d82acf | ||
|   | 9ba0db9372 | ||
|   | 5d468b542d | ||
|   | 32273165c7 | ||
|   | 46fdb75bf4 | ||
|   | baf381814b | ||
|   | e90387c14e | ||
|   | 876790d499 | ||
|   | 8b56edda4b | ||
|   | 33352256d6 | ||
|   | e368ef11fa | ||
|   | 045f7c3185 | ||
|   | bf40e5a5c5 | ||
|   | cda3635d97 | ||
|   | 2eb561f132 | ||
|   | b5f6465d61 | ||
|   | 9725076c46 | 
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "ホーム" |     home: "ホーム" | ||||||
|     local: "ローカル" |     local: "ローカル" | ||||||
|     hybrid: "ソーシャル" |     hybrid: "ソーシャル" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "グローバル" |     global: "グローバル" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "通知" |     notifications: "通知" | ||||||
|     list: "リスト" |     list: "リスト" | ||||||
|     swap-left: "左に移動" |     swap-left: "左に移動" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "Startseite" |     home: "Startseite" | ||||||
|     local: "Lokal" |     local: "Lokal" | ||||||
|     hybrid: "ソーシャル" |     hybrid: "ソーシャル" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "Global" |     global: "Global" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "Mitteilungen" |     notifications: "Mitteilungen" | ||||||
|     list: "Listen" |     list: "Listen" | ||||||
|     swap-left: "Nach links" |     swap-left: "Nach links" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "Lokal" |   local: "Lokal" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "Global" |   global: "Global" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "Listen" |   list: "Listen" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "Do not omit the hostname from the username" |   show-full-acct: "Do not omit the hostname from the username" | ||||||
|   enable-animations: "Enable animations" |   reduce-motion: "Reduce motion in UI" | ||||||
|   this-setting-is-this-device-only: "Only for this device" |   this-setting-is-this-device-only: "Only for this device" | ||||||
|   do-not-use-in-production: 'As this is for development, do not use this in production.' |   do-not-use-in-production: 'As this is for development, do not use this in production.' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "Home" |     home: "Home" | ||||||
|     local: "Local" |     local: "Local" | ||||||
|     hybrid: "Social" |     hybrid: "Social" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "Global" |     global: "Global" | ||||||
|  |     mentions: "Mentions" | ||||||
|     notifications: "Notifications" |     notifications: "Notifications" | ||||||
|     list: "Lists" |     list: "Lists" | ||||||
|     swap-left: "Move to the left" |     swap-left: "Move to the left" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "Local" |   local: "Local" | ||||||
|   hybrid: "Social" |   hybrid: "Social" | ||||||
|   global: "Global" |   global: "Global" | ||||||
|  |   mentions: "Mentions" | ||||||
|   list: "Lists" |   list: "Lists" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "Welcome back," |   welcome-back: "Welcome back," | ||||||
|   adjective: "-san" |   adjective: "-san" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "Local" |   local: "Local" | ||||||
|   hybrid: "Social" |   hybrid: "Social" | ||||||
|   global: "Global" |   global: "Global" | ||||||
|  |   mentions: "Mentions" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "No posts \"{}\" found." |   no-posts-found: "No posts \"{}\" found." | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'Esto está en desarrollo, no usarlo para producción.' |   do-not-use-in-production: 'Esto está en desarrollo, no usarlo para producción.' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "Inicio" |     home: "Inicio" | ||||||
|     local: "Local" |     local: "Local" | ||||||
|     hybrid: "Social" |     hybrid: "Social" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "Global" |     global: "Global" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "Notificaciones" |     notifications: "Notificaciones" | ||||||
|     list: "Listado" |     list: "Listado" | ||||||
|     swap-left: "Desplazar a la izq." |     swap-left: "Desplazar a la izq." | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "Bienvenido/a de vuelta," |   welcome-back: "Bienvenido/a de vuelta," | ||||||
|   adjective: "-san" |   adjective: "-san" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "Réduire les animations dans l’interface utilisateur" | ||||||
|   this-setting-is-this-device-only: "Uniquement sur cet appareil" |   this-setting-is-this-device-only: "Uniquement sur cet appareil" | ||||||
|   do-not-use-in-production: 'Il s’agit d’une version de développement. Ne pas utiliser dans un environnement de production.' |   do-not-use-in-production: 'Il s’agit d’une version de développement. Ne pas utiliser dans un environnement de production.' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "Accueil" |     home: "Accueil" | ||||||
|     local: "Local" |     local: "Local" | ||||||
|     hybrid: "Social" |     hybrid: "Social" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "Global" |     global: "Global" | ||||||
|  |     mentions: "Mentions" | ||||||
|     notifications: "Notifications" |     notifications: "Notifications" | ||||||
|     list: "Liste" |     list: "Liste" | ||||||
|     swap-left: "Déplacer à gauche" |     swap-left: "Déplacer à gauche" | ||||||
| @@ -259,8 +261,8 @@ common/views/components/connect-failed.troubleshooter.vue: | |||||||
|   flush: "Vider le cache" |   flush: "Vider le cache" | ||||||
|   set-version: "Choisissez une version" |   set-version: "Choisissez une version" | ||||||
| common/views/components/media-banner.vue: | common/views/components/media-banner.vue: | ||||||
|   sensitive: "閲覧注意" |   sensitive: "Contenu sensible" | ||||||
|   click-to-show: "クリックして表示" |   click-to-show: "Cliquer pour afficher" | ||||||
| common/views/components/cw-button.vue: | common/views/components/cw-button.vue: | ||||||
|   hide: "Masquer" |   hide: "Masquer" | ||||||
|   show: "Voir plus" |   show: "Voir plus" | ||||||
| @@ -483,7 +485,7 @@ desktop/views/components/charts.vue: | |||||||
|     drive-files-total: "ドライブのファイル数の累計" |     drive-files-total: "ドライブのファイル数の累計" | ||||||
|     network-requests: "Requêtes" |     network-requests: "Requêtes" | ||||||
|     network-time: "Temps de réponse" |     network-time: "Temps de réponse" | ||||||
|     network-usage: "通信量" |     network-usage: "Traffic" | ||||||
| desktop/views/components/choose-file-from-drive-window.vue: | desktop/views/components/choose-file-from-drive-window.vue: | ||||||
|   choose-file: "Sélection de fichiers" |   choose-file: "Sélection de fichiers" | ||||||
|   upload: "Téléverser des fichiers à partir de votre ordinateur" |   upload: "Téléverser des fichiers à partir de votre ordinateur" | ||||||
| @@ -790,7 +792,7 @@ desktop/views/components/settings.profile.vue: | |||||||
|   birthday: "Date de naissance" |   birthday: "Date de naissance" | ||||||
|   save: "Mettre à jour le profil" |   save: "Mettre à jour le profil" | ||||||
|   locked-account: "Protéger votre compte" |   locked-account: "Protéger votre compte" | ||||||
|   is-locked: "フォローを承認制にする" |   is-locked: "Demande d’abonnement en attente d’approbation" | ||||||
|   other: "Autre" |   other: "Autre" | ||||||
|   is-bot: "Ce compte est un Bot" |   is-bot: "Ce compte est un Bot" | ||||||
|   is-cat: "Ce compte est un Chat" |   is-cat: "Ce compte est un Chat" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "Local" |   local: "Local" | ||||||
|   hybrid: "Social" |   hybrid: "Social" | ||||||
|   global: "Global" |   global: "Global" | ||||||
|  |   mentions: "Mentions" | ||||||
|   list: "Listes" |   list: "Listes" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "Content de vous revoir !" |   welcome-back: "Content de vous revoir !" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "Local" |   local: "Local" | ||||||
|   hybrid: "Social" |   hybrid: "Social" | ||||||
|   global: "Global" |   global: "Global" | ||||||
|  |   mentions: "Mentions" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "Pas de message avec un hashtag {} trouvé." |   no-posts-found: "Pas de message avec un hashtag {} trouvé." | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
| @@ -1172,7 +1180,7 @@ mobile/views/pages/settings/settings.profile.vue: | |||||||
|   avatar: "Avatar" |   avatar: "Avatar" | ||||||
|   banner: "Bannière" |   banner: "Bannière" | ||||||
|   is-cat: "Ce compte est un Bot" |   is-cat: "Ce compte est un Bot" | ||||||
|   is-locked: "フォローを承認制にする" |   is-locked: "Demande d’abonnement en attente d’approbation" | ||||||
|   advanced: "Avancé" |   advanced: "Avancé" | ||||||
|   privacy: "Vie privée" |   privacy: "Vie privée" | ||||||
|   save: "Mettre à jour le profil" |   save: "Mettre à jour le profil" | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "ホーム" |     home: "ホーム" | ||||||
|     local: "ローカル" |     local: "ローカル" | ||||||
|     hybrid: "ソーシャル" |     hybrid: "ソーシャル" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "グローバル" |     global: "グローバル" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "通知" |     notifications: "通知" | ||||||
|     list: "リスト" |     list: "リスト" | ||||||
|     swap-left: "左に移動" |     swap-left: "左に移動" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -119,7 +119,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|  |  | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
| @@ -166,7 +166,9 @@ common: | |||||||
|     home: "ホーム" |     home: "ホーム" | ||||||
|     local: "ローカル" |     local: "ローカル" | ||||||
|     hybrid: "ソーシャル" |     hybrid: "ソーシャル" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "グローバル" |     global: "グローバル" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "通知" |     notifications: "通知" | ||||||
|     list: "リスト" |     list: "リスト" | ||||||
|     swap-left: "左に移動" |     swap-left: "左に移動" | ||||||
| @@ -913,7 +915,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
|  |  | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
| @@ -1314,6 +1321,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|  |  | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "うち" |     home: "うち" | ||||||
|     local: "ローカル" |     local: "ローカル" | ||||||
|     hybrid: "ソーシャル" |     hybrid: "ソーシャル" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "グローバル" |     global: "グローバル" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "通知" |     notifications: "通知" | ||||||
|     list: "リスト" |     list: "リスト" | ||||||
|     swap-left: "左に移動や!" |     swap-left: "左に移動や!" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえり、" |   welcome-back: "おかえり、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "홈" |     home: "홈" | ||||||
|     local: "로컬" |     local: "로컬" | ||||||
|     hybrid: "소셜" |     hybrid: "소셜" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "글로벌" |     global: "글로벌" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "통지" |     notifications: "통지" | ||||||
|     list: "목록" |     list: "목록" | ||||||
|     swap-left: "左に移動" |     swap-left: "左に移動" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "ホーム" |     home: "ホーム" | ||||||
|     local: "ローカル" |     local: "ローカル" | ||||||
|     hybrid: "ソーシャル" |     hybrid: "ソーシャル" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "グローバル" |     global: "グローバル" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "通知" |     notifications: "通知" | ||||||
|     list: "リスト" |     list: "リスト" | ||||||
|     swap-left: "左に移動" |     swap-left: "左に移動" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "Lokaal" |   local: "Lokaal" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "Algemeen" |   global: "Algemeen" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "Lijsten" |   list: "Lijsten" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "ホーム" |     home: "ホーム" | ||||||
|     local: "ローカル" |     local: "ローカル" | ||||||
|     hybrid: "ソーシャル" |     hybrid: "ソーシャル" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "グローバル" |     global: "グローバル" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "通知" |     notifications: "通知" | ||||||
|     list: "リスト" |     list: "リスト" | ||||||
|     swap-left: "左に移動" |     swap-left: "左に移動" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "Strona główna" |     home: "Strona główna" | ||||||
|     local: "Lokalne" |     local: "Lokalne" | ||||||
|     hybrid: "Społeczność" |     hybrid: "Społeczność" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "Globalne" |     global: "Globalne" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "Powiadomienia" |     notifications: "Powiadomienia" | ||||||
|     list: "Listy" |     list: "Listy" | ||||||
|     swap-left: "Przesuń w lewo" |     swap-left: "Przesuń w lewo" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "Lokalne" |   local: "Lokalne" | ||||||
|   hybrid: "Społeczność" |   hybrid: "Społeczność" | ||||||
|   global: "Globalne" |   global: "Globalne" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "Listy" |   list: "Listy" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "Witaj ponownie," |   welcome-back: "Witaj ponownie," | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "Lokalne" |   local: "Lokalne" | ||||||
|   hybrid: "Społeczność" |   hybrid: "Społeczność" | ||||||
|   global: "Globalne" |   global: "Globalne" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "Nie znaleziono wpisów zawierających „{}”." |   no-posts-found: "Nie znaleziono wpisów zawierających „{}”." | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -7,12 +7,12 @@ common: | |||||||
|   about-title: "Uma ⭐ do fediverso." |   about-title: "Uma ⭐ do fediverso." | ||||||
|   about: "Obrigado por encontrar Misskey. Uma <b>plataforma descentralizada de microblog</b> nascida na Terra. Já que ela existe no Fediverso (um universo onde várias plataformas de mídia social são organizadas), ela é ligada com outras plataformas.Por que você não tira uma folga do agito e confusão da cidade, e mergulha em uma nova internet?" |   about: "Obrigado por encontrar Misskey. Uma <b>plataforma descentralizada de microblog</b> nascida na Terra. Já que ela existe no Fediverso (um universo onde várias plataformas de mídia social são organizadas), ela é ligada com outras plataformas.Por que você não tira uma folga do agito e confusão da cidade, e mergulha em uma nova internet?" | ||||||
|   intro: |   intro: | ||||||
|     title: "Misskeyって?" |     title: "O que é Misskey?" | ||||||
|     about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。" |     about: "Misskey é um <b>serviço de microblog descentralizado</b>. Personalização sofisticada da interface, variedade de reações a posts, armazenamento de arquivos grátis com gerenciamento integrado e outras funções avançadas estão disponíveis. Um sistema em rede chamado \"Fediverso\" permite que nos comuniquemos com usuários em outras redes sociais. Se você postar algo, por exemplo, seu post não será mandado apenas para o Misskey, mas também para o Mastodon. Apenas imagine que o planeta está enviando ondas de rádio para outros planetas para se comunicar." | ||||||
|     features: "特徴" |     features: "Recursos" | ||||||
|     rich-contents: "投稿" |     rich-contents: "Post" | ||||||
|     rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。" |     rich-contents-desc: "Apenas poste suas ideias, temas do momento e qualquer coisa que você queira compartilhar. Você pode querer decorar suas palavras, anexar suas imagens favoritas, enviar arquivos, inclusive vídeos ou criar uma enquete. Essas são as coisas que você pode fazer em Misskey." | ||||||
|     reaction: "リアクション" |     reaction: "Reações" | ||||||
|     reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。" |     reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。" | ||||||
|     ui: "インターフェース" |     ui: "インターフェース" | ||||||
|     ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。" |     ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。" | ||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "Início" |     home: "Início" | ||||||
|     local: "Local" |     local: "Local" | ||||||
|     hybrid: "Social" |     hybrid: "Social" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "Global" |     global: "Global" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "Notificações" |     notifications: "Notificações" | ||||||
|     list: "Listas" |     list: "Listas" | ||||||
|     swap-left: "Mover para a esquerda" |     swap-left: "Mover para a esquerda" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
| @@ -1164,23 +1172,23 @@ mobile/views/pages/games/reversi.vue: | |||||||
|   reversi: "リバーシ" |   reversi: "リバーシ" | ||||||
| mobile/views/pages/settings/settings.profile.vue: | mobile/views/pages/settings/settings.profile.vue: | ||||||
|   title: "プロフィール" |   title: "プロフィール" | ||||||
|   name: "名前" |   name: "Nome" | ||||||
|   account: "アカウント" |   account: "Conta" | ||||||
|   location: "場所" |   location: "Lugar" | ||||||
|   description: "自己紹介" |   description: "Biografia" | ||||||
|   birthday: "誕生日" |   birthday: "Data de nascimento" | ||||||
|   avatar: "アイコン" |   avatar: "Avatar" | ||||||
|   banner: "バナー" |   banner: "Capa" | ||||||
|   is-cat: "このアカウントはCatです" |   is-cat: "Esta conta é gato" | ||||||
|   is-locked: "フォローを承認制にする" |   is-locked: "Pedido para seguir precisa ser aprovado" | ||||||
|   advanced: "その他" |   advanced: "Avançado" | ||||||
|   privacy: "プライバシー" |   privacy: "Provacidade" | ||||||
|   save: "保存" |   save: "Atualizar perfil" | ||||||
|   saved: "プロフィールを保存しました" |   saved: "Perfil atualizado" | ||||||
|   uploading: "アップロード中" |   uploading: "Enviando" | ||||||
|   upload-failed: "アップロードに失敗しました" |   upload-failed: "Falha ao enviar" | ||||||
| mobile/views/pages/search.vue: | mobile/views/pages/search.vue: | ||||||
|   search: "検索" |   search: "Pesquisar" | ||||||
|   empty: "「{}」に関する投稿は見つかりませんでした。" |   empty: "「{}」に関する投稿は見つかりませんでした。" | ||||||
|   not-found: "「{}」に関する投稿は見つかりませんでした。" |   not-found: "「{}」に関する投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/selectdrive.vue: | mobile/views/pages/selectdrive.vue: | ||||||
| @@ -1217,47 +1225,47 @@ mobile/views/pages/settings.vue: | |||||||
|   load-raw-images: "添付された画像を高画質で表示する" |   load-raw-images: "添付された画像を高画質で表示する" | ||||||
|   load-remote-media: "リモートサーバーのメディアを表示する" |   load-remote-media: "リモートサーバーのメディアを表示する" | ||||||
|   twitter: "Twitter連携" |   twitter: "Twitter連携" | ||||||
|   twitter-connect: "Twitterアカウントに接続する" |   twitter-connect: "Conectar à sua conta no Twitter" | ||||||
|   twitter-reconnect: "再接続する" |   twitter-reconnect: "Reconectar" | ||||||
|   twitter-disconnect: "切断する" |   twitter-disconnect: "Desconectar" | ||||||
|   update: "Misskey Update" |   update: "Atualizar Misskey" | ||||||
|   version: "バージョン:" |   version: "Versão atual;" | ||||||
|   latest-version: "最新のバージョン:" |   latest-version: "Última versão:" | ||||||
|   update-checking: "アップデートを確認中" |   update-checking: "Verificando atualizações" | ||||||
|   check-for-updates: "アップデートを確認" |   check-for-updates: "Verificar atualizações" | ||||||
|   no-updates: "利用可能な更新はありません" |   no-updates: "Sem atualizações" | ||||||
|   no-updates-desc: "お使いのMisskeyは最新です。" |   no-updates-desc: "Seu Misskey está atualizado" | ||||||
|   update-available: "新しいバージョンが利用可能です" |   update-available: "Uma nova versão está disponível" | ||||||
|   update-available-desc: "ページを再度読み込みすると更新が適用されます。" |   update-available-desc: "Atualizações vão ser aplicadas depois de recarregar a página" | ||||||
|   settings: "設定" |   settings: "Configurações" | ||||||
|   signout: "サインアウト" |   signout: "Sair" | ||||||
|   sound: "サウンド" |   sound: "Sons" | ||||||
|   enable-sounds: "サウンドを有効にする" |   enable-sounds: "Ativar sons" | ||||||
| mobile/views/pages/user.vue: | mobile/views/pages/user.vue: | ||||||
|   follows-you: "フォローされています" |   follows-you: "Te segue" | ||||||
|   following: "フォロー" |   following: "Seguindo" | ||||||
|   followers: "フォロワー" |   followers: "Seguidores" | ||||||
|   notes: "投稿" |   notes: "Posts" | ||||||
|   overview: "概要" |   overview: "概要" | ||||||
|   timeline: "タイムライン" |   timeline: "Linha do tempo" | ||||||
|   media: "メディア" |   media: "Mídia" | ||||||
|   is-suspended: "このユーザーは凍結されています。" |   is-suspended: "Esta conta foi suspensa" | ||||||
|   is-remote: "Este é uma usuário remoto. O perfil que vê aqui pode não estar completo." |   is-remote: "Este é uma usuário remoto. O perfil que vê aqui pode não estar completo." | ||||||
|   view-remote: "Ver o perfil completo." |   view-remote: "Ver o perfil completo." | ||||||
| mobile/views/pages/user/home.vue: | mobile/views/pages/user/home.vue: | ||||||
|   recent-notes: "Notas recentes" |   recent-notes: "Notas recentes" | ||||||
|   images: "Imagens" |   images: "Imagens" | ||||||
|   activity: "Atividade" |   activity: "Atividade" | ||||||
|   keywords: "キーワード" |   keywords: "Palavras chave" | ||||||
|   domains: "頻出ドメイン" |   domains: "Domínios" | ||||||
|   frequently-replied-users: "よく会話するユーザー" |   frequently-replied-users: "Perguntas frequentes" | ||||||
|   followers-you-know: "Seguidores que você conhece" |   followers-you-know: "Seguidores que você conhece" | ||||||
|   last-used-at: "Ativo pela última vez:" |   last-used-at: "Ativo pela última vez:" | ||||||
| mobile/views/pages/user/home.followers-you-know.vue: | mobile/views/pages/user/home.followers-you-know.vue: | ||||||
|   loading: "Carregando" |   loading: "Carregando" | ||||||
|   no-users: "知り合いのユーザーはいません" |   no-users: "知り合いのユーザーはいません" | ||||||
| mobile/views/pages/user/home.friends.vue: | mobile/views/pages/user/home.friends.vue: | ||||||
|   loading: "読み込み中" |   loading: "Carregando" | ||||||
|   no-users: "よく会話するユーザーはいません" |   no-users: "よく会話するユーザーはいません" | ||||||
| mobile/views/pages/user/home.notes.vue: | mobile/views/pages/user/home.notes.vue: | ||||||
|   loading: "Carregando" |   loading: "Carregando" | ||||||
| @@ -1267,14 +1275,14 @@ mobile/views/pages/user/home.photos.vue: | |||||||
|   no-photos: "Sem fotos" |   no-photos: "Sem fotos" | ||||||
| docs: | docs: | ||||||
|   edit-this-page-on-github: "間違いや改善点を見つけましたか?" |   edit-this-page-on-github: "間違いや改善点を見つけましたか?" | ||||||
|   edit-this-page-on-github-link: "このページをGitHubで編集" |   edit-this-page-on-github-link: "Edite esta página no GitHub!" | ||||||
|   api: |   api: | ||||||
|     entities: |     entities: | ||||||
|       properties: "プロパティ" |       properties: "Propriedades" | ||||||
|     endpoints: |     endpoints: | ||||||
|       params: "パラメータ" |       params: "Parâmetros" | ||||||
|       no-params: "パラメータはありません" |       no-params: "Sem parâmetros" | ||||||
|       res: "レスポンス" |       res: "Resposta" | ||||||
|       require-credential: "このエンドポイントは認証情報が必須です。" |       require-credential: "このエンドポイントは認証情報が必須です。" | ||||||
|       require-permission: "このエンドポイントは{permission}の権限を必要とします。" |       require-permission: "このエンドポイントは{permission}の権限を必要とします。" | ||||||
|       has-limit: "レートリミットがあります。" |       has-limit: "レートリミットがあります。" | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "ホーム" |     home: "ホーム" | ||||||
|     local: "ローカル" |     local: "ローカル" | ||||||
|     hybrid: "ソーシャル" |     hybrid: "ソーシャル" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "グローバル" |     global: "グローバル" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "通知" |     notifications: "通知" | ||||||
|     list: "リスト" |     list: "リスト" | ||||||
|     swap-left: "左に移動" |     swap-left: "左に移動" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ common: | |||||||
|   always-show-nsfw: "常に閲覧注意のメディアを表示する" |   always-show-nsfw: "常に閲覧注意のメディアを表示する" | ||||||
|   always-mark-nsfw: "常にメディアを閲覧注意として投稿" |   always-mark-nsfw: "常にメディアを閲覧注意として投稿" | ||||||
|   show-full-acct: "ユーザー名のホストを省略しない" |   show-full-acct: "ユーザー名のホストを省略しない" | ||||||
|   enable-animations: "アニメーションを使用" |   reduce-motion: "UIの動きを減らす" | ||||||
|   this-setting-is-this-device-only: "このデバイスのみ" |   this-setting-is-this-device-only: "このデバイスのみ" | ||||||
|   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' |   do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' | ||||||
|   reversi: |   reversi: | ||||||
| @@ -155,7 +155,9 @@ common: | |||||||
|     home: "ホーム" |     home: "ホーム" | ||||||
|     local: "ローカル" |     local: "ローカル" | ||||||
|     hybrid: "ソーシャル" |     hybrid: "ソーシャル" | ||||||
|  |     hashtag: "ハッシュタグ" | ||||||
|     global: "グローバル" |     global: "グローバル" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|     notifications: "通知" |     notifications: "通知" | ||||||
|     list: "リスト" |     list: "リスト" | ||||||
|     swap-left: "左に移動" |     swap-left: "左に移動" | ||||||
| @@ -807,7 +809,12 @@ desktop/views/components/timeline.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
|   list: "リスト" |   list: "リスト" | ||||||
|  |   hashtag: "ハッシュタグ" | ||||||
|  |   add-tag-timeline: "ハッシュタグを追加" | ||||||
|  |   add-list: "リストを追加" | ||||||
|  |   list-name: "リスト名" | ||||||
| desktop/views/components/ui.header.vue: | desktop/views/components/ui.header.vue: | ||||||
|   welcome-back: "おかえりなさい、" |   welcome-back: "おかえりなさい、" | ||||||
|   adjective: "さん" |   adjective: "さん" | ||||||
| @@ -1132,6 +1139,7 @@ mobile/views/pages/home.vue: | |||||||
|   local: "ローカル" |   local: "ローカル" | ||||||
|   hybrid: "ソーシャル" |   hybrid: "ソーシャル" | ||||||
|   global: "グローバル" |   global: "グローバル" | ||||||
|  |   mentions: "あなた宛て" | ||||||
| mobile/views/pages/tag.vue: | mobile/views/pages/tag.vue: | ||||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" |   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||||
| mobile/views/pages/welcome.vue: | mobile/views/pages/welcome.vue: | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"author": "syuilo <i@syuilo.com>", | 	"author": "syuilo <i@syuilo.com>", | ||||||
| 	"version": "8.42.0", | 	"version": "8.45.1", | ||||||
| 	"clientVersion": "1.0.9769", | 	"clientVersion": "1.0.9840", | ||||||
| 	"codename": "nighthike", | 	"codename": "nighthike", | ||||||
| 	"main": "./built/index.js", | 	"main": "./built/index.js", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| @@ -20,10 +20,10 @@ | |||||||
| 		"format": "gulp format" | 		"format": "gulp format" | ||||||
| 	}, | 	}, | ||||||
| 	"dependencies": { | 	"dependencies": { | ||||||
| 		"@fortawesome/fontawesome": "1.1.8", | 		"@fortawesome/fontawesome-svg-core": "1.2.4", | ||||||
| 		"@fortawesome/fontawesome-free-brands": "5.0.13", | 		"@fortawesome/free-brands-svg-icons": "5.3.1", | ||||||
| 		"@fortawesome/fontawesome-free-regular": "5.0.13", | 		"@fortawesome/free-regular-svg-icons": "5.3.1", | ||||||
| 		"@fortawesome/fontawesome-free-solid": "5.0.13", | 		"@fortawesome/free-solid-svg-icons": "5.3.1", | ||||||
| 		"@koa/cors": "2.2.2", | 		"@koa/cors": "2.2.2", | ||||||
| 		"@prezzemolo/rap": "0.1.2", | 		"@prezzemolo/rap": "0.1.2", | ||||||
| 		"@prezzemolo/zip": "0.0.3", | 		"@prezzemolo/zip": "0.0.3", | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								src/client/app/common/scripts/streaming/hashtag.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/client/app/common/scripts/streaming/hashtag.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import Stream from './stream'; | ||||||
|  | import MiOS from '../../../mios'; | ||||||
|  |  | ||||||
|  | export class HashtagStream extends Stream { | ||||||
|  | 	constructor(os: MiOS, me, q) { | ||||||
|  | 		super(os, 'hashtag', me ? { | ||||||
|  | 			i: me.token, | ||||||
|  | 			q: JSON.stringify(q) | ||||||
|  | 		} : { | ||||||
|  | 			q: JSON.stringify(q) | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -100,7 +100,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 	created() { | 	created() { | ||||||
| 		(this as any).api('chart', { | 		(this as any).api('chart', { | ||||||
| 			limit: 32 | 			limit: 35 | ||||||
| 		}).then(chart => { | 		}).then(chart => { | ||||||
| 			this.chart = chart; | 			this.chart = chart; | ||||||
| 		}); | 		}); | ||||||
| @@ -681,6 +681,6 @@ export default Vue.extend({ | |||||||
| 	> div | 	> div | ||||||
| 		> * | 		> * | ||||||
| 			display block | 			display block | ||||||
| 			height 320px | 			height 350px | ||||||
|  |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ | |||||||
| 	</div> | 	</div> | ||||||
|  |  | ||||||
| 	<!-- トランジションを有効にするとなぜかメモリリークする --> | 	<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||||
| 	<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div"> | 	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div"> | ||||||
| 		<template v-for="(note, i) in _notes"> | 		<template v-for="(note, i) in _notes"> | ||||||
| 			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> | 			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> | ||||||
| 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> | 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| <div class="mk-notifications"> | <div class="mk-notifications"> | ||||||
| 	<div class="notifications" v-if="notifications.length != 0"> | 	<div class="notifications" v-if="notifications.length != 0"> | ||||||
| 		<!-- トランジションを有効にするとなぜかメモリリークする --> | 		<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||||
| 		<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div"> | 		<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div"> | ||||||
| 			<template v-for="(notification, i) in _notifications"> | 			<template v-for="(notification, i) in _notifications"> | ||||||
| 				<div class="notification" :class="notification.type" :key="notification.id"> | 				<div class="notification" :class="notification.type" :key="notification.id"> | ||||||
| 					<mk-time :time="notification.createdAt"/> | 					<mk-time :time="notification.createdAt"/> | ||||||
|   | |||||||
| @@ -1,13 +1,19 @@ | |||||||
| <template> | <template> | ||||||
| <mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy"> | <mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy"> | ||||||
| 	<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span> | 	<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span> | ||||||
| 	<mk-settings @done="close"/> | 	<mk-settings :initial-page="initialPage" @done="close"/> | ||||||
| </mk-window> | </mk-window> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
|  | 	props: { | ||||||
|  | 		initialPage: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: false | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		close() { | 		close() { | ||||||
| 			(this as any).$refs.window.close(); | 			(this as any).$refs.window.close(); | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								src/client/app/desktop/views/components/settings.tags.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/client/app/desktop/views/components/settings.tags.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | <template> | ||||||
|  | <div class="vfcitkilproprqtbnpoertpsziierwzi"> | ||||||
|  | 	<div v-for="timeline in timelines" class="timeline"> | ||||||
|  | 		<ui-input v-model="timeline.title" @change="save"> | ||||||
|  | 			<span>%i18n:@title%</span> | ||||||
|  | 		</ui-input> | ||||||
|  | 		<ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" @input="onQueryChange(timeline, $event)"> | ||||||
|  | 			<span>%i18n:@query%</span> | ||||||
|  | 		</ui-textarea> | ||||||
|  | 		<ui-button class="save" @click="save">%i18n:@save%</ui-button> | ||||||
|  | 	</div> | ||||||
|  | 	<ui-button class="add" @click="add">%i18n:@add%</ui-button> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import * as uuid from 'uuid'; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			timelines: this.$store.state.settings.tagTimelines | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	methods: { | ||||||
|  | 		add() { | ||||||
|  | 			this.timelines.push({ | ||||||
|  | 				id: uuid(), | ||||||
|  | 				title: '', | ||||||
|  | 				query: '' | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			this.save(); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		save() { | ||||||
|  | 			this.$store.dispatch('settings/set', { key: 'tagTimelines', value: this.timelines }); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		onQueryChange(timeline, value) { | ||||||
|  | 			timeline.query = value.split('\n').map(tags => tags.split(' ')); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  |  | ||||||
|  | root(isDark) | ||||||
|  | 	> .timeline | ||||||
|  | 		padding-bottom 16px | ||||||
|  | 		border-bottom solid 1px rgba(#000, 0.1) | ||||||
|  |  | ||||||
|  | 	> .add | ||||||
|  | 		margin-top 16px | ||||||
|  |  | ||||||
|  | .vfcitkilproprqtbnpoertpsziierwzi[data-darkmode] | ||||||
|  | 	root(true) | ||||||
|  |  | ||||||
|  | .vfcitkilproprqtbnpoertpsziierwzi:not([data-darkmode]) | ||||||
|  | 	root(false) | ||||||
|  |  | ||||||
|  | </style> | ||||||
| @@ -5,6 +5,7 @@ | |||||||
| 		<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p> | 		<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p> | ||||||
| 		<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p> | 		<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p> | ||||||
| 		<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p> | 		<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p> | ||||||
|  | 		<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p> | ||||||
| 		<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p> | 		<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p> | ||||||
| 		<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p> | 		<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p> | ||||||
| 		<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p> | 		<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p> | ||||||
| @@ -60,7 +61,7 @@ | |||||||
| 				<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button> | 				<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button> | ||||||
| 				<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/> | 				<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/> | ||||||
| 				<mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/> | 				<mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/> | ||||||
| 				<mk-switch v-model="animations" text="%i18n:common.enable-animations%"/> | 				<mk-switch v-model="reduceMotion" text="%i18n:common.reduce-motion%"/> | ||||||
| 				<mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/> | 				<mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/> | ||||||
| 				<mk-switch v-model="showFullAcct" text="%i18n:common.show-full-acct%"/> | 				<mk-switch v-model="showFullAcct" text="%i18n:common.show-full-acct%"/> | ||||||
| 				<mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/> | 				<mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/> | ||||||
| @@ -138,6 +139,11 @@ | |||||||
| 			<x-drive/> | 			<x-drive/> | ||||||
| 		</section> | 		</section> | ||||||
|  |  | ||||||
|  | 		<section class="hashtags" v-show="page == 'hashtags'"> | ||||||
|  | 			<h1>%i18n:@tags%</h1> | ||||||
|  | 			<x-tags/> | ||||||
|  | 		</section> | ||||||
|  |  | ||||||
| 		<section class="mute" v-show="page == 'mute'"> | 		<section class="mute" v-show="page == 'mute'"> | ||||||
| 			<h1>%i18n:@mute%</h1> | 			<h1>%i18n:@mute%</h1> | ||||||
| 			<x-mute/> | 			<x-mute/> | ||||||
| @@ -222,6 +228,7 @@ import XApi from './settings.api.vue'; | |||||||
| import XApps from './settings.apps.vue'; | import XApps from './settings.apps.vue'; | ||||||
| import XSignins from './settings.signins.vue'; | import XSignins from './settings.signins.vue'; | ||||||
| import XDrive from './settings.drive.vue'; | import XDrive from './settings.drive.vue'; | ||||||
|  | import XTags from './settings.tags.vue'; | ||||||
| import { url, langs, version } from '../../../config'; | import { url, langs, version } from '../../../config'; | ||||||
| import checkForUpdate from '../../../common/scripts/check-for-update'; | import checkForUpdate from '../../../common/scripts/check-for-update'; | ||||||
|  |  | ||||||
| @@ -234,11 +241,18 @@ export default Vue.extend({ | |||||||
| 		XApi, | 		XApi, | ||||||
| 		XApps, | 		XApps, | ||||||
| 		XSignins, | 		XSignins, | ||||||
| 		XDrive | 		XDrive, | ||||||
|  | 		XTags | ||||||
|  | 	}, | ||||||
|  | 	props: { | ||||||
|  | 		initialPage: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: false | ||||||
|  | 		} | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			page: 'profile', | 			page: this.initialPage || 'profile', | ||||||
| 			meta: null, | 			meta: null, | ||||||
| 			version, | 			version, | ||||||
| 			langs, | 			langs, | ||||||
| @@ -247,9 +261,9 @@ export default Vue.extend({ | |||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	computed: { | 	computed: { | ||||||
| 		animations: { | 		reduceMotion: { | ||||||
| 			get() { return this.$store.state.device.animations; }, | 			get() { return this.$store.state.device.reduceMotion; }, | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'animations', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); } | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		apiViaStream: { | 		apiViaStream: { | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ | |||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
|  | import { HashtagStream } from '../../../common/scripts/streaming/hashtag'; | ||||||
|  |  | ||||||
| const fetchLimit = 10; | const fetchLimit = 10; | ||||||
|  |  | ||||||
| @@ -23,6 +24,9 @@ export default Vue.extend({ | |||||||
| 		src: { | 		src: { | ||||||
| 			type: String, | 			type: String, | ||||||
| 			required: true | 			required: true | ||||||
|  | 		}, | ||||||
|  | 		tagTl: { | ||||||
|  | 			required: false | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| @@ -31,6 +35,7 @@ export default Vue.extend({ | |||||||
| 			fetching: true, | 			fetching: true, | ||||||
| 			moreFetching: false, | 			moreFetching: false, | ||||||
| 			existMore: false, | 			existMore: false, | ||||||
|  | 			streamManager: null, | ||||||
| 			connection: null, | 			connection: null, | ||||||
| 			connectionId: null, | 			connectionId: null, | ||||||
| 			date: null | 			date: null | ||||||
| @@ -42,21 +47,14 @@ export default Vue.extend({ | |||||||
| 			return this.$store.state.i.followingCount == 0; | 			return this.$store.state.i.followingCount == 0; | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		stream(): any { |  | ||||||
| 			switch (this.src) { |  | ||||||
| 				case 'home': return (this as any).os.stream; |  | ||||||
| 				case 'local': return (this as any).os.streams.localTimelineStream; |  | ||||||
| 				case 'hybrid': return (this as any).os.streams.hybridTimelineStream; |  | ||||||
| 				case 'global': return (this as any).os.streams.globalTimelineStream; |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
|  |  | ||||||
| 		endpoint(): string { | 		endpoint(): string { | ||||||
| 			switch (this.src) { | 			switch (this.src) { | ||||||
| 				case 'home': return 'notes/timeline'; | 				case 'home': return 'notes/timeline'; | ||||||
| 				case 'local': return 'notes/local-timeline'; | 				case 'local': return 'notes/local-timeline'; | ||||||
| 				case 'hybrid': return 'notes/hybrid-timeline'; | 				case 'hybrid': return 'notes/hybrid-timeline'; | ||||||
| 				case 'global': return 'notes/global-timeline'; | 				case 'global': return 'notes/global-timeline'; | ||||||
|  | 				case 'mentions': return 'notes/mentions'; | ||||||
|  | 				case 'tag': return 'notes/search_by_tag'; | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| @@ -66,13 +64,36 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.connection = this.stream.getConnection(); | 		if (this.src == 'tag') { | ||||||
| 		this.connectionId = this.stream.use(); | 			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||||
|  | 			this.connection.on('note', this.onNote); | ||||||
|  | 		} else if (this.src == 'home') { | ||||||
|  | 			this.streamManager = (this as any).os.stream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
| 			this.connection.on('note', this.onNote); | 			this.connection.on('note', this.onNote); | ||||||
| 		if (this.src == 'home') { |  | ||||||
| 			this.connection.on('follow', this.onChangeFollowing); | 			this.connection.on('follow', this.onChangeFollowing); | ||||||
| 			this.connection.on('unfollow', this.onChangeFollowing); | 			this.connection.on('unfollow', this.onChangeFollowing); | ||||||
|  | 		} else if (this.src == 'local') { | ||||||
|  | 			this.streamManager = (this as any).os.streams.localTimelineStream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
|  | 			this.connection.on('note', this.onNote); | ||||||
|  | 		} else if (this.src == 'hybrid') { | ||||||
|  | 			this.streamManager = (this as any).os.streams.hybridTimelineStream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
|  | 			this.connection.on('note', this.onNote); | ||||||
|  | 		} else if (this.src == 'global') { | ||||||
|  | 			this.streamManager = (this as any).os.streams.globalTimelineStream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
|  | 			this.connection.on('note', this.onNote); | ||||||
|  | 		} else if (this.src == 'mentions') { | ||||||
|  | 			this.streamManager = (this as any).os.stream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
|  | 			this.connection.on('mention', this.onNote); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		document.addEventListener('keydown', this.onKeydown); | 		document.addEventListener('keydown', this.onKeydown); | ||||||
| @@ -81,12 +102,27 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
|  | 		if (this.src == 'tag') { | ||||||
|  | 			this.connection.off('note', this.onNote); | ||||||
|  | 			this.connection.close(); | ||||||
|  | 		} else if (this.src == 'home') { | ||||||
| 			this.connection.off('note', this.onNote); | 			this.connection.off('note', this.onNote); | ||||||
| 		if (this.src == 'home') { |  | ||||||
| 			this.connection.off('follow', this.onChangeFollowing); | 			this.connection.off('follow', this.onChangeFollowing); | ||||||
| 			this.connection.off('unfollow', this.onChangeFollowing); | 			this.connection.off('unfollow', this.onChangeFollowing); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
|  | 		} else if (this.src == 'local') { | ||||||
|  | 			this.connection.off('note', this.onNote); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
|  | 		} else if (this.src == 'hybrid') { | ||||||
|  | 			this.connection.off('note', this.onNote); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
|  | 		} else if (this.src == 'global') { | ||||||
|  | 			this.connection.off('note', this.onNote); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
|  | 		} else if (this.src == 'mentions') { | ||||||
|  | 			this.connection.off('mention', this.onNote); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
| 		} | 		} | ||||||
| 		this.stream.dispose(this.connectionId); |  | ||||||
|  |  | ||||||
| 		document.removeEventListener('keydown', this.onKeydown); | 		document.removeEventListener('keydown', this.onKeydown); | ||||||
| 	}, | 	}, | ||||||
| @@ -101,7 +137,8 @@ export default Vue.extend({ | |||||||
| 					untilDate: this.date ? this.date.getTime() : undefined, | 					untilDate: this.date ? this.date.getTime() : undefined, | ||||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes | 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||||
|  | 					query: this.tagTl ? this.tagTl.query : undefined | ||||||
| 				}).then(notes => { | 				}).then(notes => { | ||||||
| 					if (notes.length == fetchLimit + 1) { | 					if (notes.length == fetchLimit + 1) { | ||||||
| 						notes.pop(); | 						notes.pop(); | ||||||
| @@ -124,7 +161,8 @@ export default Vue.extend({ | |||||||
| 				untilId: (this.$refs.timeline as any).tail().id, | 				untilId: (this.$refs.timeline as any).tail().id, | ||||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes | 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||||
|  | 				query: this.tagTl ? this.tagTl.query : undefined | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
| 			promise.then(notes => { | 			promise.then(notes => { | ||||||
|   | |||||||
| @@ -5,13 +5,20 @@ | |||||||
| 		<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span> | 		<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span> | ||||||
| 		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> | 		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> | ||||||
| 		<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> | 		<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> | ||||||
|  | 		<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span> | ||||||
|  | 		<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span> | ||||||
| 		<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> | 		<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> | ||||||
| 		<button @click="chooseList" title="%i18n:@list%">%fa:list%</button> | 		<div class="buttons"> | ||||||
|  | 			<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button> | ||||||
|  | 			<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button> | ||||||
|  | 		</div> | ||||||
| 	</header> | 	</header> | ||||||
| 	<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> | 	<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> | ||||||
| 	<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> | 	<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> | ||||||
| 	<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | 	<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | ||||||
| 	<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> | 	<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> | ||||||
|  | 	<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> | ||||||
|  | 	<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> | ||||||
| 	<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> | 	<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| @@ -19,7 +26,8 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import XCore from './timeline.core.vue'; | import XCore from './timeline.core.vue'; | ||||||
| import MkUserListsWindow from './user-lists-window.vue'; | import Menu from '../../../common/views/components/menu.vue'; | ||||||
|  | import MkSettingsWindow from './settings-window.vue'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| @@ -30,6 +38,7 @@ export default Vue.extend({ | |||||||
| 		return { | 		return { | ||||||
| 			src: 'home', | 			src: 'home', | ||||||
| 			list: null, | 			list: null, | ||||||
|  | 			tagTl: null, | ||||||
| 			enableLocalTimeline: false | 			enableLocalTimeline: false | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| @@ -39,8 +48,14 @@ export default Vue.extend({ | |||||||
| 			this.saveSrc(); | 			this.saveSrc(); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		list() { | 		list(x) { | ||||||
| 			this.saveSrc(); | 			this.saveSrc(); | ||||||
|  | 			if (x != null) this.tagTl = null; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		tagTl(x) { | ||||||
|  | 			this.saveSrc(); | ||||||
|  | 			if (x != null) this.list = null; | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| @@ -53,6 +68,8 @@ export default Vue.extend({ | |||||||
| 			this.src = this.$store.state.device.tl.src; | 			this.src = this.$store.state.device.tl.src; | ||||||
| 			if (this.src == 'list') { | 			if (this.src == 'list') { | ||||||
| 				this.list = this.$store.state.device.tl.arg; | 				this.list = this.$store.state.device.tl.arg; | ||||||
|  | 			} else if (this.src == 'tag') { | ||||||
|  | 				this.tagTl = this.$store.state.device.tl.arg; | ||||||
| 			} | 			} | ||||||
| 		} else if (this.$store.state.i.followingCount == 0) { | 		} else if (this.$store.state.i.followingCount == 0) { | ||||||
| 			this.src = 'hybrid'; | 			this.src = 'hybrid'; | ||||||
| @@ -69,7 +86,7 @@ export default Vue.extend({ | |||||||
| 		saveSrc() { | 		saveSrc() { | ||||||
| 			this.$store.commit('device/setTl', { | 			this.$store.commit('device/setTl', { | ||||||
| 				src: this.src, | 				src: this.src, | ||||||
| 				arg: this.list | 				arg: this.src == 'list' ? this.list : this.tagTl | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| @@ -77,12 +94,74 @@ export default Vue.extend({ | |||||||
| 			(this.$refs.tl as any).warp(date); | 			(this.$refs.tl as any).warp(date); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		chooseList() { | 		async chooseList() { | ||||||
| 			const w = (this as any).os.new(MkUserListsWindow); | 			const lists = await (this as any).api('users/lists/list'); | ||||||
| 			w.$once('choosen', list => { |  | ||||||
|  | 			let menu = [{ | ||||||
|  | 				icon: '%fa:plus%', | ||||||
|  | 				text: '%i18n:@add-list%', | ||||||
|  | 				action: () => { | ||||||
|  | 					(this as any).apis.input({ | ||||||
|  | 						title: '%i18n:@list-name%', | ||||||
|  | 					}).then(async title => { | ||||||
|  | 						const list = await (this as any).api('users/lists/create', { | ||||||
|  | 							title | ||||||
|  | 						}); | ||||||
|  |  | ||||||
| 						this.list = list; | 						this.list = list; | ||||||
| 						this.src = 'list'; | 						this.src = 'list'; | ||||||
| 				w.close(); | 					}); | ||||||
|  | 				} | ||||||
|  | 			}]; | ||||||
|  |  | ||||||
|  | 			if (lists.length > 0) { | ||||||
|  | 				menu.push(null); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			menu = menu.concat(lists.map(list => ({ | ||||||
|  | 				icon: '%fa:list%', | ||||||
|  | 				text: list.title, | ||||||
|  | 				action: () => { | ||||||
|  | 					this.list = list; | ||||||
|  | 					this.src = 'list'; | ||||||
|  | 				} | ||||||
|  | 			}))); | ||||||
|  |  | ||||||
|  | 			this.os.new(Menu, { | ||||||
|  | 				source: this.$refs.listButton, | ||||||
|  | 				compact: false, | ||||||
|  | 				items: menu | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		chooseTag() { | ||||||
|  | 			let menu = [{ | ||||||
|  | 				icon: '%fa:plus%', | ||||||
|  | 				text: '%i18n:@add-tag-timeline%', | ||||||
|  | 				action: () => { | ||||||
|  | 					(this as any).os.new(MkSettingsWindow, { | ||||||
|  | 						initialPage: 'hashtags' | ||||||
|  | 					}); | ||||||
|  | 				} | ||||||
|  | 			}]; | ||||||
|  |  | ||||||
|  | 			if (this.$store.state.settings.tagTimelines.length > 0) { | ||||||
|  | 				menu.push(null); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({ | ||||||
|  | 				icon: '%fa:hashtag%', | ||||||
|  | 				text: t.title, | ||||||
|  | 				action: () => { | ||||||
|  | 					this.tagTl = t; | ||||||
|  | 					this.src = 'tag'; | ||||||
|  | 				} | ||||||
|  | 			}))); | ||||||
|  |  | ||||||
|  | 			this.os.new(Menu, { | ||||||
|  | 				source: this.$refs.tagButton, | ||||||
|  | 				compact: false, | ||||||
|  | 				items: menu | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -104,13 +183,15 @@ root(isDark) | |||||||
| 		border-radius 6px 6px 0 0 | 		border-radius 6px 6px 0 0 | ||||||
| 		box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08) | 		box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08) | ||||||
|  |  | ||||||
| 		> button | 		> .buttons | ||||||
| 			position absolute | 			position absolute | ||||||
| 			z-index 2 | 			z-index 2 | ||||||
| 			top 0 | 			top 0 | ||||||
| 			right 0 | 			right 0 | ||||||
| 			padding 0 | 			padding-right 8px | ||||||
| 			width 42px |  | ||||||
|  | 			> button | ||||||
|  | 				padding 0 8px | ||||||
| 				font-size 0.9em | 				font-size 0.9em | ||||||
| 				line-height 42px | 				line-height 42px | ||||||
| 				color isDark ? #9baec8 : #ccc | 				color isDark ? #9baec8 : #ccc | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ | |||||||
| <x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/> | <x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/> | ||||||
| <x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/> | <x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/> | ||||||
| <x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/> | <x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/> | ||||||
|  | <x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked"/> | ||||||
|  | <x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| @@ -13,12 +15,14 @@ import Vue from 'vue'; | |||||||
| import XTlColumn from './deck.tl-column.vue'; | import XTlColumn from './deck.tl-column.vue'; | ||||||
| import XNotificationsColumn from './deck.notifications-column.vue'; | import XNotificationsColumn from './deck.notifications-column.vue'; | ||||||
| import XWidgetsColumn from './deck.widgets-column.vue'; | import XWidgetsColumn from './deck.widgets-column.vue'; | ||||||
|  | import XMentionsColumn from './deck.mentions-column.vue'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XTlColumn, | 		XTlColumn, | ||||||
| 		XNotificationsColumn, | 		XNotificationsColumn, | ||||||
| 		XWidgetsColumn | 		XWidgetsColumn, | ||||||
|  | 		XMentionsColumn | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	props: { | 	props: { | ||||||
|   | |||||||
							
								
								
									
										117
									
								
								src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | <template> | ||||||
|  | 	<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import XNotes from './deck.notes.vue'; | ||||||
|  | import { HashtagStream } from '../../../../common/scripts/streaming/hashtag'; | ||||||
|  |  | ||||||
|  | const fetchLimit = 10; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	components: { | ||||||
|  | 		XNotes | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	props: { | ||||||
|  | 		tagTl: { | ||||||
|  | 			type: Object, | ||||||
|  | 			required: true | ||||||
|  | 		}, | ||||||
|  | 		mediaOnly: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		}, | ||||||
|  | 		mediaView: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			fetching: true, | ||||||
|  | 			moreFetching: false, | ||||||
|  | 			existMore: false, | ||||||
|  | 			connection: null | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	watch: { | ||||||
|  | 		mediaOnly() { | ||||||
|  | 			this.fetch(); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	mounted() { | ||||||
|  | 		if (this.connection) this.connection.close(); | ||||||
|  | 		this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||||
|  | 		this.connection.on('note', this.onNote); | ||||||
|  |  | ||||||
|  | 		this.fetch(); | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	beforeDestroy() { | ||||||
|  | 		this.connection.close(); | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	methods: { | ||||||
|  | 		fetch() { | ||||||
|  | 			this.fetching = true; | ||||||
|  |  | ||||||
|  | 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||||
|  | 				(this as any).api('notes/search_by_tag', { | ||||||
|  | 					limit: fetchLimit + 1, | ||||||
|  | 					withFiles: this.mediaOnly, | ||||||
|  | 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||||
|  | 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||||
|  | 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||||
|  | 					query: this.tagTl.query | ||||||
|  | 				}).then(notes => { | ||||||
|  | 					if (notes.length == fetchLimit + 1) { | ||||||
|  | 						notes.pop(); | ||||||
|  | 						this.existMore = true; | ||||||
|  | 					} | ||||||
|  | 					res(notes); | ||||||
|  | 					this.fetching = false; | ||||||
|  | 					this.$emit('loaded'); | ||||||
|  | 				}, rej); | ||||||
|  | 			})); | ||||||
|  | 		}, | ||||||
|  | 		more() { | ||||||
|  | 			this.moreFetching = true; | ||||||
|  |  | ||||||
|  | 			const promise = (this as any).api('notes/search_by_tag', { | ||||||
|  | 				limit: fetchLimit + 1, | ||||||
|  | 				untilId: (this.$refs.timeline as any).tail().id, | ||||||
|  | 				withFiles: this.mediaOnly, | ||||||
|  | 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||||
|  | 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||||
|  | 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||||
|  | 				query: this.tagTl.query | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			promise.then(notes => { | ||||||
|  | 				if (notes.length == fetchLimit + 1) { | ||||||
|  | 					notes.pop(); | ||||||
|  | 				} else { | ||||||
|  | 					this.existMore = false; | ||||||
|  | 				} | ||||||
|  | 				notes.forEach(n => (this.$refs.timeline as any).append(n)); | ||||||
|  | 				this.moreFetching = false; | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			return promise; | ||||||
|  | 		}, | ||||||
|  | 		onNote(note) { | ||||||
|  | 			if (this.mediaOnly && note.files.length == 0) return; | ||||||
|  |  | ||||||
|  | 			// Prepend a note | ||||||
|  | 			(this.$refs.timeline as any).prepend(note); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | <template> | ||||||
|  | <x-column :name="name" :column="column" :is-stacked="isStacked"> | ||||||
|  | 	<span slot="header">%fa:at%{{ name }}</span> | ||||||
|  |  | ||||||
|  | 	<x-mentions/> | ||||||
|  | </x-column> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import XColumn from './deck.column.vue'; | ||||||
|  | import XMentions from './deck.mentions.vue'; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	components: { | ||||||
|  | 		XColumn, | ||||||
|  | 		XMentions | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	props: { | ||||||
|  | 		column: { | ||||||
|  | 			type: Object, | ||||||
|  | 			required: true | ||||||
|  | 		}, | ||||||
|  | 		isStacked: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	computed: { | ||||||
|  | 		name(): string { | ||||||
|  | 			if (this.column.name) return this.column.name; | ||||||
|  | 			return '%i18n:common.deck.mentions%'; | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
							
								
								
									
										93
									
								
								src/client/app/desktop/views/pages/deck/deck.mentions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/client/app/desktop/views/pages/deck/deck.mentions.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | <template> | ||||||
|  | 	<x-notes ref="timeline" :more="existMore ? more : null"/> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import XNotes from './deck.notes.vue'; | ||||||
|  |  | ||||||
|  | const fetchLimit = 10; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	components: { | ||||||
|  | 		XNotes | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	props: { | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			fetching: true, | ||||||
|  | 			moreFetching: false, | ||||||
|  | 			existMore: false, | ||||||
|  | 			connection: null, | ||||||
|  | 			connectionId: null | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	mounted() { | ||||||
|  | 		this.connection = (this as any).os.stream.getConnection(); | ||||||
|  | 		this.connectionId = (this as any).os.stream.use(); | ||||||
|  |  | ||||||
|  | 		this.connection.on('mention', this.onNote); | ||||||
|  |  | ||||||
|  | 		this.fetch(); | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	beforeDestroy() { | ||||||
|  | 		this.connection.off('mention', this.onNote); | ||||||
|  | 		(this as any).os.stream.dispose(this.connectionId); | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	methods: { | ||||||
|  | 		fetch() { | ||||||
|  | 			this.fetching = true; | ||||||
|  |  | ||||||
|  | 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||||
|  | 				(this as any).api('notes/mentions', { | ||||||
|  | 					limit: fetchLimit + 1, | ||||||
|  | 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||||
|  | 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||||
|  | 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||||
|  | 				}).then(notes => { | ||||||
|  | 					if (notes.length == fetchLimit + 1) { | ||||||
|  | 						notes.pop(); | ||||||
|  | 						this.existMore = true; | ||||||
|  | 					} | ||||||
|  | 					res(notes); | ||||||
|  | 					this.fetching = false; | ||||||
|  | 					this.$emit('loaded'); | ||||||
|  | 				}, rej); | ||||||
|  | 			})); | ||||||
|  | 		}, | ||||||
|  | 		more() { | ||||||
|  | 			this.moreFetching = true; | ||||||
|  |  | ||||||
|  | 			const promise = (this as any).api('notes/mentions', { | ||||||
|  | 				limit: fetchLimit + 1, | ||||||
|  | 				untilId: (this.$refs.timeline as any).tail().id, | ||||||
|  | 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||||
|  | 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||||
|  | 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			promise.then(notes => { | ||||||
|  | 				if (notes.length == fetchLimit + 1) { | ||||||
|  | 					notes.pop(); | ||||||
|  | 				} else { | ||||||
|  | 					this.existMore = false; | ||||||
|  | 				} | ||||||
|  | 				notes.forEach(n => (this.$refs.timeline as any).append(n)); | ||||||
|  | 				this.moreFetching = false; | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			return promise; | ||||||
|  | 		}, | ||||||
|  | 		onNote(note) { | ||||||
|  | 			// Prepend a note | ||||||
|  | 			(this.$refs.timeline as any).prepend(note); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
| <div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> | <div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> | ||||||
| 	<!-- トランジションを有効にするとなぜかメモリリークする --> | 	<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||||
| 	<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications"> | 	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications"> | ||||||
| 		<template v-for="(notification, i) in _notifications"> | 		<template v-for="(notification, i) in _notifications"> | ||||||
| 			<x-notification class="notification" :notification="notification" :key="notification.id"/> | 			<x-notification class="notification" :notification="notification" :key="notification.id"/> | ||||||
| 			<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> | 			<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ | |||||||
| 		<template v-if="column.type == 'hybrid'">%fa:share-alt%</template> | 		<template v-if="column.type == 'hybrid'">%fa:share-alt%</template> | ||||||
| 		<template v-if="column.type == 'global'">%fa:globe%</template> | 		<template v-if="column.type == 'global'">%fa:globe%</template> | ||||||
| 		<template v-if="column.type == 'list'">%fa:list%</template> | 		<template v-if="column.type == 'list'">%fa:list%</template> | ||||||
|  | 		<template v-if="column.type == 'hashtag'">%fa:hashtag%</template> | ||||||
| 		<span>{{ name }}</span> | 		<span>{{ name }}</span> | ||||||
| 	</span> | 	</span> | ||||||
|  |  | ||||||
| @@ -14,6 +15,7 @@ | |||||||
| 		<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/> | 		<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/> | ||||||
| 	</div> | 	</div> | ||||||
| 	<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> | 	<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> | ||||||
|  | 	<x-hashtag-tl v-if="column.type == 'hashtag'" :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> | ||||||
| 	<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> | 	<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> | ||||||
| </x-column> | </x-column> | ||||||
| </template> | </template> | ||||||
| @@ -23,12 +25,14 @@ import Vue from 'vue'; | |||||||
| import XColumn from './deck.column.vue'; | import XColumn from './deck.column.vue'; | ||||||
| import XTl from './deck.tl.vue'; | import XTl from './deck.tl.vue'; | ||||||
| import XListTl from './deck.list-tl.vue'; | import XListTl from './deck.list-tl.vue'; | ||||||
|  | import XHashtagTl from './deck.hashtag-tl.vue'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XTl, | 		XTl, | ||||||
| 		XListTl | 		XListTl, | ||||||
|  | 		XHashtagTl | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	props: { | 	props: { | ||||||
| @@ -65,6 +69,7 @@ export default Vue.extend({ | |||||||
| 				case 'hybrid': return '%i18n:common.deck.hybrid%'; | 				case 'hybrid': return '%i18n:common.deck.hybrid%'; | ||||||
| 				case 'global': return '%i18n:common.deck.global%'; | 				case 'global': return '%i18n:common.deck.global%'; | ||||||
| 				case 'list': return this.column.list.title; | 				case 'list': return this.column.list.title; | ||||||
|  | 				case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -138,6 +138,15 @@ export default Vue.extend({ | |||||||
| 							type: 'global' | 							type: 'global' | ||||||
| 						}); | 						}); | ||||||
| 					} | 					} | ||||||
|  | 				}, { | ||||||
|  | 					icon: '%fa:at%', | ||||||
|  | 					text: '%i18n:common.deck.mentions%', | ||||||
|  | 					action: () => { | ||||||
|  | 						this.$store.dispatch('settings/addDeckColumn', { | ||||||
|  | 							id: uuid(), | ||||||
|  | 							type: 'mentions' | ||||||
|  | 						}); | ||||||
|  | 					} | ||||||
| 				}, { | 				}, { | ||||||
| 					icon: '%fa:list%', | 					icon: '%fa:list%', | ||||||
| 					text: '%i18n:common.deck.list%', | 					text: '%i18n:common.deck.list%', | ||||||
| @@ -152,6 +161,20 @@ export default Vue.extend({ | |||||||
| 							w.close(); | 							w.close(); | ||||||
| 						}); | 						}); | ||||||
| 					} | 					} | ||||||
|  | 				}, { | ||||||
|  | 					icon: '%fa:hashtag%', | ||||||
|  | 					text: '%i18n:common.deck.hashtag%', | ||||||
|  | 					action: () => { | ||||||
|  | 						(this as any).apis.input({ | ||||||
|  | 							title: '%i18n:@enter-hashtag-tl-title%' | ||||||
|  | 						}).then(title => { | ||||||
|  | 							this.$store.dispatch('settings/addDeckColumn', { | ||||||
|  | 								id: uuid(), | ||||||
|  | 								type: 'hashtag', | ||||||
|  | 								tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id | ||||||
|  | 							}); | ||||||
|  | 						}); | ||||||
|  | 					} | ||||||
| 				}, { | 				}, { | ||||||
| 					icon: '%fa:bell R%', | 					icon: '%fa:bell R%', | ||||||
| 					text: '%i18n:common.deck.notifications%', | 					text: '%i18n:common.deck.notifications%', | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ export default Vue.extend({ | |||||||
| 	> .stats | 	> .stats | ||||||
| 		display flex | 		display flex | ||||||
| 		justify-content center | 		justify-content center | ||||||
| 		margin-bottom 16px | 		margin 0 auto 16px auto | ||||||
| 		padding 32px | 		padding 32px | ||||||
| 		background #fff | 		background #fff | ||||||
| 		box-shadow 0 2px 8px rgba(#000, 0.1) | 		box-shadow 0 2px 8px rgba(#000, 0.1) | ||||||
| @@ -60,5 +60,6 @@ export default Vue.extend({ | |||||||
| 				font-size 70% | 				font-size 70% | ||||||
|  |  | ||||||
| 	> div | 	> div | ||||||
| 		max-width 850px | 		max-width 950px | ||||||
|  | 		margin 0 auto | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ | |||||||
| 	</div> | 	</div> | ||||||
|  |  | ||||||
| 	<!-- トランジションを有効にするとなぜかメモリリークする --> | 	<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||||
| 	<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div"> | 	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div"> | ||||||
| 		<template v-for="(note, i) in _notes"> | 		<template v-for="(note, i) in _notes"> | ||||||
| 			<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> | 			<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> | ||||||
| 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> | 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
| <div class="mk-notifications"> | <div class="mk-notifications"> | ||||||
| 	<!-- トランジションを有効にするとなぜかメモリリークする --> | 	<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||||
| 	<component :is="$store.state.device.animations ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications"> | 	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications"> | ||||||
| 		<template v-for="(notification, i) in _notifications"> | 		<template v-for="(notification, i) in _notifications"> | ||||||
| 			<mk-notification :notification="notification" :key="notification.id"/> | 			<mk-notification :notification="notification" :key="notification.id"/> | ||||||
| 			<p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date"> | 			<p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date"> | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ | |||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
|  | import { HashtagStream } from '../../../common/scripts/streaming/hashtag'; | ||||||
|  |  | ||||||
| const fetchLimit = 10; | const fetchLimit = 10; | ||||||
|  |  | ||||||
| @@ -21,6 +22,9 @@ export default Vue.extend({ | |||||||
| 		src: { | 		src: { | ||||||
| 			type: String, | 			type: String, | ||||||
| 			required: true | 			required: true | ||||||
|  | 		}, | ||||||
|  | 		tagTl: { | ||||||
|  | 			required: false | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| @@ -29,6 +33,7 @@ export default Vue.extend({ | |||||||
| 			fetching: true, | 			fetching: true, | ||||||
| 			moreFetching: false, | 			moreFetching: false, | ||||||
| 			existMore: false, | 			existMore: false, | ||||||
|  | 			streamManager: null, | ||||||
| 			connection: null, | 			connection: null, | ||||||
| 			connectionId: null, | 			connectionId: null, | ||||||
| 			unreadCount: 0, | 			unreadCount: 0, | ||||||
| @@ -41,21 +46,14 @@ export default Vue.extend({ | |||||||
| 			return this.$store.state.i.followingCount == 0; | 			return this.$store.state.i.followingCount == 0; | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		stream(): any { |  | ||||||
| 			switch (this.src) { |  | ||||||
| 				case 'home': return (this as any).os.stream; |  | ||||||
| 				case 'local': return (this as any).os.streams.localTimelineStream; |  | ||||||
| 				case 'hybrid': return (this as any).os.streams.hybridTimelineStream; |  | ||||||
| 				case 'global': return (this as any).os.streams.globalTimelineStream; |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
|  |  | ||||||
| 		endpoint(): string { | 		endpoint(): string { | ||||||
| 			switch (this.src) { | 			switch (this.src) { | ||||||
| 				case 'home': return 'notes/timeline'; | 				case 'home': return 'notes/timeline'; | ||||||
| 				case 'local': return 'notes/local-timeline'; | 				case 'local': return 'notes/local-timeline'; | ||||||
| 				case 'hybrid': return 'notes/hybrid-timeline'; | 				case 'hybrid': return 'notes/hybrid-timeline'; | ||||||
| 				case 'global': return 'notes/global-timeline'; | 				case 'global': return 'notes/global-timeline'; | ||||||
|  | 				case 'mentions': return 'notes/mentions'; | ||||||
|  | 				case 'tag': return 'notes/search_by_tag'; | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| @@ -65,25 +63,63 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.connection = this.stream.getConnection(); | 		if (this.src == 'tag') { | ||||||
| 		this.connectionId = this.stream.use(); | 			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||||
|  | 			this.connection.on('note', this.onNote); | ||||||
|  | 		} else if (this.src == 'home') { | ||||||
|  | 			this.streamManager = (this as any).os.stream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
| 			this.connection.on('note', this.onNote); | 			this.connection.on('note', this.onNote); | ||||||
| 		if (this.src == 'home') { |  | ||||||
| 			this.connection.on('follow', this.onChangeFollowing); | 			this.connection.on('follow', this.onChangeFollowing); | ||||||
| 			this.connection.on('unfollow', this.onChangeFollowing); | 			this.connection.on('unfollow', this.onChangeFollowing); | ||||||
|  | 		} else if (this.src == 'local') { | ||||||
|  | 			this.streamManager = (this as any).os.streams.localTimelineStream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
|  | 			this.connection.on('note', this.onNote); | ||||||
|  | 		} else if (this.src == 'hybrid') { | ||||||
|  | 			this.streamManager = (this as any).os.streams.hybridTimelineStream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
|  | 			this.connection.on('note', this.onNote); | ||||||
|  | 		} else if (this.src == 'global') { | ||||||
|  | 			this.streamManager = (this as any).os.streams.globalTimelineStream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
|  | 			this.connection.on('note', this.onNote); | ||||||
|  | 		} else if (this.src == 'mentions') { | ||||||
|  | 			this.streamManager = (this as any).os.stream; | ||||||
|  | 			this.connection = this.streamManager.getConnection(); | ||||||
|  | 			this.connectionId = this.streamManager.use(); | ||||||
|  | 			this.connection.on('mention', this.onNote); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		this.fetch(); | 		this.fetch(); | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
|  | 		if (this.src == 'tag') { | ||||||
|  | 			this.connection.off('note', this.onNote); | ||||||
|  | 			this.connection.close(); | ||||||
|  | 		} else if (this.src == 'home') { | ||||||
| 			this.connection.off('note', this.onNote); | 			this.connection.off('note', this.onNote); | ||||||
| 		if (this.src == 'home') { |  | ||||||
| 			this.connection.off('follow', this.onChangeFollowing); | 			this.connection.off('follow', this.onChangeFollowing); | ||||||
| 			this.connection.off('unfollow', this.onChangeFollowing); | 			this.connection.off('unfollow', this.onChangeFollowing); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
|  | 		} else if (this.src == 'local') { | ||||||
|  | 			this.connection.off('note', this.onNote); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
|  | 		} else if (this.src == 'hybrid') { | ||||||
|  | 			this.connection.off('note', this.onNote); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
|  | 		} else if (this.src == 'global') { | ||||||
|  | 			this.connection.off('note', this.onNote); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
|  | 		} else if (this.src == 'mentions') { | ||||||
|  | 			this.connection.off('mention', this.onNote); | ||||||
|  | 			this.streamManager.dispose(this.connectionId); | ||||||
| 		} | 		} | ||||||
| 		this.stream.dispose(this.connectionId); |  | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	methods: { | 	methods: { | ||||||
| @@ -96,7 +132,8 @@ export default Vue.extend({ | |||||||
| 					untilDate: this.date ? this.date.getTime() : undefined, | 					untilDate: this.date ? this.date.getTime() : undefined, | ||||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes | 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||||
|  | 					query: this.tagTl ? this.tagTl.query : undefined | ||||||
| 				}).then(notes => { | 				}).then(notes => { | ||||||
| 					if (notes.length == fetchLimit + 1) { | 					if (notes.length == fetchLimit + 1) { | ||||||
| 						notes.pop(); | 						notes.pop(); | ||||||
| @@ -119,7 +156,8 @@ export default Vue.extend({ | |||||||
| 				untilId: (this.$refs.timeline as any).tail().id, | 				untilId: (this.$refs.timeline as any).tail().id, | ||||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes | 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||||
|  | 				query: this.tagTl ? this.tagTl.query : undefined | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
| 			promise.then(notes => { | 			promise.then(notes => { | ||||||
|   | |||||||
| @@ -6,7 +6,9 @@ | |||||||
| 			<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span> | 			<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span> | ||||||
| 			<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span> | 			<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span> | ||||||
| 			<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span> | 			<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span> | ||||||
|  | 			<span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span> | ||||||
| 			<span v-if="src == 'list'">%fa:list%{{ list.title }}</span> | 			<span v-if="src == 'list'">%fa:list%{{ list.title }}</span> | ||||||
|  | 			<span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span> | ||||||
| 		</span> | 		</span> | ||||||
| 		<span style="margin-left:8px"> | 		<span style="margin-left:8px"> | ||||||
| 			<template v-if="!showNav">%fa:angle-down%</template> | 			<template v-if="!showNav">%fa:angle-down%</template> | ||||||
| @@ -27,9 +29,11 @@ | |||||||
| 					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span> | 					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span> | ||||||
| 					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> | 					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> | ||||||
| 					<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> | 					<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> | ||||||
|  | 					<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span> | ||||||
| 					<template v-if="lists"> | 					<template v-if="lists"> | ||||||
| 						<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span> | 						<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span> | ||||||
| 					</template> | 					</template> | ||||||
|  | 					<span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id">%fa:hashtag% {{ tl.title }}</span> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| @@ -39,6 +43,8 @@ | |||||||
| 			<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> | 			<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> | ||||||
| 			<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | 			<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | ||||||
| 			<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> | 			<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> | ||||||
|  | 			<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> | ||||||
|  | 			<x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> | ||||||
| 			<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> | 			<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> | ||||||
| 		</div> | 		</div> | ||||||
| 	</main> | 	</main> | ||||||
| @@ -60,6 +66,7 @@ export default Vue.extend({ | |||||||
| 			src: 'home', | 			src: 'home', | ||||||
| 			list: null, | 			list: null, | ||||||
| 			lists: null, | 			lists: null, | ||||||
|  | 			tagTl: null, | ||||||
| 			showNav: false, | 			showNav: false, | ||||||
| 			enableLocalTimeline: false | 			enableLocalTimeline: false | ||||||
| 		}; | 		}; | ||||||
| @@ -71,9 +78,16 @@ export default Vue.extend({ | |||||||
| 			this.saveSrc(); | 			this.saveSrc(); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		list() { | 		list(x) { | ||||||
| 			this.showNav = false; | 			this.showNav = false; | ||||||
| 			this.saveSrc(); | 			this.saveSrc(); | ||||||
|  | 			if (x != null) this.tagTl = null; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		tagTl(x) { | ||||||
|  | 			this.showNav = false; | ||||||
|  | 			this.saveSrc(); | ||||||
|  | 			if (x != null) this.list = null; | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		showNav(v) { | 		showNav(v) { | ||||||
| @@ -94,6 +108,8 @@ export default Vue.extend({ | |||||||
| 			this.src = this.$store.state.device.tl.src; | 			this.src = this.$store.state.device.tl.src; | ||||||
| 			if (this.src == 'list') { | 			if (this.src == 'list') { | ||||||
| 				this.list = this.$store.state.device.tl.arg; | 				this.list = this.$store.state.device.tl.arg; | ||||||
|  | 			} else if (this.src == 'tag') { | ||||||
|  | 				this.tagTl = this.$store.state.device.tl.arg; | ||||||
| 			} | 			} | ||||||
| 		} else if (this.$store.state.i.followingCount == 0) { | 		} else if (this.$store.state.i.followingCount == 0) { | ||||||
| 			this.src = 'hybrid'; | 			this.src = 'hybrid'; | ||||||
| @@ -118,7 +134,7 @@ export default Vue.extend({ | |||||||
| 		saveSrc() { | 		saveSrc() { | ||||||
| 			this.$store.commit('device/setTl', { | 			this.$store.commit('device/setTl', { | ||||||
| 				src: this.src, | 				src: this.src, | ||||||
| 				arg: this.list | 				arg: this.src == 'list' ? this.list : this.tagTl | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ | |||||||
| 				<section> | 				<section> | ||||||
| 					<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch> | 					<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch> | ||||||
| 					<ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch> | 					<ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch> | ||||||
| 					<ui-switch v-model="animations">%i18n:common.enable-animations% (%i18n:common.this-setting-is-this-device-only%)</ui-switch> | 					<ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch> | ||||||
| 					<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch> | 					<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch> | ||||||
| 					<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch> | 					<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch> | ||||||
| 					<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch> | 					<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch> | ||||||
| @@ -169,9 +169,9 @@ export default Vue.extend({ | |||||||
| 			set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		animations: { | 		reduceMotion: { | ||||||
| 			get() { return this.$store.state.device.animations; }, | 			get() { return this.$store.state.device.reduceMotion; }, | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'animations', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); } | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		alwaysShowNsfw: { | 		alwaysShowNsfw: { | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ const defaultSettings = { | |||||||
| 	home: null, | 	home: null, | ||||||
| 	mobileHome: [], | 	mobileHome: [], | ||||||
| 	deck: null, | 	deck: null, | ||||||
|  | 	tagTimelines: [], | ||||||
| 	fetchOnScroll: true, | 	fetchOnScroll: true, | ||||||
| 	showMaps: true, | 	showMaps: true, | ||||||
| 	showPostFormOnTopOfTl: false, | 	showPostFormOnTopOfTl: false, | ||||||
| @@ -38,7 +39,7 @@ const defaultSettings = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const defaultDeviceSettings = { | const defaultDeviceSettings = { | ||||||
| 	animations: true, | 	reduceMotion: false, | ||||||
| 	apiViaStream: true, | 	apiViaStream: true, | ||||||
| 	autoPopout: false, | 	autoPopout: false, | ||||||
| 	darkmode: false, | 	darkmode: false, | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ html(lang= lang) | |||||||
| 		link(rel="stylesheet" href="/docs/assets/style.css") | 		link(rel="stylesheet" href="/docs/assets/style.css") | ||||||
| 		link(rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css") | 		link(rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css") | ||||||
| 		script(src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js") | 		script(src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js") | ||||||
| 		link(rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous") | 		link(rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous") | ||||||
| 		block meta | 		block meta | ||||||
|  |  | ||||||
| 	body | 	body | ||||||
|   | |||||||
| @@ -82,9 +82,13 @@ const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers: | |||||||
|  |  | ||||||
| 	text({ document }, { content }) { | 	text({ document }, { content }) { | ||||||
| 		const nodes = (content as string).split('\n').map(x => document.createTextNode(x)); | 		const nodes = (content as string).split('\n').map(x => document.createTextNode(x)); | ||||||
| 		for (const x of intersperse(document.createElement('br'), nodes)) { | 		for (const x of intersperse('br', nodes)) { | ||||||
|  | 			if (x === 'br') { | ||||||
|  | 				document.body.appendChild(document.createElement('br')); | ||||||
|  | 			} else { | ||||||
| 				document.body.appendChild(x); | 				document.body.appendChild(x); | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	url({ document }, { url }) { | 	url({ document }, { url }) { | ||||||
|   | |||||||
| @@ -2,12 +2,12 @@ | |||||||
|  * Replace fontawesome symbols |  * Replace fontawesome symbols | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| import * as fontawesome from '@fortawesome/fontawesome'; | import * as fontawesome from '@fortawesome/fontawesome-svg-core'; | ||||||
| import regular from '@fortawesome/fontawesome-free-regular'; | import { far } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import solid from '@fortawesome/fontawesome-free-solid'; | import { fas } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import brands from '@fortawesome/fontawesome-free-brands'; | import { fab } from '@fortawesome/free-brands-svg-icons'; | ||||||
|  |  | ||||||
| fontawesome.library.add(regular, solid, brands); | fontawesome.library.add(far, fas, fab); | ||||||
|  |  | ||||||
| export const pattern = /%fa:(.+?)%/g; | export const pattern = /%fa:(.+?)%/g; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,6 +17,8 @@ import Following from './following'; | |||||||
| const Note = db.get<INote>('notes'); | const Note = db.get<INote>('notes'); | ||||||
| Note.createIndex('uri', { sparse: true, unique: true }); | Note.createIndex('uri', { sparse: true, unique: true }); | ||||||
| Note.createIndex('userId'); | Note.createIndex('userId'); | ||||||
|  | Note.createIndex('mentions'); | ||||||
|  | Note.createIndex('visibleUserIds'); | ||||||
| Note.createIndex('tagsLower'); | Note.createIndex('tagsLower'); | ||||||
| Note.createIndex('_files.contentType'); | Note.createIndex('_files.contentType'); | ||||||
| Note.createIndex({ | Note.createIndex({ | ||||||
| @@ -24,6 +26,21 @@ Note.createIndex({ | |||||||
| }); | }); | ||||||
| export default Note; | export default Note; | ||||||
|  |  | ||||||
|  | // 後方互換性のため | ||||||
|  | Note.findOne({ | ||||||
|  | 	fileIds: { $exists: true } | ||||||
|  | }).then(n => { | ||||||
|  | 	if (n == null) { | ||||||
|  | 		Note.update({}, { | ||||||
|  | 			$rename: { | ||||||
|  | 				mediaIds: 'fileIds' | ||||||
|  | 			} | ||||||
|  | 		}, { | ||||||
|  | 			multi: true | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
| export function isValidText(text: string): boolean { | export function isValidText(text: string): boolean { | ||||||
| 	return length(text.trim()) <= 1000 && text.trim() != ''; | 	return length(text.trim()) <= 1000 && text.trim() != ''; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,22 +1,10 @@ | |||||||
| import { INote } from '../../../models/note'; | import { INote } from '../../../models/note'; | ||||||
| import toHtml from '../../../mfm/html'; | import toHtml from '../../../mfm/html'; | ||||||
| import parse from '../../../mfm/parse'; | import parse from '../../../mfm/parse'; | ||||||
| import config from '../../../config'; |  | ||||||
|  |  | ||||||
| export default function(note: INote) { | export default function(note: INote) { | ||||||
| 	let html = toHtml(parse(note.text), note.mentionedRemoteUsers); | 	let html = toHtml(parse(note.text), note.mentionedRemoteUsers); | ||||||
| 	if (html == null) html = ''; | 	if (html == null) html = ''; | ||||||
|  |  | ||||||
| 	if (note.poll != null) { |  | ||||||
| 		const url = `${config.url}/notes/${note._id}`; |  | ||||||
| 		// TODO: i18n |  | ||||||
| 		html += `<p><a href="${url}">【Misskeyで投票を見る】</a></p>`; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if (note.renoteId != null) { |  | ||||||
| 		const url = `${config.url}/notes/${note.renoteId}`; |  | ||||||
| 		html += `<p>RE: <a href="${url}">${url}</a></p>`; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return html; | 	return html; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import DriveFile, { IDriveFile } from '../../../models/drive-file'; | |||||||
| import Note, { INote } from '../../../models/note'; | import Note, { INote } from '../../../models/note'; | ||||||
| import User from '../../../models/user'; | import User from '../../../models/user'; | ||||||
| import toHtml from '../misc/get-note-html'; | import toHtml from '../misc/get-note-html'; | ||||||
|  | import parseMfm from '../../../mfm/parse'; | ||||||
|  |  | ||||||
| export default async function renderNote(note: INote, dive = true): Promise<any> { | export default async function renderNote(note: INote, dive = true): Promise<any> { | ||||||
| 	const promisedFiles: Promise<IDriveFile[]> = note.fileIds | 	const promisedFiles: Promise<IDriveFile[]> = note.fileIds | ||||||
| @@ -81,13 +82,39 @@ export default async function renderNote(note: INote, dive = true): Promise<any> | |||||||
|  |  | ||||||
| 	const files = await promisedFiles; | 	const files = await promisedFiles; | ||||||
|  |  | ||||||
|  | 	let text = note.text; | ||||||
|  |  | ||||||
|  | 	if (note.poll != null) { | ||||||
|  | 		if (text == null) text = ''; | ||||||
|  | 		const url = `${config.url}/notes/${note._id}`; | ||||||
|  | 		// TODO: i18n | ||||||
|  | 		text += `\n\n[投票を見る](${url})`; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (note.renoteId != null) { | ||||||
|  | 		if (text == null) text = ''; | ||||||
|  | 		const url = `${config.url}/notes/${note.renoteId}`; | ||||||
|  | 		text += `\n\nRE: ${url}`; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 省略されたメンションのホストを復元する | ||||||
|  | 	if (text != null) { | ||||||
|  | 		text = parseMfm(text).map(x => { | ||||||
|  | 			if (x.type == 'mention' && x.host == null) { | ||||||
|  | 				return `${x.content}@${config.host}`; | ||||||
|  | 			} else { | ||||||
|  | 				return x.content; | ||||||
|  | 			} | ||||||
|  | 		}).join(''); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return { | 	return { | ||||||
| 		id: `${config.url}/notes/${note._id}`, | 		id: `${config.url}/notes/${note._id}`, | ||||||
| 		type: 'Note', | 		type: 'Note', | ||||||
| 		attributedTo, | 		attributedTo, | ||||||
| 		summary: note.cw, | 		summary: note.cw, | ||||||
| 		content: toHtml(note), | 		content: toHtml(Object.assign({}, note, { text })), | ||||||
| 		_misskey_content_: note.text, | 		_misskey_content: text, | ||||||
| 		published: note.createdAt.toISOString(), | 		published: note.createdAt.toISOString(), | ||||||
| 		to, | 		to, | ||||||
| 		cc, | 		cc, | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import Note from '../../../../models/note'; | |||||||
| import { getFriendIds } from '../../common/get-friends'; | import { getFriendIds } from '../../common/get-friends'; | ||||||
| import { pack } from '../../../../models/note'; | import { pack } from '../../../../models/note'; | ||||||
| import { ILocalUser } from '../../../../models/user'; | import { ILocalUser } from '../../../../models/user'; | ||||||
|  | import getParams from '../../get-params'; | ||||||
|  |  | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
| @@ -10,42 +11,48 @@ export const meta = { | |||||||
| 		'en-US': 'Get mentions of myself.' | 		'en-US': 'Get mentions of myself.' | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	requireCredential: true | 	requireCredential: true, | ||||||
|  |  | ||||||
|  | 	params: { | ||||||
|  | 		following: $.bool.optional.note({ | ||||||
|  | 			default: false | ||||||
|  | 		}), | ||||||
|  |  | ||||||
|  | 		limit: $.num.optional.range(1, 100).note({ | ||||||
|  | 			default: 10 | ||||||
|  | 		}), | ||||||
|  |  | ||||||
|  | 		sinceId: $.type(ID).optional.note({ | ||||||
|  | 		}), | ||||||
|  |  | ||||||
|  | 		untilId: $.type(ID).optional.note({ | ||||||
|  | 		}), | ||||||
|  | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { | export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { | ||||||
| 	// Get 'following' parameter | 	const [ps, psErr] = getParams(meta, params); | ||||||
| 	const [following = false, followingError] = | 	if (psErr) throw psErr; | ||||||
| 		$.bool.optional.get(params.following); |  | ||||||
| 	if (followingError) return rej('invalid following param'); |  | ||||||
|  |  | ||||||
| 	// Get 'limit' parameter |  | ||||||
| 	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); |  | ||||||
| 	if (limitErr) return rej('invalid limit param'); |  | ||||||
|  |  | ||||||
| 	// Get 'sinceId' parameter |  | ||||||
| 	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); |  | ||||||
| 	if (sinceIdErr) return rej('invalid sinceId param'); |  | ||||||
|  |  | ||||||
| 	// Get 'untilId' parameter |  | ||||||
| 	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); |  | ||||||
| 	if (untilIdErr) return rej('invalid untilId param'); |  | ||||||
|  |  | ||||||
| 	// Check if both of sinceId and untilId is specified | 	// Check if both of sinceId and untilId is specified | ||||||
| 	if (sinceId && untilId) { | 	if (ps.sinceId && ps.untilId) { | ||||||
| 		return rej('cannot set sinceId and untilId'); | 		return rej('cannot set sinceId and untilId'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Construct query | 	// Construct query | ||||||
| 	const query = { | 	const query = { | ||||||
|  | 		$or: [{ | ||||||
| 			mentions: user._id | 			mentions: user._id | ||||||
|  | 		}, { | ||||||
|  | 			visibleUserIds: user._id | ||||||
|  | 		}] | ||||||
| 	} as any; | 	} as any; | ||||||
|  |  | ||||||
| 	const sort = { | 	const sort = { | ||||||
| 		_id: -1 | 		_id: -1 | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	if (following) { | 	if (ps.following) { | ||||||
| 		const followingIds = await getFriendIds(user._id); | 		const followingIds = await getFriendIds(user._id); | ||||||
|  |  | ||||||
| 		query.userId = { | 		query.userId = { | ||||||
| @@ -53,26 +60,24 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = | |||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (sinceId) { | 	if (ps.sinceId) { | ||||||
| 		sort._id = 1; | 		sort._id = 1; | ||||||
| 		query._id = { | 		query._id = { | ||||||
| 			$gt: sinceId | 			$gt: ps.sinceId | ||||||
| 		}; | 		}; | ||||||
| 	} else if (untilId) { | 	} else if (ps.untilId) { | ||||||
| 		query._id = { | 		query._id = { | ||||||
| 			$lt: untilId | 			$lt: ps.untilId | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Issue query | 	// Issue query | ||||||
| 	const mentions = await Note | 	const mentions = await Note | ||||||
| 		.find(query, { | 		.find(query, { | ||||||
| 			limit: limit, | 			limit: ps.limit, | ||||||
| 			sort: sort | 			sort: sort | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 	// Serialize | 	// Serialize | ||||||
| 	res(await Promise.all(mentions.map(async mention => | 	res(await Promise.all(mentions.map(mention => pack(mention, user)))); | ||||||
| 		await pack(mention, user) |  | ||||||
| 	))); |  | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -13,12 +13,18 @@ export const meta = { | |||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	params: { | 	params: { | ||||||
| 		tag: $.str.note({ | 		tag: $.str.optional.note({ | ||||||
| 			desc: { | 			desc: { | ||||||
| 				'ja-JP': 'タグ' | 				'ja-JP': 'タグ' | ||||||
| 			} | 			} | ||||||
| 		}), | 		}), | ||||||
|  |  | ||||||
|  | 		query: $.arr($.arr($.str)).optional.note({ | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'クエリ' | ||||||
|  | 			} | ||||||
|  | 		}), | ||||||
|  |  | ||||||
| 		includeUserIds: $.arr($.type(ID)).optional.note({ | 		includeUserIds: $.arr($.type(ID)).optional.note({ | ||||||
| 			default: [] | 			default: [] | ||||||
| 		}), | 		}), | ||||||
| @@ -59,11 +65,9 @@ export const meta = { | |||||||
| 			} | 			} | ||||||
| 		}), | 		}), | ||||||
|  |  | ||||||
| 		withFiles: $.bool.optional.nullable.note({ | 		withFiles: $.bool.optional.note({ | ||||||
| 			default: null, |  | ||||||
|  |  | ||||||
| 			desc: { | 			desc: { | ||||||
| 				'ja-JP': 'ファイルが添付された投稿に限定するか否か' | 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' | ||||||
| 			} | 			} | ||||||
| 		}), | 		}), | ||||||
|  |  | ||||||
| @@ -83,6 +87,12 @@ export const meta = { | |||||||
| 			} | 			} | ||||||
| 		}), | 		}), | ||||||
|  |  | ||||||
|  | 		untilId: $.type(ID).optional.note({ | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' | ||||||
|  | 			} | ||||||
|  | 		}), | ||||||
|  |  | ||||||
| 		sinceDate: $.num.optional.note({ | 		sinceDate: $.num.optional.note({ | ||||||
| 		}), | 		}), | ||||||
|  |  | ||||||
| @@ -126,8 +136,14 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const q: any = { | 	const q: any = { | ||||||
| 		$and: [{ | 		$and: [ps.tag ? { | ||||||
| 			tagsLower: ps.tag.toLowerCase() | 			tagsLower: ps.tag.toLowerCase() | ||||||
|  | 		} : { | ||||||
|  | 			$or: ps.query.map(tags => ({ | ||||||
|  | 				$and: tags.map(t => ({ | ||||||
|  | 					tagsLower: t.toLowerCase() | ||||||
|  | 				})) | ||||||
|  | 			})) | ||||||
| 		}], | 		}], | ||||||
| 		deletedAt: { $exists: false } | 		deletedAt: { $exists: false } | ||||||
| 	}; | 	}; | ||||||
| @@ -281,25 +297,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => | |||||||
|  |  | ||||||
| 	const withFiles = ps.withFiles != null ? ps.withFiles : ps.media; | 	const withFiles = ps.withFiles != null ? ps.withFiles : ps.media; | ||||||
|  |  | ||||||
| 	if (withFiles != null) { |  | ||||||
| 	if (withFiles) { | 	if (withFiles) { | ||||||
| 		push({ | 		push({ | ||||||
| 				fileIds: { | 			fileIds: { $exists: true, $ne: [] } | ||||||
| 					$exists: true, |  | ||||||
| 					$ne: null |  | ||||||
| 				} |  | ||||||
| 		}); | 		}); | ||||||
| 		} else { |  | ||||||
| 			push({ |  | ||||||
| 				$or: [{ |  | ||||||
| 					fileIds: { |  | ||||||
| 						$exists: false |  | ||||||
| 					} |  | ||||||
| 				}, { |  | ||||||
| 					fileIds: null |  | ||||||
| 				}] |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (ps.poll != null) { | 	if (ps.poll != null) { | ||||||
| @@ -323,6 +324,14 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.untilId) { | ||||||
|  | 		push({ | ||||||
|  | 			_id: { | ||||||
|  | 				$lt: ps.untilId | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if (ps.sinceDate) { | 	if (ps.sinceDate) { | ||||||
| 		push({ | 		push({ | ||||||
| 			createdAt: { | 			createdAt: { | ||||||
|   | |||||||
							
								
								
									
										48
									
								
								src/server/api/stream/hashtag.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/server/api/stream/hashtag.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | import * as websocket from 'websocket'; | ||||||
|  | import Xev from 'xev'; | ||||||
|  |  | ||||||
|  | import { IUser } from '../../../models/user'; | ||||||
|  | import Mute from '../../../models/mute'; | ||||||
|  | import { pack } from '../../../models/note'; | ||||||
|  |  | ||||||
|  | export default async function( | ||||||
|  | 	request: websocket.request, | ||||||
|  | 	connection: websocket.connection, | ||||||
|  | 	subscriber: Xev, | ||||||
|  | 	user?: IUser | ||||||
|  | ) { | ||||||
|  | 	const mute = user ? await Mute.find({ muterId: user._id }) : null; | ||||||
|  | 	const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; | ||||||
|  |  | ||||||
|  | 	const q: Array<string[]> = JSON.parse((request.resourceURL.query as any).q); | ||||||
|  |  | ||||||
|  | 	// Subscribe stream | ||||||
|  | 	subscriber.on('hashtag', async note => { | ||||||
|  | 		const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase()))); | ||||||
|  | 		if (!matched) return; | ||||||
|  |  | ||||||
|  | 		// Renoteなら再pack | ||||||
|  | 		if (note.renoteId != null) { | ||||||
|  | 			note.renote = await pack(note.renoteId, user, { | ||||||
|  | 				detail: true | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する | ||||||
|  | 		if (mutedUserIds.indexOf(note.userId) != -1) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		//#endregion | ||||||
|  |  | ||||||
|  | 		connection.send(JSON.stringify({ | ||||||
|  | 			type: 'note', | ||||||
|  | 			body: note | ||||||
|  | 		})); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
| @@ -14,6 +14,7 @@ import reversiGameStream from './stream/games/reversi-game'; | |||||||
| import reversiStream from './stream/games/reversi'; | import reversiStream from './stream/games/reversi'; | ||||||
| import serverStatsStream from './stream/server-stats'; | import serverStatsStream from './stream/server-stats'; | ||||||
| import notesStatsStream from './stream/notes-stats'; | import notesStatsStream from './stream/notes-stats'; | ||||||
|  | import hashtagStream from './stream/hashtag'; | ||||||
| import { ParsedUrlQuery } from 'querystring'; | import { ParsedUrlQuery } from 'querystring'; | ||||||
| import authenticate from './authenticate'; | import authenticate from './authenticate'; | ||||||
|  |  | ||||||
| @@ -44,6 +45,12 @@ module.exports = (server: http.Server) => { | |||||||
| 			ev.removeAllListeners(); | 			ev.removeAllListeners(); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
|  | 		connection.on('message', async (data) => { | ||||||
|  | 			if (data.utf8Data == 'ping') { | ||||||
|  | 				connection.send('pong'); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  |  | ||||||
| 		const q = request.resourceURL.query as ParsedUrlQuery; | 		const q = request.resourceURL.query as ParsedUrlQuery; | ||||||
| 		const [user, app] = await authenticate(q.i as string); | 		const [user, app] = await authenticate(q.i as string); | ||||||
|  |  | ||||||
| @@ -57,6 +64,11 @@ module.exports = (server: http.Server) => { | |||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if (request.resourceURL.pathname === '/hashtag') { | ||||||
|  | 			hashtagStream(request, connection, ev, user); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if (user == null) { | 		if (user == null) { | ||||||
| 			connection.send('authentication-failed'); | 			connection.send('authentication-failed'); | ||||||
| 			connection.close(); | 			connection.close(); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import es from '../../db/elasticsearch'; | import es from '../../db/elasticsearch'; | ||||||
| import Note, { pack, INote } from '../../models/note'; | import Note, { pack, INote } from '../../models/note'; | ||||||
| import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user'; | import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user'; | ||||||
| import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../stream'; | import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../../stream'; | ||||||
| import Following from '../../models/following'; | import Following from '../../models/following'; | ||||||
| import { deliver } from '../../queue'; | import { deliver } from '../../queue'; | ||||||
| import renderNote from '../../remote/activitypub/renderer/note'; | import renderNote from '../../remote/activitypub/renderer/note'; | ||||||
| @@ -138,6 +138,10 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< | |||||||
|  |  | ||||||
| 	const mentionedUsers = await extractMentionedUsers(tokens); | 	const mentionedUsers = await extractMentionedUsers(tokens); | ||||||
|  |  | ||||||
|  | 	if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) { | ||||||
|  | 		mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	const note = await insertNote(user, data, tags, mentionedUsers); | 	const note = await insertNote(user, data, tags, mentionedUsers); | ||||||
|  |  | ||||||
| 	res(note); | 	res(note); | ||||||
| @@ -177,6 +181,10 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< | |||||||
| 		noteObj.isFirstNote = true; | 		noteObj.isFirstNote = true; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if (tags.length > 0) { | ||||||
|  | 		publishHashtagStream(noteObj); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	const nm = new NotificationManager(user, note); | 	const nm = new NotificationManager(user, note); | ||||||
| 	const nmRelatedPromises = []; | 	const nmRelatedPromises = []; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -78,6 +78,10 @@ class Publisher { | |||||||
| 	public publishGlobalTimelineStream = (note: any): void => { | 	public publishGlobalTimelineStream = (note: any): void => { | ||||||
| 		this.publish('global-timeline', null, note); | 		this.publish('global-timeline', null, note); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	public publishHashtagStream = (note: any): void => { | ||||||
|  | 		this.publish('hashtag', null, note); | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| const publisher = new Publisher(); | const publisher = new Publisher(); | ||||||
| @@ -95,3 +99,4 @@ export const publishReversiGameStream = publisher.publishReversiGameStream; | |||||||
| export const publishLocalTimelineStream = publisher.publishLocalTimelineStream; | export const publishLocalTimelineStream = publisher.publishLocalTimelineStream; | ||||||
| export const publishHybridTimelineStream = publisher.publishHybridTimelineStream; | export const publishHybridTimelineStream = publisher.publishHybridTimelineStream; | ||||||
| export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream; | export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream; | ||||||
|  | export const publishHashtagStream = publisher.publishHashtagStream; | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as assert from 'assert'; | import * as assert from 'assert'; | ||||||
|  |  | ||||||
| import analyze from '../src/mfm/parse'; | import analyze from '../src/mfm/parse'; | ||||||
|  | import toHtml from '../src/mfm/html'; | ||||||
| import syntaxhighlighter from '../src/mfm/parse/core/syntax-highlighter'; | import syntaxhighlighter from '../src/mfm/parse/core/syntax-highlighter'; | ||||||
|  |  | ||||||
| describe('Text', () => { | describe('Text', () => { | ||||||
| @@ -170,4 +171,12 @@ describe('Text', () => { | |||||||
| 			assert.equal(html, '<span class="symbol">/</span>'); | 			assert.equal(html, '<span class="symbol">/</span>'); | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | 	describe('toHtml', () => { | ||||||
|  | 		it('br', () => { | ||||||
|  | 			const input = 'foo\nbar\nbaz'; | ||||||
|  | 			const output = '<p>foo<br>bar<br>baz</p>'; | ||||||
|  | 			assert.equal(toHtml(analyze(input)), output); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
| }); | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user