Compare commits

...

69 Commits

Author SHA1 Message Date
f00f5cbed1 10.29.0 2018-10-20 15:51:47 +09:00
c4e8cabae9 ファイル作成APIにforceオプションを実装 2018-10-20 15:50:13 +09:00
1729d05e8c Localized mark all as read message (#2956)
* Localized read-all message

* Fixed some weirdness:   src/client/app/init.ts

* Unfucked server api stuff (sorry lol):   src/server/api/endpoints/i/read_all_unread_notes.ts

* Clean up
2018-10-20 15:46:01 +09:00
770fb46ca7 Improve usability 2018-10-20 15:41:27 +09:00
a3c4e54bc0 🎨 2018-10-20 15:37:17 +09:00
b8a77fbada Show image info in tooltip 2018-10-20 15:21:24 +09:00
c2663529c1 Localized BSoD messages. (#2953)
* Added VSCode workspace files to :   .gitignore

* Localized Blue Screen of Death:   locales/ja-JP.yml
Localized Blue Screen of Death:   src/client/app/init.ts
2018-10-20 11:57:23 +09:00
9df74a02b6 Fix bug 2018-10-20 11:24:02 +09:00
71c9964e19 Fix bug and clean up 2018-10-20 11:19:27 +09:00
ae2e47f6a9 Merge pull request #2950 from syuilo/l10n_develop
New Crowdin translations
2018-10-20 09:45:44 +09:00
1524d35f66 New translations ja-JP.yml (English) 2018-10-20 09:41:11 +09:00
845be966a0 10.28.0 2018-10-20 09:39:56 +09:00
80818d79eb Fix bug 2018-10-20 09:38:36 +09:00
cb9b3c00dd Use router-link instead of a to improve usability 2018-10-20 09:34:34 +09:00
b3997fb5df New translations ja-JP.yml (Norwegian) 2018-10-20 09:33:07 +09:00
09dde6b78a New translations ja-JP.yml (Dutch) 2018-10-20 09:32:57 +09:00
3345d3ab35 New translations ja-JP.yml (Japanese, Kansai) 2018-10-20 09:32:51 +09:00
366be7bbdd New translations ja-JP.yml (Spanish) 2018-10-20 09:32:47 +09:00
7008ea66f8 New translations ja-JP.yml (Russian) 2018-10-20 09:32:43 +09:00
70f881e989 New translations ja-JP.yml (Portuguese) 2018-10-20 09:32:38 +09:00
94d2355089 New translations ja-JP.yml (Polish) 2018-10-20 09:32:33 +09:00
dfbe48b25b New translations ja-JP.yml (Korean) 2018-10-20 09:32:29 +09:00
931cb38b54 New translations ja-JP.yml (Italian) 2018-10-20 09:32:23 +09:00
e5fd34f94e New translations ja-JP.yml (German) 2018-10-20 09:32:17 +09:00
c638d7eb48 New translations ja-JP.yml (French) 2018-10-20 09:32:12 +09:00
7e96384618 New translations ja-JP.yml (English) 2018-10-20 09:32:08 +09:00
829cb99f5b New translations ja-JP.yml (Chinese Simplified) 2018-10-20 09:32:04 +09:00
1f93c99304 New translations ja-JP.yml (Catalan) 2018-10-20 09:32:00 +09:00
dbb7c756cd Fix bug 2018-10-20 09:31:56 +09:00
13f381710c Validate param 2018-10-20 09:31:52 +09:00
70897c0e9a 🎨 2018-10-20 09:28:48 +09:00
f51d1c5264 Fix test 2018-10-20 09:14:16 +09:00
70d0937aab Fix #2949 2018-10-20 09:03:04 +09:00
7d1ab6102f 10.27.0 2018-10-20 08:04:52 +09:00
77ddd778be ハッシュタグもデッキ内ナビゲーションするように 2018-10-20 08:03:45 +09:00
890ecb693f Improve 2018-10-20 07:28:01 +09:00
209fe7dcaf Improve deck usability 2018-10-20 07:21:22 +09:00
e0d6f7c7c4 RP --> RN 2018-10-20 07:01:09 +09:00
5d3fe9599b Improve performance 2018-10-20 06:42:19 +09:00
0fe0b6d254 10.26.0 2018-10-20 02:52:11 +09:00
b794216eaf Merge pull request #2940 from syuilo/l10n_develop
New Crowdin translations
2018-10-20 02:51:43 +09:00
1fccde38f6 デッキのキーボードショートカットを強化 2018-10-20 02:49:39 +09:00
41bd436d3e デッキのキーボードショートカットを強化 2018-10-20 02:40:37 +09:00
c66155ed48 Improve shortcut key detection 2018-10-20 01:45:31 +09:00
627bd410fa New translations ja-JP.yml (German) 2018-10-19 22:22:36 +09:00
41a3932c6b New translations ja-JP.yml (English) 2018-10-19 22:12:39 +09:00
785b8d7846 New translations ja-JP.yml (French) 2018-10-19 21:02:41 +09:00
622c8f9598 New translations ja-JP.yml (French) 2018-10-19 20:52:24 +09:00
ef978a6364 10.25.0 2018-10-19 15:07:46 +09:00
d95fbe1c6b fix(package): update reconnecting-websocket to version 4.1.10 (#2937) 2018-10-19 15:04:46 +09:00
d4ffddc2ab Merge pull request #2936 from syuilo/l10n_develop
New Crowdin translations
2018-10-19 15:04:36 +09:00
3d497cedfc デッキで'T'のショートカットを使えるように 2018-10-19 15:03:23 +09:00
e8de29ae79 Resolve #2935 2018-10-19 14:34:51 +09:00
b622946844 10.24.0 2018-10-19 11:14:27 +09:00
d013f78cc7 New translations ja-JP.yml (Norwegian) 2018-10-19 11:12:15 +09:00
2afbafdb3b New translations ja-JP.yml (Dutch) 2018-10-19 11:12:11 +09:00
67148114a8 New translations ja-JP.yml (Japanese, Kansai) 2018-10-19 11:12:05 +09:00
7903140ec2 New translations ja-JP.yml (Spanish) 2018-10-19 11:12:01 +09:00
cefd296200 New translations ja-JP.yml (Russian) 2018-10-19 11:11:55 +09:00
99d1c15851 New translations ja-JP.yml (Portuguese) 2018-10-19 11:11:51 +09:00
a3107ab26f New translations ja-JP.yml (Polish) 2018-10-19 11:11:46 +09:00
854cfae75b New translations ja-JP.yml (Korean) 2018-10-19 11:11:42 +09:00
36ab82957d New translations ja-JP.yml (Italian) 2018-10-19 11:11:36 +09:00
de9f54386c New translations ja-JP.yml (German) 2018-10-19 11:11:32 +09:00
7f43820765 New translations ja-JP.yml (French) 2018-10-19 11:11:28 +09:00
955e907e7f New translations ja-JP.yml (English) 2018-10-19 11:11:24 +09:00
4c18022e7d New translations ja-JP.yml (Chinese Simplified) 2018-10-19 11:11:20 +09:00
509f59e46d New translations ja-JP.yml (Catalan) 2018-10-19 11:11:16 +09:00
f14c372f5e Resolve #2719 2018-10-19 11:10:49 +09:00
56 changed files with 627 additions and 205 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ api-docs.json
/redis
/mongo
/elasticsearch
*.code-workspace

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "引き分け"
my-turn: "あなたのターンです"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "デザインと表示"
customize: "ホームをカスタマイズ"
wallpaper: "壁紙"

View File

@ -32,7 +32,7 @@ common:
paragraph2: "一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。"
paragraph3: "ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。"
paragraph4: "カスタマイズを終了するには、右上の「完了」をクリックします。"
gotit: "Got it!"
gotit: "Verstanden!"
notification:
file-uploaded: "Datei hochgeladen!"
message-from: "Nachricht von {}:"
@ -115,6 +115,9 @@ common:
reduce-motion: "Animationen der Benutzeroberfläche reduzieren"
this-setting-is-this-device-only: "Nur auf diesem Gerät"
do-not-use-in-production: 'Dies ist eine Entwicklungsversion. Nicht in einer Produktionsumgebung verwenden.'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "Unentschieden"
my-turn: "Du bist am Zug"
@ -175,7 +178,7 @@ auth/views/form.vue:
permission-ask: "このアプリは次の権限を要求しています:"
account-read: "アカウントの情報を見る。"
account-write: "アカウントの情報を操作する。"
note-write: "投稿する。"
note-write: "Senden."
like-write: "いいねしたりいいね解除する。"
following-write: "フォローしたりフォロー解除する。"
drive-read: "ドライブを見る。"
@ -186,14 +189,14 @@ auth/views/form.vue:
accept: "Zugriff erlauben."
auth/views/index.vue:
loading: "Lädt"
denied: "アプリケーションの連携をキャンセルしました。"
denied: "Autorisierung der Anwendung wurde verweigert."
denied-paragraph: "このアプリがあなたのアカウントにアクセスすることはありません。"
already-authorized: "このアプリは既に連携済みです"
allowed: "アプリケーションの連携を許可しました"
already-authorized: "Diese Anwendung ist bereits autorisiert."
allowed: "Autorisierung der Anwendung wurde erlaubt."
callback-url: "アプリケーションに戻っています"
please-go-back: "アプリケーションに戻って、やっていってください。"
error: "セッションが存在しません。"
sign-in: "サインインしてください"
please-go-back: "Bitte gehen Sie zurück zur Anwendung."
error: "Sitzung ist nicht vorhanden."
sign-in: "Bitte melde dich an."
common/views/components/games/reversi/reversi.vue:
matching:
waiting-for: "Warten auf {}"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "API-Anfrage über WebSocket statt native Aktualisierungs-API (für bessere Leistung). Diese Einstellung wird im Browser gespeichert."
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "Erscheinungsbild und Anzeige"
customize: "Startseite anpassen"
wallpaper: "壁紙"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "Reduce motion in UI"
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.'
error:
title: 'Something happened :('
retry: 'Retry'
reversi:
drawn: "Draw"
my-turn: "Your turn"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "API request is performed via the WebSocket connection instead of native fetch API (for better performance). This setting is stored in the browser."
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "Use Deck as default UI"
display: "Design and display"
customize: "Customize home layout"
wallpaper: "Wallpaper"
@ -884,7 +888,7 @@ desktop/views/components/ui.header.account.vue:
admin: "Admin"
settings: "Settings"
signout: "Sign out"
dark: "Submerge in dark"
dark: "Toggle dark mode"
desktop/views/components/ui.header.nav.vue:
home: "Home"
deck: "Deck"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'Esto está en desarrollo, no usarlo para producción.'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "Empatado"
my-turn: "Mi turno"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "Las peticiones de las API se realizan por conexiones WebSocket en lugar de las tradicionales (para una mejora en el rendimiento). Esta función depende del navegador."
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "Diseño y pantalla"
customize: "Personaliza la página principal"
wallpaper: "壁紙"

View File

@ -106,15 +106,18 @@ common:
my-token-regenerated: "Votre jeton vient dêtre généré, vous allez maintenant être déconnecté."
i-like-sushi: "Je préfère les sushis plutôt que le pudding"
show-reversi-board-labels: "Afficher les étiquettes des lignes et colonnes dans Reversi"
use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける"
use-contrast-reversi-stones: "Icône avec contraste sur Reversi"
verified-user: "Compte vérifié"
disable-animated-mfm: "Désactiver les textes animés dans les publications"
always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
always-show-nsfw: "Toujours afficher les contenus sensibles"
always-mark-nsfw: "Toujours marquer les notes ayant des attachements comme sensibles"
show-full-acct: "Afficher ladresse complète de lutilisateur"
reduce-motion: "Réduire les animations dans linterface utilisateur"
this-setting-is-this-device-only: "Uniquement sur cet appareil"
do-not-use-in-production: 'Il sagit dune version de développement. Ne pas utiliser dans un environnement de production.'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "Partie nulle"
my-turn: "Cest votre tour"
@ -417,24 +420,24 @@ common/views/components/trends.vue:
count: "{} utilisateurs·rices mentionnés·es"
empty: "Aucune tendance"
common/views/components/profile-editor.vue:
title: "プロフィール"
name: "名前"
account: "アカウント"
location: "場所"
description: "自己紹介"
birthday: "誕生日"
avatar: "アイコン"
banner: "バナー"
is-cat: "このアカウントはCatです"
is-bot: "このアカウントはBotです"
is-locked: "フォローを承認制にする"
title: "Profil"
name: "Nom"
account: "Compte"
location: "Lieu"
description: "À propos de moi"
birthday: "Date de naissance"
avatar: "Avatar"
banner: "Bannière"
is-cat: "Ce compte est un Chat"
is-bot: "Ce compte est un Bot"
is-locked: "Demandes dabonnements requièrent lapprobation"
careful-bot: "Botからのフォローだけ承認制にする"
advanced: "その他"
privacy: "プライバシー"
save: "保存"
saved: "プロフィールを保存しました"
uploading: "アップロード中"
upload-failed: "アップロードに失敗しました"
advanced: "Avancé"
privacy: "Vie privée"
save: "Mettre à jour le profil"
saved: "Profil mis à jour avec succès"
uploading: "En cours d'envoi …"
upload-failed: "Échec de l'envoi"
common/views/widgets/broadcast.vue:
fetching: "Récupération"
no-broadcasts: "Aucune annonce"
@ -660,9 +663,9 @@ desktop/views/components/note-detail.vue:
renote: "Republier"
add-reaction: "Ajouter votre reaction"
desktop/views/components/note.vue:
reposted-by: "{}がRenote"
reply: "返信"
renote: "Renote"
reposted-by: "Partagé par {}"
reply: "Répondre"
renote: "Partager"
add-reaction: "リアクション"
detail: "詳細"
private: "この投稿は非公開です"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "Affichage et design"
customize: "Personnaliser l'Accueil"
wallpaper: "壁紙"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "引き分け"
my-turn: "あなたのターンです"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "デザインと表示"
customize: "ホームをカスタマイズ"
wallpaper: "壁紙"

View File

@ -25,6 +25,15 @@ common:
application-authorization: "アプリの連携"
close: "閉じる"
do-not-copy-paste: "ここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。"
BSoD:
fatal-error: ":( 致命的な問題が発生しました。"
update-browser-os: "お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。"
error-code: "エラーコード"
browser-version: "ブラウザ バージョン"
client-version: "クライアント バージョン"
email-support: "問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。"
thanks: "Thank you for using Misskey."
got-it: "わかった"
customization-tips:
title: "カスタマイズのヒント"
@ -124,6 +133,10 @@ common:
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "引き分け"
my-turn: "あなたのターンです"
@ -836,6 +849,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "デザインと表示"
customize: "ホームをカスタマイズ"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UI、動き過ぎや、静かにしてや"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: '開発ビルドや。本番環境で使わんといて!知らんで!'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "おあいこ"
my-turn: "あんさんのターンや"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、WebSocket接続を経由してAPIリクエストが行われんで(パフォーマンス向上するかも、知らんけど)。オフにすると、ネイティブの fetch API が利用されるで。この設定はこのデバイスのみ有効やで。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "見た感じ"
customize: "ホームをカスタマイズ"
wallpaper: "壁紙"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "이 장치만"
do-not-use-in-production: '이것은 개발 빌드입니다. 프로덕션 환경에서 사용하지 마십시오.'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "무승부"
my-turn: "당신의 차례입니다"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "デザインと表示"
customize: "ホームをカスタマイズ"
wallpaper: "壁紙"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "引き分け"
my-turn: "あなたのターンです"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "API-verzoek wordt uitgevoerd via de WebSocket-verbinding i.p.v. de ingebouwde ophaal-API (voor verbeterde prestaties). Deze instelling wordt opgeslagen in je browser."
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "Ontwerp en weergave"
customize: "Startpagina aanpassen"
wallpaper: "壁紙"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "引き分け"
my-turn: "あなたのターンです"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "デザインと表示"
customize: "ホームをカスタマイズ"
wallpaper: "壁紙"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "Remis"
my-turn: "Twoja kolej"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "Wygląd i wyświetlanie"
customize: "Dostosuj stronę główną"
wallpaper: "壁紙"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "Empatado"
my-turn: "Seu turno"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "デザインと表示"
customize: "ホームをカスタマイズ"
wallpaper: "壁紙"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "引き分け"
my-turn: "あなたのターンです"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "デザインと表示"
customize: "ホームをカスタマイズ"
wallpaper: "壁紙"

View File

@ -115,6 +115,9 @@ common:
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
error:
title: '問題が発生しました'
retry: 'やり直す'
reversi:
drawn: "引き分け"
my-turn: "あなたのターンです"
@ -747,6 +750,7 @@ desktop/views/components/settings.vue:
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "デザインと表示"
customize: "ホームをカスタマイズ"
wallpaper: "壁紙"

View File

@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.23.1",
"clientVersion": "1.0.10732",
"version": "10.29.0",
"clientVersion": "1.0.10801",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@ -179,7 +179,7 @@
"qrcode": "1.3.0",
"ratelimiter": "3.2.0",
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.1.9",
"reconnecting-websocket": "4.1.10",
"redis": "2.8.0",
"request": "2.88.0",
"request-promise-native": "1.0.5",

View File

@ -46,6 +46,16 @@ const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): a
const ignoreElemens = ['input', 'textarea'];
function match(e: KeyboardEvent, patterns: action['patterns']): boolean {
const key = e.code.toLowerCase();
return patterns.some(pattern => pattern.which.includes(key) &&
pattern.ctrl == e.ctrlKey &&
pattern.shift == e.shiftKey &&
pattern.alt == e.altKey &&
e.metaKey == false
);
}
export default {
install(Vue) {
Vue.directive('hotkey', {
@ -55,37 +65,27 @@ export default {
const actions = getKeyMap(binding.value);
// flatten
const reservedKeys = concat(concat(actions.map(a => a.patterns.map(p => p.which))));
const reservedKeys = concat(actions.map(a => a.patterns));
el.dataset.reservedKeys = reservedKeys.map(key => `'${key}'`).join(' ');
el._misskey_reservedKeys = reservedKeys;
el._keyHandler = (e: KeyboardEvent) => {
const key = e.code.toLowerCase();
const targetReservedKeys = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeys || '' : '';
const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : [];
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
for (const action of actions) {
if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break;
const matched = action.patterns.some(pattern => {
const matched = pattern.which.includes(key) &&
pattern.ctrl == e.ctrlKey &&
pattern.shift == e.shiftKey &&
pattern.alt == e.altKey &&
e.metaKey == false;
if (matched) {
e.preventDefault();
e.stopPropagation();
action.callback(e);
return true;
} else {
return false;
}
});
const matched = match(e, action.patterns);
if (matched) {
if (el._hotkey_global) {
if (match(e, targetReservedKeys)) {
return;
}
}
e.preventDefault();
e.stopPropagation();
action.callback(e);
break;
}
}

View File

@ -29,14 +29,18 @@ export default (opts: Opts = {}) => ({
computed: {
keymap(): any {
return {
'r|left': () => this.reply(true),
'r': () => this.reply(true),
'e|a|plus': () => this.react(true),
'q|right': () => this.renote(true),
'q': () => this.renote(true),
'f|b': this.favorite,
'delete|ctrl+d': this.del,
'ctrl+q|ctrl+right': this.renoteDirectly,
'ctrl+q': this.renoteDirectly,
'up|k|shift+tab': this.focusBefore,
'down|j|tab': this.focusAfter,
'shift+up': () => this.$emit('parentFocus', 'up'),
'shift+down': () => this.$emit('parentFocus', 'down'),
'shift+left': () => this.$emit('parentFocus', 'left'),
'shift+right': () => this.$emit('parentFocus', 'right'),
'esc': this.blur,
'm|o': () => this.menu(true),
's': this.toggleShowContent,

View File

@ -114,10 +114,9 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'mention': {
return (createElement as any)('a', {
return (createElement as any)('router-link', {
attrs: {
href: `${url}/${token.canonical}`,
target: '_blank',
to: `/${token.canonical}`,
dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
style: 'color:var(--mfmMention);'
},
@ -129,10 +128,9 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'hashtag': {
return [createElement('a', {
return [createElement('router-link', {
attrs: {
href: `${url}/tags/${encodeURIComponent(token.hashtag)}`,
target: '_blank',
to: `/tags/${encodeURIComponent(token.hashtag)}`,
style: 'color:var(--mfmHashtag);'
}
}, token.content)];

View File

@ -21,6 +21,7 @@ import updateAvatar from './api/update-avatar';
import updateBanner from './api/update-banner';
import MkIndex from './views/pages/index.vue';
import MkHome from './views/pages/home.vue';
import MkDeck from './views/pages/deck/deck.vue';
import MkAdmin from './views/pages/admin/admin.vue';
import MkStats from './views/pages/stats/stats.vue';
@ -54,6 +55,7 @@ init(async (launch) => {
mode: 'history',
routes: [
{ path: '/', name: 'index', component: MkIndex },
{ path: '/home', name: 'home', component: MkHome },
{ path: '/deck', name: 'deck', component: MkDeck },
{ path: '/admin', name: 'admin', component: MkAdmin },
{ path: '/stats', name: 'stats', component: MkStats },
@ -64,7 +66,7 @@ init(async (launch) => {
{ path: '/i/drive/folder/:folder', component: MkDrive },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag },
{ path: '/tags/:tag', name: 'tag', component: MkTag },
{ path: '/share', component: MkShare },
{ path: '/reversi/:game?', component: MkReversi },
{ path: '/@:user', name: 'user', component: MkUser },

View File

@ -35,7 +35,7 @@
<span v-if="appearNote.isHidden" style="opacity: 0.5">%i18n:@private%</span>
<a class="reply" v-if="appearNote.reply">%fa:reply%</a>
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text"/>
<a class="rp" v-if="appearNote.renote">RP:</a>
<a class="rp" v-if="appearNote.renote">RN:</a>
</div>
<div class="files" v-if="appearNote.files.length > 0">
<mk-media-list :media-list="appearNote.files"/>

View File

@ -4,9 +4,9 @@
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
<div v-if="!fetching && requestInitPromise != null">
<p>%i18n:@error%</p>
<button @click="resolveInitPromise">%i18n:@retry%</button>
<div v-if="!fetching && requestInitPromise != null" class="error">
<p>%fa:exclamation-triangle% %i18n:common.error.title%</p>
<ui-button @click="resolveInitPromise">%i18n:common.error.retry%</ui-button>
</div>
<div class="placeholder" v-if="fetching">
@ -38,7 +38,6 @@
<script lang="ts">
import Vue from 'vue';
import * as config from '../../../config';
import getNoteSummary from '../../../../../misc/get-note-summary';
import XNote from './note.vue';
@ -61,7 +60,6 @@ export default Vue.extend({
requestInitPromise: null as () => Promise<any[]>,
notes: [],
queue: [],
unreadCount: 0,
fetching: true,
moreFetching: false
};
@ -80,12 +78,10 @@ export default Vue.extend({
},
mounted() {
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
window.addEventListener('scroll', this.onScroll, { passive: true });
},
beforeDestroy() {
document.removeEventListener('visibilitychange', this.onVisibilitychange);
window.removeEventListener('scroll', this.onScroll);
},
@ -147,10 +143,9 @@ export default Vue.extend({
}
//#endregion
// 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
if ((document.hidden || !this.isScrollTop()) && note.userId !== this.$store.state.i.id) {
this.unreadCount++;
document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
if (document.hidden || !this.isScrollTop()) {
this.$store.commit('pushBehindNote', note);
}
if (this.isScrollTop()) {
@ -195,21 +190,9 @@ export default Vue.extend({
this.moreFetching = false;
},
clearNotification() {
this.unreadCount = 0;
document.title = (this as any).os.instanceName;
},
onVisibilitychange() {
if (!document.hidden) {
this.clearNotification();
}
},
onScroll() {
if (this.isScrollTop()) {
this.releaseQueue();
this.clearNotification();
}
if (this.$store.state.settings.fetchOnScroll !== false) {
@ -232,6 +215,13 @@ export default Vue.extend({
> *
transition transform .3s ease, opacity .3s ease
> .error
max-width 300px
margin 0 auto
padding 16px
text-align center
color var(--text)
> .placeholder
padding 32px
opacity 0.3

View File

@ -97,6 +97,9 @@
<ui-radio v-model="navbar" value="left">%i18n:@navbar-position-left%</ui-radio>
<ui-radio v-model="navbar" value="right">%i18n:@navbar-position-right%</ui-radio>
</section>
<section>
<ui-switch v-model="deckDefault">%i18n:@deck-default%</ui-switch>
</section>
<section>
<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
<ui-switch v-model="useShadow">%i18n:@use-shadow%</ui-switch>
@ -366,6 +369,11 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
},
deckDefault: {
get() { return this.$store.state.device.deckDefault; },
set(value) { this.$store.commit('device/set', { key: 'deckDefault', value }); }
},
enableSounds: {
get() { return this.$store.state.device.enableSounds; },
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }

View File

@ -5,7 +5,7 @@
<span v-if="note.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
<a class="reply" v-if="note.replyId">%fa:reply%</a>
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RP: ...</a>
<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RN: ...</a>
</div>
<details v-if="note.files.length > 0">
<summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>

View File

@ -2,18 +2,22 @@
<div class="nav">
<ul>
<template v-if="$store.getters.isSignedIn">
<li class="home" :class="{ active: $route.name == 'index' }" @click="goToTop">
<router-link to="/">
%fa:home%
<p>%i18n:@home%</p>
</router-link>
</li>
<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
<router-link to="/deck">
%fa:columns%
<p>%i18n:@deck%</p>
</router-link>
</li>
<template v-if="$store.state.device.deckDefault">
<li class="deck" :class="{ active: $route.name == 'deck' || $route.name == 'index' }" @click="goToTop">
<router-link to="/">%fa:columns%<p>%i18n:@deck%</p></router-link>
</li>
<li class="home" :class="{ active: $route.name == 'home' }" @click="goToTop">
<router-link to="/home">%fa:home%<p>%i18n:@home%</p></router-link>
</li>
</template>
<template v-else>
<li class="home" :class="{ active: $route.name == 'home' || $route.name == 'index' }" @click="goToTop">
<router-link to="/">%fa:home%<p>%i18n:@home%</p></router-link>
</li>
<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
<router-link to="/deck">%fa:columns%<p>%i18n:@deck%</p></router-link>
</li>
</template>
<li class="messaging">
<a @click="messaging">
%fa:comments%

View File

@ -6,12 +6,22 @@
</div>
<div class="nav" v-if="$store.getters.isSignedIn">
<div class="home" :class="{ active: $route.name == 'index' }" @click="goToTop">
<router-link to="/">%fa:home%</router-link>
</div>
<div class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
<router-link to="/deck">%fa:columns%</router-link>
</div>
<template v-if="$store.state.device.deckDefault">
<div class="deck" :class="{ active: $route.name == 'deck' || $route.name == 'index' }" @click="goToTop">
<router-link to="/">%fa:columns%</router-link>
</div>
<div class="home" :class="{ active: $route.name == 'home' }" @click="goToTop">
<router-link to="/home">%fa:home%</router-link>
</div>
</template>
<template v-else>
<div class="home" :class="{ active: $route.name == 'home' || $route.name == 'index' }" @click="goToTop">
<router-link to="/">%fa:home%</router-link>
</div>
<div class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
<router-link to="/deck">%fa:columns%</router-link>
</div>
</template>
<div class="messaging">
<a @click="messaging">%fa:comments%<template v-if="hasUnreadMessagingMessage">%fa:circle%</template></a>
</div>

View File

@ -1,14 +1,14 @@
<template>
<x-widgets-column v-if="column.type == 'widgets'" :column="column" :is-stacked="isStacked"/>
<x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'local'" :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 == '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"/>
<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked" @parentFocus="parentFocus"/>
<x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked" @parentFocus="parentFocus"/>
<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked" @parentFocus="parentFocus"/>
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked" @parentFocus="parentFocus"/>
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked" @parentFocus="parentFocus"/>
<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked" @parentFocus="parentFocus"/>
<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked" @parentFocus="parentFocus"/>
<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked" @parentFocus="parentFocus"/>
</template>
<script lang="ts">
@ -38,6 +38,16 @@ export default Vue.extend({
required: false,
default: false
}
},
methods: {
focus() {
this.$children[0].focus();
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
}
});
</script>

View File

@ -2,7 +2,7 @@
<x-column :name="name" :column="column" :is-stacked="isStacked">
<span slot="header">%fa:envelope R%{{ name }}</span>
<x-direct/>
<x-direct @parentFocus="parentFocus"/>
</x-column>
</template>
@ -34,5 +34,15 @@ export default Vue.extend({
return '%i18n:common.deck.direct%';
}
},
methods: {
focus() {
this.$refs.tl.focus();
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
}
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null"/>
<x-notes ref="timeline" :more="existMore ? more : null" @parentFocus="parentFocus"/>
</template>
<script lang="ts">
@ -58,6 +58,7 @@ export default Vue.extend({
}, rej);
}));
},
more() {
this.moreFetching = true;
@ -82,12 +83,21 @@ export default Vue.extend({
return promise;
},
onNote(note) {
// Prepend a note
if (note.visibility == 'specified') {
(this.$refs.timeline as any).prepend(note);
}
}
},
focus() {
this.$refs.timeline.focus();
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
}
});
</script>

View File

@ -0,0 +1,37 @@
<template>
<x-column>
<span slot="header">
%fa:hashtag%<span>{{ tag }}</span>
</span>
<x-hashtag-tl :tag-tl="tagTl"/>
</x-column>
</template>
<script lang="ts">
import Vue from 'vue';
import XColumn from './deck.column.vue';
import XHashtagTl from './deck.hashtag-tl.vue';
export default Vue.extend({
components: {
XColumn,
XHashtagTl
},
props: {
tag: {
type: String,
required: true
}
},
computed: {
tagTl(): any {
return {
query: [[this.tag]]
};
}
}
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView" @parentFocus="parentFocus"/>
</template>
<script lang="ts">
@ -47,14 +47,16 @@ export default Vue.extend({
mounted() {
if (this.connection) this.connection.close();
this.connection = (this as any).os.stream.connectToChannel('hashtag', this.tagTl.query);
this.connection = (this as any).os.stream.connectToChannel('hashtag', {
q: this.tagTl.query
});
this.connection.on('note', this.onNote);
this.fetch();
},
beforeDestroy() {
this.connection.close();
this.connection.dispose();
},
methods: {
@ -80,6 +82,7 @@ export default Vue.extend({
}, rej);
}));
},
more() {
this.moreFetching = true;
@ -105,12 +108,21 @@ export default Vue.extend({
return promise;
},
onNote(note) {
if (this.mediaOnly && note.files.length == 0) return;
// Prepend a note
(this.$refs.timeline as any).prepend(note);
}
},
focus() {
this.$refs.timeline.focus();
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
}
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView" @parentFocus="parentFocus"/>
</template>
<script lang="ts">
@ -84,6 +84,7 @@ export default Vue.extend({
}, rej);
}));
},
more() {
this.moreFetching = true;
@ -109,18 +110,29 @@ export default Vue.extend({
return promise;
},
onNote(note) {
if (this.mediaOnly && note.files.length == 0) return;
// Prepend a note
(this.$refs.timeline as any).prepend(note);
},
onUserAdded() {
this.fetch();
},
onUserRemoved() {
this.fetch();
}
},
focus() {
this.$refs.timeline.focus();
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
}
});
</script>

View File

@ -2,7 +2,7 @@
<x-column :name="name" :column="column" :is-stacked="isStacked">
<span slot="header">%fa:at%{{ name }}</span>
<x-mentions/>
<x-mentions ref="tl" @parentFocus="parentFocus"/>
</x-column>
</template>
@ -34,5 +34,15 @@ export default Vue.extend({
return '%i18n:common.deck.mentions%';
}
},
methods: {
focus() {
this.$refs.tl.focus();
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
}
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null"/>
<x-notes ref="timeline" :more="existMore ? more : null" @parentFocus="parentFocus"/>
</template>
<script lang="ts">
@ -57,6 +57,7 @@ export default Vue.extend({
}, rej);
}));
},
more() {
this.moreFetching = true;
@ -80,10 +81,19 @@ export default Vue.extend({
return promise;
},
onNote(note) {
// Prepend a note
(this.$refs.timeline as any).prepend(note);
}
},
focus() {
this.$refs.timeline.focus();
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
}
});
</script>

View File

@ -8,16 +8,22 @@
</template>
</div>
<div v-if="!fetching && requestInitPromise != null">
<p>%i18n:@error%</p>
<button @click="resolveInitPromise">%i18n:@retry%</button>
<div v-if="!fetching && requestInitPromise != null" class="error">
<p>%fa:exclamation-triangle% %i18n:common.error.title%</p>
<ui-button @click="resolveInitPromise">%i18n:common.error.retry%</ui-button>
</div>
<!-- トランジションを有効にするとなぜかメモリリークする -->
<!--<transition-group name="mk-notes" class="transition">-->
<div class="notes">
<!--<transition-group name="mk-notes" class="transition" ref="notes">-->
<div class="notes" ref="notes">
<template v-for="(note, i) in _notes">
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :media-view="mediaView" :mini="true"/>
<x-note
:note="note"
:key="note.id"
@update:note="onNoteUpdated(i, $event)"
:media-view="mediaView"
:mini="true"
@parentFocus="parentFocus"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
<span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
@ -102,7 +108,11 @@ export default Vue.extend({
methods: {
focus() {
(this.$el as any).children[0].focus();
(this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
onNoteUpdated(i, note) {
@ -154,6 +164,11 @@ export default Vue.extend({
}
//#endregion
// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
if (document.hidden || !this.isScrollTop()) {
this.$store.commit('pushBehindNote', note);
}
if (this.isScrollTop()) {
// Prepend the note
this.notes.unshift(note);
@ -211,6 +226,13 @@ export default Vue.extend({
> *
transition transform .3s ease, opacity .3s ease
> .error
max-width 300px
margin 0 auto
padding 16px
text-align center
color var(--text)
> .placeholder
padding 16px
opacity 0.3

View File

@ -14,9 +14,28 @@
<ui-switch v-model="column.isMediaOnly" @change="onChangeSettings">%i18n:@is-media-only%</ui-switch>
<ui-switch v-model="column.isMediaView" @change="onChangeSettings">%i18n:@is-media-view%</ui-switch>
</div>
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
<x-hashtag-tl v-else-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-list-tl v-if="column.type == 'list'"
:list="column.list"
:media-only="column.isMediaOnly"
:media-view="column.isMediaView"
ref="tl"
@parentFocus="parentFocus"
/>
<x-hashtag-tl v-else-if="column.type == 'hashtag'"
:tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)"
:media-only="column.isMediaOnly"
:media-view="column.isMediaView"
ref="tl"
@parentFocus="parentFocus"
/>
<x-tl v-else
:src="column.type"
:media-only="column.isMediaOnly"
:media-view="column.isMediaView"
ref="tl"
@parentFocus="parentFocus"
/>
</x-column>
</template>
@ -77,7 +96,15 @@ export default Vue.extend({
methods: {
onChangeSettings(v) {
this.$store.dispatch('settings/saveDeck');
}
},
focus() {
this.$refs.tl.focus();
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
}
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView" @parentFocus="parentFocus"/>
</template>
<script lang="ts">
@ -143,7 +143,11 @@ export default Vue.extend({
focus() {
(this.$refs.timeline as any).focus();
}
},
parentFocus(direction) {
this.$emit('parentFocus', direction);
},
}
});
</script>

View File

@ -8,6 +8,7 @@
<div class="is-remote" v-if="user.host != null">%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></div>
<header :style="bannerStyle">
<div>
<button class="menu" @click="menu" ref="menu">%fa:ellipsis-h%</button>
<mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow"/>
<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
<span class="name">{{ user | userName }}</span>
@ -26,7 +27,12 @@
</div>
</div>
<div class="images" v-if="images.length > 0">
<router-link v-for="image in images" :style="`background-image: url(${image.thumbnailUrl})`" :key="`${image.id}:${image._note.id}`" :to="image._note | notePage"></router-link>
<router-link v-for="image in images"
:style="`background-image: url(${image.thumbnailUrl})`"
:key="`${image.id}:${image._note.id}`"
:to="image._note | notePage"
:title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`"
></router-link>
</div>
<div class="tl">
<x-notes ref="timeline" :more="existMore ? fetchMoreNotes : null"/>
@ -41,6 +47,9 @@ import parseAcct from '../../../../../../misc/acct/parse';
import XColumn from './deck.column.vue';
import XNotes from './deck.notes.vue';
import XNote from '../../components/note.vue';
import Menu from '../../../../common/views/components/menu.vue';
import MkUserListsWindow from '../../components/user-lists-window.vue';
import Ok from '../../../../common/views/components/ok.vue';
import { concat } from '../../../../../../prelude/array';
const fetchLimit = 10;
@ -161,6 +170,30 @@ export default Vue.extend({
return promise;
},
menu() {
let menu = [{
icon: '%fa:list%',
text: '%i18n:@push-to-a-list%',
action: () => {
const w = (this as any).os.new(MkUserListsWindow);
w.$once('choosen', async list => {
w.close();
await (this as any).api('users/lists/push', {
listId: list.id,
userId: this.user.id
});
(this as any).os.new(Ok);
});
}
}];
this.os.new(Menu, {
source: this.$refs.menu,
compact: false,
items: menu
});
}
}
});
</script>
@ -191,6 +224,14 @@ export default Vue.extend({
color #fff
text-align center
> .menu
position absolute
top 8px
left 8px
padding 8px
font-size 16px
text-shadow 0 0 8px #000
> .follow
position absolute
top 16px

View File

@ -1,17 +1,18 @@
<template>
<mk-ui :class="$style.root">
<div class="qlvquzbjribqcaozciifydkngcwtyzje" :style="style" :class="{ center: $store.state.device.deckColumnAlign == 'center' }">
<div class="qlvquzbjribqcaozciifydkngcwtyzje" ref="body" :style="style" :class="{ center: $store.state.device.deckColumnAlign == 'center' }" v-hotkey.global="keymap">
<template v-for="ids in layout">
<div v-if="ids.length > 1" class="folder">
<template v-for="id, i in ids">
<x-column-core :ref="id" :key="id" :column="columns.find(c => c.id == id)" :is-stacked="true"/>
<x-column-core :ref="id" :key="id" :column="columns.find(c => c.id == id)" :is-stacked="true" @parentFocus="moveFocus(id, $event)"/>
</template>
</div>
<x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])"/>
<x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])" @parentFocus="moveFocus(ids[0], $event)"/>
</template>
<template v-if="temporaryColumn">
<x-user-column v-if="temporaryColumn.type == 'user'" :acct="temporaryColumn.acct" :key="temporaryColumn.acct"/>
<x-note-column v-else-if="temporaryColumn.type == 'note'" :note-id="temporaryColumn.noteId" :key="temporaryColumn.noteId"/>
<x-hashtag-column v-else-if="temporaryColumn.type == 'tag'" :tag="temporaryColumn.tag" :key="temporaryColumn.tag"/>
</template>
<button ref="add" @click="add" title="%i18n:common.deck.add-column%">%fa:plus%</button>
</div>
@ -25,6 +26,7 @@ import Menu from '../../../../common/views/components/menu.vue';
import MkUserListsWindow from '../../components/user-lists-window.vue';
import XUserColumn from './deck.user-column.vue';
import XNoteColumn from './deck.note-column.vue';
import XHashtagColumn from './deck.hashtag-column.vue';
import * as uuid from 'uuid';
@ -32,7 +34,8 @@ export default Vue.extend({
components: {
XColumnCore,
XUserColumn,
XNoteColumn
XNoteColumn,
XHashtagColumn
},
computed: {
@ -55,6 +58,25 @@ export default Vue.extend({
temporaryColumn(): any {
return this.$store.state.device.deckTemporaryColumn;
},
keymap(): any {
return {
't': this.focus
};
}
},
watch: {
temporaryColumn() {
if (this.temporaryColumn != null) {
this.$nextTick(() => {
this.$refs.body.scrollTo({
left: this.$refs.body.scrollWidth - this.$refs.body.clientWidth,
behavior: 'smooth'
});
});
}
}
},
@ -143,6 +165,15 @@ export default Vue.extend({
}
});
return true;
} else if (to.name == 'tag') {
this.$store.commit('device/set', {
key: 'deckTemporaryColumn',
value: {
type: 'tag',
tag: to.params.tag
}
});
return true;
}
},
@ -253,6 +284,71 @@ export default Vue.extend({
}
}]
});
},
focus() {
// Flatten array of arrays
const ids = [].concat.apply([], this.layout);
const firstTl = ids.find(id => this.isTlColumn(id));
if (firstTl) {
this.$refs[firstTl][0].focus();
}
},
moveFocus(id, direction) {
let targetColumn;
if (direction == 'right') {
const currentColumnIndex = this.layout.findIndex(ids => ids.includes(id));
this.layout.some((ids, i) => {
if (i <= currentColumnIndex) return false;
const tl = ids.find(id => this.isTlColumn(id));
if (tl) {
targetColumn = tl;
return true;
}
});
} else if (direction == 'left') {
const currentColumnIndex = [...this.layout].reverse().findIndex(ids => ids.includes(id));
[...this.layout].reverse().some((ids, i) => {
if (i <= currentColumnIndex) return false;
const tl = ids.find(id => this.isTlColumn(id));
if (tl) {
targetColumn = tl;
return true;
}
});
} else if (direction == 'down') {
const currentColumn = this.layout.find(ids => ids.includes(id));
const currentIndex = currentColumn.indexOf(id);
currentColumn.some((_id, i) => {
if (i <= currentIndex) return false;
if (this.isTlColumn(_id)) {
targetColumn = _id;
return true;
}
});
} else if (direction == 'up') {
const currentColumn = [...this.layout.find(ids => ids.includes(id))].reverse();
const currentIndex = currentColumn.indexOf(id);
currentColumn.some((_id, i) => {
if (i <= currentIndex) return false;
if (this.isTlColumn(_id)) {
targetColumn = _id;
return true;
}
});
}
if (targetColumn) {
this.$refs[targetColumn][0].focus();
}
},
isTlColumn(id) {
const column = this.columns.find(c => c.id === id);
return ['home', 'local', 'hybrid', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type);
}
}
});

View File

@ -1,16 +1,25 @@
<template>
<component :is="$store.getters.isSignedIn ? 'home' : 'welcome'"></component>
<component :is="page"></component>
</template>
<script lang="ts">
import Vue from 'vue';
import Home from './home.vue';
import Welcome from './welcome.vue';
import Deck from './deck/deck.vue';
export default Vue.extend({
components: {
Home,
Deck,
Welcome
},
computed: {
page(): string {
if (!this.$store.getters.isSignedIn) return 'welcome';
return this.$store.state.device.deckDefault ? 'deck' : 'home';
}
}
});
</script>

View File

@ -9,11 +9,11 @@
</p>
</div>
<div class="action-form">
<button class="mute ui" @click="user.isMuted ? unmute() : mute()" v-if="$store.state.i.id != user.id">
<ui-button @click="user.isMuted ? unmute() : mute()" v-if="$store.state.i.id != user.id">
<span v-if="user.isMuted">%fa:eye% %i18n:@unmute%</span>
<span v-if="!user.isMuted">%fa:eye-slash% %i18n:@mute%</span>
</button>
<button class="mute ui" @click="list">%fa:list% %i18n:@push-to-a-list%</button>
</ui-button>
<ui-button @click="list">%fa:list% %i18n:@push-to-a-list%</ui-button>
</div>
</div>
</template>

View File

@ -161,6 +161,12 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
}
});
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
os.store.commit('clearBehindNotes');
}
}, false);
Vue.mixin({
data() {
return {
@ -210,15 +216,15 @@ function panic(e) {
document.documentElement.style.background = '#1269e2';
document.body.innerHTML =
'<div id="error">'
+ '<h1>:( 致命的な問題が発生しました。</h1>'
+ '<p>お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。</p>'
+ '<h1>%i18n.common.BSoD.fatal-error%</h1>'
+ '<p>%i18n.common.BSoD.update-browser-os%</p>'
+ '<hr>'
+ `<p>エラーコード: ${e.toString()}</p>`
+ `<p>ブラウザ バージョン: ${navigator.userAgent}</p>`
+ `<p>クライアント バージョン: ${version}</p>`
+ `<p>%i18n.common.BSoD.error-code%: ${e.toString()}</p>`
+ `<p>%i18n.common.BSoD.browser-version%: ${navigator.userAgent}</p>`
+ `<p>%i18n.common.BSoD.client-version%: ${version}</p>`
+ '<hr>'
+ '<p>問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。</p>'
+ '<p>Thank you for using Misskey.</p>'
+ '<p>%i18n.common.BSoD.email-support%</p>'
+ '<p>%i18n.common.BSoD.thanks%</p>'
+ '</div>';
// TODO: Report the bug

View File

@ -244,10 +244,10 @@ export default class MiOS extends EventEmitter {
this.store.dispatch('login', me);
fetched();
} else {
this.initStream();
// Finish init
callback();
this.initStream();
}
});
}

View File

@ -31,7 +31,7 @@
<span v-if="appearNote.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
<a class="reply" v-if="appearNote.reply">%fa:reply%</a>
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text"/>
<a class="rp" v-if="appearNote.renote != null">RP:</a>
<a class="rp" v-if="appearNote.renote != null">RN:</a>
</div>
<div class="files" v-if="appearNote.files.length > 0">
<mk-media-list :media-list="appearNote.files"/>

View File

@ -37,7 +37,6 @@
<script lang="ts">
import Vue from 'vue';
import getNoteSummary from '../../../../../misc/get-note-summary';
const displayLimit = 30;
@ -54,7 +53,6 @@ export default Vue.extend({
requestInitPromise: null as () => Promise<any[]>,
notes: [],
queue: [],
unreadCount: 0,
fetching: true,
moreFetching: false
};
@ -83,12 +81,10 @@ export default Vue.extend({
},
mounted() {
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
window.addEventListener('scroll', this.onScroll, { passive: true });
},
beforeDestroy() {
document.removeEventListener('visibilitychange', this.onVisibilitychange);
window.removeEventListener('scroll', this.onScroll);
},
@ -146,10 +142,9 @@ export default Vue.extend({
}
//#endregion
// 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
if ((document.hidden || !this.isScrollTop()) && note.userId !== this.$store.state.i.id) {
this.unreadCount++;
document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
if (document.hidden || !this.isScrollTop()) {
this.$store.commit('pushBehindNote', note);
}
if (this.isScrollTop()) {
@ -187,21 +182,9 @@ export default Vue.extend({
this.moreFetching = false;
},
clearNotification() {
this.unreadCount = 0;
document.title = (this as any).os.instanceName;
},
onVisibilitychange() {
if (!document.hidden) {
this.clearNotification();
}
},
onScroll() {
if (this.isScrollTop()) {
this.releaseQueue();
this.clearNotification();
}
if (this.$store.state.settings.fetchOnScroll !== false) {

View File

@ -5,7 +5,7 @@
<span v-if="note.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
<a class="reply" v-if="note.replyId">%fa:reply%</a>
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
<a class="rp" v-if="note.renoteId">RP: ...</a>
<a class="rp" v-if="note.renoteId">RN: ...</a>
</div>
<details v-if="note.files.length > 0">
<summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>

View File

@ -5,6 +5,7 @@ import * as nestedProperty from 'nested-property';
import MiOS from './mios';
import { hostname } from './config';
import { erase } from '../../prelude/array';
import getNoteSummary from '../../misc/get-note-summary';
const defaultSettings = {
home: null,
@ -60,7 +61,8 @@ const defaultDeviceSettings = {
navbar: 'top',
deckColumnAlign: 'center',
mobileNotificationPosition: 'bottom',
deckTemporaryColumn: null
deckTemporaryColumn: null,
deckDefault: false
};
export default (os: MiOS) => new Vuex.Store({
@ -72,7 +74,8 @@ export default (os: MiOS) => new Vuex.Store({
i: null,
indicate: false,
uiHeaderHeight: 0,
navHook: null
navHook: null,
behindNotes: []
},
getters: {
@ -98,6 +101,18 @@ export default (os: MiOS) => new Vuex.Store({
navHook(state, callback) {
state.navHook = callback;
},
pushBehindNote(state, note) {
if (note.userId === state.i.id) return;
if (state.behindNotes.some(n => n.id === note.id)) return;
state.behindNotes.push(note);
document.title = `(${state.behindNotes.length}) ${getNoteSummary(note)}`;
},
clearBehindNotes(state) {
state.behindNotes = [];
document.title = os.instanceName;
}
},

View File

@ -25,9 +25,9 @@
<tbody>
<tr><td><kbd class="key"></kbd>, <kbd class="key">K</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>上の投稿にフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">J</kbd>, <kbd class="key">Tab</kbd></td><td>下の投稿にフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">R</kbd></td><td>返信フォームを開く</td><td><b>R</b>eply</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">Q</kbd></td><td>Renoteフォームを開く</td><td><b>Q</b>uote</td></tr>
<tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key"></kbd></kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>即刻Renoteする(フォームを開かずに)</td><td>-</td></tr>
<tr><td><kbd class="key">R</kbd></td><td>返信フォームを開く</td><td><b>R</b>eply</td></tr>
<tr><td><kbd class="key">Q</kbd></td><td>Renoteフォームを開く</td><td><b>Q</b>uote</td></tr>
<tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>即刻Renoteする(フォームを開かずに)</td><td>-</td></tr>
<tr><td><kbd class="key">E</kbd>, <kbd class="key">A</kbd>, <kbd class="key">+</kbd></td><td>リアクションフォームを開く</td><td><b>E</b>mote, re<b>A</b>ction</td></tr>
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションをする(対応については後述)</td><td>-</td></tr>
<tr><td><kbd class="key">F</kbd>, <kbd class="key">B</kbd></td><td>お気に入りに登録</td><td><b>F</b>avorite, <b>B</b>ookmark</td></tr>
@ -86,6 +86,19 @@
</tbody>
</table>
## デッキ
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<tr><td>投稿にフォーカスした状態で<kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key"></kbd></kbd></td><td>上のカラムにフォーカス</td><td>-</td></tr>
<tr><td>投稿にフォーカスした状態で<kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key"></kbd></kbd></td><td>下のカラムにフォーカス</td><td>-</td></tr>
<tr><td>投稿にフォーカスした状態で<kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key"></kbd></kbd></td><td>右のカラムにフォーカス</td><td>-</td></tr>
<tr><td>投稿にフォーカスした状態で<kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key"></kbd></kbd></td><td>左のカラムにフォーカス</td><td>-</td></tr>
</tbody>
</table>
# 例
<table>
<thead>

View File

@ -9,9 +9,9 @@ export type TextElementHashtag = {
};
export default function(text: string, i: number) {
if (!(/^\s#[^\s\.,]+/.test(text) || (i == 0 && /^#[^\s\.,]+/.test(text)))) return null;
if (!(/^\s#[^\s\.,!\?]+/.test(text) || (i == 0 && /^#[^\s\.,!\?]+/.test(text)))) return null;
const isHead = text.startsWith('#');
const hashtag = text.match(/^\s?#[^\s\.,]+/)[0];
const hashtag = text.match(/^\s?#[^\s\.,!\?]+/)[0];
const res: any[] = !isHead ? [{
type: 'text',
content: text[0]

View File

@ -38,9 +38,9 @@ const summarize = (note: any): string => {
// Renoteのとき
if (note.renoteId) {
if (note.renote) {
summary += ` RP: ${summarize(note.renote)}`;
summary += ` RN: ${summarize(note.renote)}`;
} else {
summary += ' RP: ...';
summary += ' RN: ...';
}
}

View File

@ -31,19 +31,23 @@ export const meta = {
}
}),
isSensitive: $.bool.optional.nullable.note({
default: null,
isSensitive: $.bool.optional.note({
default: false,
desc: {
'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか',
'en-US': 'Whether this media is NSFW'
}
}),
force: $.bool.optional.note({
default: false,
desc: {
'ja-JP': 'true にすると、同じハッシュを持つファイルが既にアップロードされていても強制的にファイルを作成します。',
}
})
}
};
/**
* Create a file
*/
export default async (file: any, params: any, user: ILocalUser): Promise<any> => {
if (file == null) {
throw 'file is required';
@ -76,7 +80,7 @@ export default async (file: any, params: any, user: ILocalUser): Promise<any> =>
try {
// Create file
const driveFile = await create(user, file.path, name, null, ps.folderId, false, false, null, null, ps.isSensitive);
const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive);
cleanup();

View File

@ -4,7 +4,8 @@ import NoteUnread from '../../../../models/note-unread';
export const meta = {
desc: {
'ja-JP': '未読の投稿をすべて既読にします。'
'ja-JP': '未読の投稿をすべて既読にします。',
'en-US': 'Mark all messages as read.'
},
requireCredential: true,

View File

@ -15,6 +15,8 @@ export default class extends Channel {
const q: Array<string[]> = params.q;
if (q == null) return;
// Subscribe stream
this.subscriber.on('hashtag', async note => {
const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase())));

View File

@ -1,6 +1,5 @@
import autobind from 'autobind-decorator';
import * as websocket from 'websocket';
import * as debug from 'debug';
import User, { IUser } from '../../../models/user';
import readNotification from '../common/read-notification';
@ -12,8 +11,6 @@ import Channel from './channel';
import channels from './channels';
import { EventEmitter } from 'events';
const log = debug('misskey');
/**
* Main stream connection
*/
@ -147,7 +144,6 @@ export default class Connection {
@autobind
private onChannelConnectRequested(payload: any) {
const { channel, id, params, pong } = payload;
log(`CH CONNECT: ${id} ${channel} by @${this.user.username}`);
this.connectChannel(id, params, channel, pong);
}
@ -157,7 +153,6 @@ export default class Connection {
@autobind
private onChannelDisconnectRequested(payload: any) {
const { id } = payload;
log(`CH DISCONNECT: ${id} by @${this.user.username}`);
this.disconnectChannel(id);
}

View File

@ -122,6 +122,12 @@ describe('Text', () => {
{ type: 'hashtag', content: '#piyo', hashtag: 'piyo' },
{ type: 'text', content: '.' }
], tokens2);
const tokens3 = analyze('#Foo!');
assert.deepEqual([
{ type: 'hashtag', content: '#Foo', hashtag: 'Foo' },
{ type: 'text', content: '!' },
], tokens3);
});
it('quote', () => {