Merge branch 'develop' into math-block

This commit is contained in:
Aya Morisawa 2019-01-27 16:41:30 +09:00 committed by GitHub
commit 1af1638e2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 791 additions and 852 deletions

View File

@ -5,12 +5,20 @@ unreleased
---------- ----------
* 返信するときにCWを維持するかどうか設定できるように * 返信するときにCWを維持するかどうか設定できるように
* 外部サービス認証情報の配信 * 外部サービス認証情報の配信
* 管理画面のモデレーションのUIを強化
* 管理画面からリモートユーザーの情報を更新できるように
* 回転構文の追加
* 左右反転構文の追加
* シンタックスハイライトの強化
* 引用投稿を削除したとき単なるRenoteとしてタイムラインに残る問題を修正
* イタリック構文の判定の改善 * イタリック構文の判定の改善
* タイトル構文の判定の改善
* テーマが反映されないことがある問題を修正 * テーマが反映されないことがある問題を修正
* ホームにフォロワー限定投稿が表示されない問題を修正 * ホームにフォロワー限定投稿が表示されない問題を修正
* 返信一覧を取得すると非公開投稿も取得されてしまう問題を修正 * 返信一覧を取得すると非公開投稿も取得されてしまう問題を修正
* メンション一覧を取得すると非公開投稿も取得されてしまう問題を修正 * メンション一覧を取得すると非公開投稿も取得されてしまう問題を修正
* 通知に非公開投稿が表示される問題を修正 * 通知に非公開投稿が表示される問題を修正
* ダイレクトで投稿すると100の確率で表示が二重になる問題を修正
* ウィジットの投稿フォームで投稿するとデフォルトの公開範囲が適用されない問題を修正 * ウィジットの投稿フォームで投稿するとデフォルトの公開範囲が適用されない問題を修正
10.78.5 10.78.5

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -807,8 +807,8 @@ desktop/views/components/settings.vue:
timeline: "Timeline" timeline: "Timeline"
show-my-renotes: "Show my renotes in the timeline" show-my-renotes: "Show my renotes in the timeline"
show-renoted-my-notes: "Show renoted posts of mine in timelines" show-renoted-my-notes: "Show renoted posts of mine in timelines"
show-local-renotes: "Show renoted local posts in timelines" show-local-renotes: "Show renoted local posts in the timelines"
show-maps: "Display a map to show the location" show-maps: "Display a map to show location"
remain-deleted-note: "Continue to show deleted posts" remain-deleted-note: "Continue to show deleted posts"
deck-column-align: "Deck column alignment" deck-column-align: "Deck column alignment"
deck-column-align-center: "Center" deck-column-align-center: "Center"
@ -1135,15 +1135,22 @@ admin/views/users.vue:
user-not-found: "User not found" user-not-found: "User not found"
lookup: "Look up" lookup: "Look up"
reset-password: "Reset password" reset-password: "Reset password"
reset-password-confirm: "Do you want to reset your password?"
password-updated: "The password is now \"{password}\"" password-updated: "The password is now \"{password}\""
suspend: "Suspend" suspend: "Suspend"
suspend-confirm: "Do you want to suspend this account?"
suspended: "Successfully suspended." suspended: "Successfully suspended."
unsuspend: "Unsuspend" unsuspend: "Unsuspend"
unsuspend-confirm: "Are you sure you want to unsuspend this account?"
unsuspended: "The user has successfully unsuspended." unsuspended: "The user has successfully unsuspended."
verify: "Verify account" verify: "Verify account"
verify-confirm: "Do you want this to be a verified account?"
verified: "The account is now being verified" verified: "The account is now being verified"
unverify: "Unverify account" unverify: "Unverify account"
unverify-confirm: "Do you want to remove the 'verified account' designation?"
unverified: "The account is now being unverified" unverified: "The account is now being unverified"
update-remote-user: "Update information about remote user"
remote-user-updated: "The information regarding the remote user has been updated."
users: users:
title: "Users" title: "Users"
sort: sort:

View File

@ -11,7 +11,7 @@ common:
about: "Misskey es un <b>Servicio de red social descentralizada de microblogging</b> de código abierto. Contiene una interfaz de usuario altamente personalizable, reacciones a posts, almacenamiento para poder manejar archivos y otras funciones avanzadas. Además de conectarse con la red llamada Fediverso, puede intercambiar mensajes con otras redes sociales. Por ejemplo, si contribuyes con algo, esa contribución es transmitida no sólo a Misskey sino a otras redes sociales. Imagina que se parece a transmitir una onda de radio de un planeta a otro." about: "Misskey es un <b>Servicio de red social descentralizada de microblogging</b> de código abierto. Contiene una interfaz de usuario altamente personalizable, reacciones a posts, almacenamiento para poder manejar archivos y otras funciones avanzadas. Además de conectarse con la red llamada Fediverso, puede intercambiar mensajes con otras redes sociales. Por ejemplo, si contribuyes con algo, esa contribución es transmitida no sólo a Misskey sino a otras redes sociales. Imagina que se parece a transmitir una onda de radio de un planeta a otro."
features: "Características" features: "Características"
rich-contents: "Posts" rich-contents: "Posts"
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。" rich-contents-desc: "Escribe sobre tus pensamientos, eventos, todo lo que quieras compartir. Si es necesario, puedes usar varias sintaxis, decorar tus posts y añadir tus imágenes favoritas, archivos de viddeo y encuestas."
reaction: "Reacciones" reaction: "Reacciones"
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。" reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
ui: "Interfaz" ui: "Interfaz"
@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -788,7 +788,7 @@ desktop/views/components/settings.vue:
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。" auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
deck-nav: "Deck sans tansitions" deck-nav: "Deck sans tansitions"
deck-nav-desc: "Vous obtenez une colonne temporaire sans transitions dans la page pendant la navigation, lors de lutilisation du Deck." deck-nav-desc: "Vous obtenez une colonne temporaire sans transitions dans la page pendant la navigation, lors de lutilisation du Deck."
keep-cw: "CW保持" keep-cw: "Maintenir l'avertissement de contenu"
keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。" keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。"
deck-default: "Utiliser le Deck comme IU par défaut" deck-default: "Utiliser le Deck comme IU par défaut"
display: "Affichage et design" display: "Affichage et design"
@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "Utilisateur non trouvé" user-not-found: "Utilisateur non trouvé"
lookup: "Recherche" lookup: "Recherche"
reset-password: "Réinitialiser mot de passe" reset-password: "Réinitialiser mot de passe"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "Le mot de passe est « {password} »" password-updated: "Le mot de passe est « {password} »"
suspend: "Suspendre" suspend: "Suspendre"
suspend-confirm: "Désirez-vous suspendre ce compte ?"
suspended: "Suspendu avec succès." suspended: "Suspendu avec succès."
unsuspend: "Suspension levée" unsuspend: "Suspension levée"
unsuspend-confirm: "Souhaiteriez-vous ne plus suspendre ce compte ?"
unsuspended: "La suspension de lutilisateur a été levée avec succès" unsuspended: "La suspension de lutilisateur a été levée avec succès"
verify: "Vérification du compte" verify: "Vérification du compte"
verify-confirm: "Souhaiteriez-vous rendre votre compte comme étant un compte vérifié ?"
verified: "Le compte a été vérifié" verified: "Le compte a été vérifié"
unverify: "Enlever la vérification du compte" unverify: "Enlever la vérification du compte"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "Ce compte n'est plus vérifié" unverified: "Ce compte n'est plus vérifié"
update-remote-user: "Mettre à jour les informations de lutilisateur·rice distant·e"
remote-user-updated: "Les informations de lutilisateur·rice distant·e ont étés mis à jour"
users: users:
title: "Utilisateurs" title: "Utilisateurs"
sort: sort:
@ -1469,7 +1476,7 @@ mobile/views/pages/settings.vue:
notification-position-top: "en haut" notification-position-top: "en haut"
behavior: "Comportement" behavior: "Comportement"
fetch-on-scroll: "Chargement lors du défilement" fetch-on-scroll: "Chargement lors du défilement"
keep-cw: "CW保持" keep-cw: "Garder l'avertissement de contenu"
note-visibility: "Visibilité de la publication" note-visibility: "Visibilité de la publication"
default-note-visibility: "Visibilité par défaut" default-note-visibility: "Visibilité par défaut"
remember-note-visibility: "Se souvenir du mode de visibilité de la publication" remember-note-visibility: "Se souvenir du mode de visibilité de la publication"

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -730,10 +730,6 @@ desktop/views/components/drive.vue:
upload: "ファイルをアップロード" upload: "ファイルをアップロード"
url-upload: "URLからアップロード" url-upload: "URLからアップロード"
desktop/views/components/media-image.vue:
sensitive: "閲覧注意"
click-to-show: "クリックして表示"
desktop/views/components/media-video.vue: desktop/views/components/media-video.vue:
sensitive: "閲覧注意" sensitive: "閲覧注意"
click-to-show: "クリックして表示" click-to-show: "クリックして表示"
@ -980,6 +976,10 @@ desktop/views/components/settings.2fa.vue:
failed: "設定に失敗しました。トークンに誤りがないかご確認ください。" failed: "設定に失敗しました。トークンに誤りがないかご確認ください。"
info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。" info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。"
common/views/components/media-image.vue:
sensitive: "閲覧注意"
click-to-show: "クリックして表示"
common/views/components/api-settings.vue: common/views/components/api-settings.vue:
intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。" intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。"
caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。" caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
@ -1266,15 +1266,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:
@ -1486,10 +1493,6 @@ mobile/views/components/drive.file-detail.vue:
mark-as-sensitive: "閲覧注意に設定" mark-as-sensitive: "閲覧注意に設定"
unmark-as-sensitive: "閲覧注意を解除" unmark-as-sensitive: "閲覧注意を解除"
mobile/views/components/media-image.vue:
sensitive: "閲覧注意"
click-to-show: "クリックして表示"
mobile/views/components/media-video.vue: mobile/views/components/media-video.vue:
sensitive: "閲覧注意" sensitive: "閲覧注意"
click-to-show: "クリックして表示" click-to-show: "クリックして表示"

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つからへん!" user-not-found: "ユーザーが見つからへん!"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password} 」やで" password-updated: "パスワードは現在「{password} 」やで"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "사용자를 찾을 수 없습니다" user-not-found: "사용자를 찾을 수 없습니다"
lookup: "조회" lookup: "조회"
reset-password: "암호 재설정" reset-password: "암호 재설정"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "암호는 현재 \"{password}\" 입니다" password-updated: "암호는 현재 \"{password}\" 입니다"
suspend: "정지" suspend: "정지"
suspend-confirm: "凍結しますか?"
suspended: "정지하였습니다" suspended: "정지하였습니다"
unsuspend: "정지 해제" unsuspend: "정지 해제"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "정지를 해제하였습니다" unsuspended: "정지를 해제하였습니다"
verify: "공식 계정으로 설정" verify: "공식 계정으로 설정"
verify-confirm: "公式アカウントにしますか?"
verified: "공식 계정으로 설정하였습니다" verified: "공식 계정으로 설정하였습니다"
unverify: "공식 계정 해제" unverify: "공식 계정 해제"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "공식 계정을 해제하였습니다" unverified: "공식 계정을 해제하였습니다"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "사용자" title: "사용자"
sort: sort:

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "Nie znaleziono użytkownika" user-not-found: "Nie znaleziono użytkownika"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "Użytkownicy" title: "Użytkownicy"
sort: sort:

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort: sort:

View File

@ -1134,15 +1134,22 @@ admin/views/users.vue:
user-not-found: "用户不存在" user-not-found: "用户不存在"
lookup: "订阅" lookup: "订阅"
reset-password: "密码重置" reset-password: "密码重置"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "密码为「{password}」" password-updated: "密码为「{password}」"
suspend: "被冻结" suspend: "被冻结"
suspend-confirm: "凍結しますか?"
suspended: "成功冻结用户" suspended: "成功冻结用户"
unsuspend: "已解除冻结" unsuspend: "已解除冻结"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "已成功解除用户冻结" unsuspended: "已成功解除用户冻结"
verify: "认证用户" verify: "认证用户"
verify-confirm: "公式アカウントにしますか?"
verified: "此账户已被认证" verified: "此账户已被认证"
unverify: "解除账户认证" unverify: "解除账户认证"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "该帐户未经认证" unverified: "该帐户未经认证"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "用户" title: "用户"
sort: sort:

View File

@ -97,8 +97,8 @@
"bootstrap-vue": "2.0.0-rc.11", "bootstrap-vue": "2.0.0-rc.11",
"cafy": "12.0.0", "cafy": "12.0.0",
"chai": "4.2.0", "chai": "4.2.0",
"chalk": "2.4.2",
"chai-http": "4.2.1", "chai-http": "4.2.1",
"chalk": "2.4.2",
"commander": "2.19.0", "commander": "2.19.0",
"crc-32": "1.2.0", "crc-32": "1.2.0",
"css-loader": "1.0.1", "css-loader": "1.0.1",
@ -178,13 +178,14 @@
"parsimmon": "1.12.0", "parsimmon": "1.12.0",
"portscanner": "2.2.0", "portscanner": "2.2.0",
"postcss-loader": "3.0.0", "postcss-loader": "3.0.0",
"prismjs": "1.15.0",
"progress-bar-webpack-plugin": "1.12.0", "progress-bar-webpack-plugin": "1.12.0",
"promise-any": "0.2.0", "promise-any": "0.2.0",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"promise-sequential": "1.1.1", "promise-sequential": "1.1.1",
"pug": "2.0.3", "pug": "2.0.3",
"punycode": "2.1.1", "punycode": "2.1.1",
"qrcode": "1.3.2", "qrcode": "1.3.3",
"randomcolor": "0.5.3", "randomcolor": "0.5.3",
"ratelimiter": "3.2.0", "ratelimiter": "3.2.0",
"recaptcha-promise": "0.1.3", "recaptcha-promise": "0.1.3",
@ -230,6 +231,7 @@
"vue-js-modal": "1.3.28", "vue-js-modal": "1.3.28",
"vue-loader": "15.5.1", "vue-loader": "15.5.1",
"vue-marquee-text-component": "1.1.1", "vue-marquee-text-component": "1.1.1",
"vue-prism-component": "1.1.1",
"vue-router": "3.0.2", "vue-router": "3.0.2",
"vue-sequential-entrance": "1.1.3", "vue-sequential-entrance": "1.1.3",
"vue-style-loader": "4.1.2", "vue-style-loader": "4.1.2",

View File

@ -0,0 +1,82 @@
<template>
<div class="kofvwchc">
<div>
<a :href="user | userPage(null, true)">
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
</a>
</div>
<div>
<header>
<b><mk-user-name :user="user"/></b>
<span class="username">@{{ user | acct }}</span>
<span class="is-admin" v-if="user.isAdmin">admin</span>
<span class="is-moderator" v-if="user.isModerator">moderator</span>
<span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span>
<span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span>
</header>
<div>
<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
</div>
<div>
<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('admin/views/users.vue'),
props: ['user'],
data() {
return {
faSnowflake
};
},
});
</script>
<style lang="stylus" scoped>
.kofvwchc
display flex
padding 16px 0
border-top solid 1px var(--faceDivider)
> div:first-child
> a
> .avatar
width 64px
height 64px
> div:last-child
flex 1
padding-left 16px
@media (max-width 500px)
font-size 14px
> header
> .username
margin-left 8px
opacity 0.7
> .is-admin
> .is-moderator
flex-shrink 0
align-self center
margin 0 0 0 .5em
padding 1px 6px
font-size 80%
border-radius 3px
background var(--noteHeaderAdminBg)
color var(--noteHeaderAdminFg)
> .is-verified
> .is-suspended
margin 0 0 0 .5em
color #4dabf7
</style>

View File

@ -3,9 +3,14 @@
<ui-card> <ui-card>
<div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div> <div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div>
<section class="fit-top"> <section class="fit-top">
<ui-input v-model="target" type="text"> <ui-input class="target" v-model="target" type="text">
<span>{{ $t('username-or-userid') }}</span> <span>{{ $t('username-or-userid') }}</span>
</ui-input> </ui-input>
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
<div class="user" v-if="user">
<x-user :user='user'/>
<div class="actions">
<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button> <ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
<ui-horizon-group> <ui-horizon-group>
<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button> <ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
@ -15,8 +20,10 @@
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button> <ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> <ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
</ui-horizon-group> </ui-horizon-group>
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> <ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button>
<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea> <ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
</div>
</div>
</section> </section>
</ui-card> </ui-card>
@ -47,29 +54,7 @@
</ui-select> </ui-select>
</ui-horizon-group> </ui-horizon-group>
<sequential-entrance animation="entranceFromTop" delay="25"> <sequential-entrance animation="entranceFromTop" delay="25">
<div class="kofvwchc" v-for="user in users" :key="user.id"> <x-user v-for="user in users" :user='user' :key="user.id"/>
<div>
<a :href="user | userPage(null, true)">
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
</a>
</div>
<div>
<header>
<b><mk-user-name :user="user"/></b>
<span class="username">@{{ user | acct }}</span>
<span class="is-admin" v-if="user.isAdmin">admin</span>
<span class="is-moderator" v-if="user.isModerator">moderator</span>
<span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span>
<span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span>
</header>
<div>
<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
</div>
<div>
<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
</div>
</div>
</div>
</sequential-entrance> </sequential-entrance>
<ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button> <ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button>
</section> </section>
@ -81,12 +66,15 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import parseAcct from "../../../../misc/acct/parse"; import parseAcct from "../../../../misc/acct/parse";
import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons'; import { faCertificate, faUsers, faTerminal, faSearch, faKey, faSync } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
import XUser from './users.user.vue';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('admin/views/users.vue'), i18n: i18n('admin/views/users.vue'),
components: {
XUser
},
data() { data() {
return { return {
user: null, user: null,
@ -102,7 +90,7 @@ export default Vue.extend({
offset: 0, offset: 0,
users: [], users: [],
existMore: false, existMore: false,
faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey, faSync
}; };
}, },
@ -131,6 +119,7 @@ export default Vue.extend({
}, },
methods: { methods: {
/** テキストエリアのユーザーを解決する */
async fetchUser() { async fetchUser() {
try { try {
return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target }); return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target });
@ -149,16 +138,27 @@ export default Vue.extend({
} }
}, },
/** テキストエリアから処理対象ユーザーを設定する */
async showUser() { async showUser() {
this.user = null;
const user = await this.fetchUser(); const user = await this.fetchUser();
this.$root.api('admin/show-user', { userId: user.id }).then(info => { this.$root.api('admin/show-user', { userId: user.id }).then(info => {
this.user = info; this.user = info;
}); });
this.target = '';
},
/** 処理対象ユーザーの情報を更新する */
async refreshUser() {
this.$root.api('admin/show-user', { userId: this.user._id }).then(info => {
this.user = info;
});
}, },
async resetPassword() { async resetPassword() {
const user = await this.fetchUser(); if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return;
this.$root.api('admin/reset-password', { userId: user.id }).then(res => {
this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => {
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('password-updated', { password: res.password }) text: this.$t('password-updated', { password: res.password })
@ -167,11 +167,12 @@ export default Vue.extend({
}, },
async verifyUser() { async verifyUser() {
if (!await this.getConfirmed(this.$t('verify-confirm'))) return;
this.verifying = true; this.verifying = true;
const process = async () => { const process = async () => {
const user = await this.fetchUser(); await this.$root.api('admin/verify-user', { userId: this.user._id });
await this.$root.api('admin/verify-user', { userId: user.id });
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('verified') text: this.$t('verified')
@ -186,14 +187,17 @@ export default Vue.extend({
}); });
this.verifying = false; this.verifying = false;
this.refreshUser();
}, },
async unverifyUser() { async unverifyUser() {
if (!await this.getConfirmed(this.$t('unverify-confirm'))) return;
this.unverifying = true; this.unverifying = true;
const process = async () => { const process = async () => {
const user = await this.fetchUser(); await this.$root.api('admin/unverify-user', { userId: this.user._id });
await this.$root.api('admin/unverify-user', { userId: user.id });
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('unverified') text: this.$t('unverified')
@ -208,14 +212,17 @@ export default Vue.extend({
}); });
this.unverifying = false; this.unverifying = false;
this.refreshUser();
}, },
async suspendUser() { async suspendUser() {
if (!await this.getConfirmed(this.$t('suspend-confirm'))) return;
this.suspending = true; this.suspending = true;
const process = async () => { const process = async () => {
const user = await this.fetchUser(); await this.$root.api('admin/suspend-user', { userId: this.user._id });
await this.$root.api('admin/suspend-user', { userId: user.id });
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('suspended') text: this.$t('suspended')
@ -230,14 +237,17 @@ export default Vue.extend({
}); });
this.suspending = false; this.suspending = false;
this.refreshUser();
}, },
async unsuspendUser() { async unsuspendUser() {
if (!await this.getConfirmed(this.$t('unsuspend-confirm'))) return;
this.unsuspending = true; this.unsuspending = true;
const process = async () => { const process = async () => {
const user = await this.fetchUser(); await this.$root.api('admin/unsuspend-user', { userId: this.user._id });
await this.$root.api('admin/unsuspend-user', { userId: user.id });
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('unsuspended') text: this.$t('unsuspended')
@ -252,8 +262,32 @@ export default Vue.extend({
}); });
this.unsuspending = false; this.unsuspending = false;
this.refreshUser();
}, },
async updateRemoteUser() {
this.$root.api('admin/update-remote-user', { userId: this.user._id }).then(res => {
this.$root.dialog({
type: 'success',
text: this.$t('remote-user-updated')
});
});
this.refreshUser();
},
async getConfirmed(text: string): Promise<Boolean> {
const confirm = await this.$root.dialog({
type: 'warning',
showCancelButton: true,
title: 'confirm',
text,
});
return !confirm.canceled;
}
fetchUsers() { fetchUsers() {
this.$root.api('admin/show-users', { this.$root.api('admin/show-users', {
state: this.state, state: this.state,
@ -277,42 +311,12 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.kofvwchc .target
display flex margin-bottom 16px !important
padding 16px 0
border-top solid 1px var(--faceDivider)
> div:first-child .user
> a margin-top 32px
> .avatar
width 64px
height 64px
> div:last-child > .actions
flex 1 margin-left 80px
padding-left 16px
@media (max-width 500px)
font-size 14px
> header
> .username
margin-left 8px
opacity 0.7
> .is-admin
> .is-moderator
flex-shrink 0
align-self center
margin 0 0 0 .5em
padding 1px 6px
font-size 80%
border-radius 3px
background var(--noteHeaderAdminBg)
color var(--noteHeaderAdminFg)
> .is-verified
> .is-suspended
margin 0 0 0 .5em
color #4dabf7
</style> </style>

View File

@ -26,3 +26,8 @@
transform: translateY(0); transform: translateY(0);
} }
} }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -72,47 +72,6 @@ body
code code
font-family Consolas, 'Courier New', Courier, Monaco, monospace font-family Consolas, 'Courier New', Courier, Monaco, monospace
.comment
opacity 0.5
.string
color #e96900
.regexp
color #e9003f
.keyword
color #2973b7
&.true
&.false
&.null
&.nil
&.undefined
color #ae81ff
.symbol
color #42b983
.number
.nan
color #ae81ff
.var:not(.keyword)
font-weight bold
font-style italic
//text-decoration underline
.method
font-style italic
color #8964c1
.property
color #a71d5d
.label
color #e9003f
pre pre
display block display block

View File

@ -133,6 +133,7 @@ export default prop => ({
case 'deleted': { case 'deleted': {
Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt); Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt);
Vue.set(this.$_ns_target, 'renote', null);
this.$_ns_target.text = null; this.$_ns_target.text = null;
this.$_ns_target.tags = []; this.$_ns_target.tags = [];
this.$_ns_target.fileIds = []; this.$_ns_target.fileIds = [];

View File

@ -0,0 +1,30 @@
<template>
<prism :inline="inline" :language="lang">{{ code }}</prism>
</template>
<script lang="ts">
import Vue from 'vue';
import 'prismjs';
import 'prismjs/themes/prism.css';
import Prism from 'vue-prism-component';
export default Vue.extend({
components: {
Prism
},
props: {
code: {
type: String,
required: true
},
lang: {
type: String,
required: false
},
inline: {
type: Boolean,
required: false
}
}
});
</script>

View File

@ -0,0 +1,28 @@
<template>
<x-code :code="code" :lang="lang" :inline="inline"/>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
components: {
XCode: () => import('./code-core.vue').then(m => m.default)
},
props: {
code: {
type: String,
required: true
},
lang: {
type: String,
required: false
},
inline: {
type: Boolean,
required: false
}
}
});
</script>

View File

@ -5,16 +5,21 @@
<span>{{ $t('click-to-show') }}</span> <span>{{ $t('click-to-show') }}</span>
</div> </div>
</div> </div>
<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else :href="image.url" target="_blank" :style="style" :title="image.name" @click.prevent="onClick"></a> <a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else
:href="image.url"
:style="style"
:title="image.name"
@click.prevent="onClick"
></a>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import ImageViewer from '../../../common/views/components/image-viewer.vue'; import ImageViewer from './image-viewer.vue';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('mobile/views/components/media-image.vue'), i18n: i18n('common/views/components/media-image.vue'),
props: { props: {
image: { image: {
type: Object, type: Object,
@ -58,6 +63,7 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.gqnyydlzavusgskkfvwvjiattxdzsqlf .gqnyydlzavusgskkfvwvjiattxdzsqlf
display block display block
cursor zoom-in
overflow hidden overflow hidden
width 100% width 100%
height 100% height 100%

View File

@ -7,7 +7,7 @@
<div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid"> <div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
<template v-for="media in mediaList"> <template v-for="media in mediaList">
<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/> <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
<mk-media-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/> <x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
</template> </template>
</div> </div>
</div> </div>
@ -17,10 +17,12 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XBanner from './media-banner.vue'; import XBanner from './media-banner.vue';
import XImage from './media-image.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XBanner XBanner,
XImage
}, },
props: { props: {
mediaList: { mediaList: {

View File

@ -6,8 +6,8 @@ import MkUrl from './url.vue';
import MkMention from './mention.vue'; import MkMention from './mention.vue';
import { concat, sum } from '../../../../../prelude/array'; import { concat, sum } from '../../../../../prelude/array';
import MkFormula from './formula.vue'; import MkFormula from './formula.vue';
import MkCode from './code.vue';
import MkGoogle from './google.vue'; import MkGoogle from './google.vue';
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
import { host } from '../../../config'; import { host } from '../../../config';
import { preorderF, countNodesF } from '../../../../../prelude/tree'; import { preorderF, countNodesF } from '../../../../../prelude/tree';
@ -124,6 +124,25 @@ export default Vue.component('misskey-flavored-markdown', {
}, genEl(token.children)); }, genEl(token.children));
} }
case 'spin': {
motionCount++;
const isLong = sumTextsLength(token.children) > 5 || countNodesF(token.children) > 3;
const isMany = motionCount > 3;
return (createElement as any)('span', {
attrs: {
style: (this.$store.state.settings.disableAnimatedMfm || isLong || isMany) ? 'display: inline-block;' : 'display: inline-block; animation: spin 1.5s linear infinite;'
},
}, genEl(token.children));
}
case 'flip': {
return (createElement as any)('span', {
attrs: {
style: 'display: inline-block; transform: scaleX(-1);'
},
}, genEl(token.children));
}
case 'url': { case 'url': {
return [createElement(MkUrl, { return [createElement(MkUrl, {
key: Math.random(), key: Math.random(),
@ -170,21 +189,22 @@ export default Vue.component('misskey-flavored-markdown', {
} }
case 'blockCode': { case 'blockCode': {
return [createElement('pre', { return [createElement(MkCode, {
class: 'code' key: Math.random(),
}, [ props: {
createElement('code', { code: token.node.props.code,
domProps: { lang: token.node.props.lang,
innerHTML: syntaxHighlight(token.node.props.code)
} }
}) })];
])];
} }
case 'inlineCode': { case 'inlineCode': {
return [createElement('code', { return [createElement(MkCode, {
domProps: { key: Math.random(),
innerHTML: syntaxHighlight(token.node.props.code) props: {
code: token.node.props.code,
lang: token.node.props.lang,
inline: true
} }
})]; })];
} }

View File

@ -24,34 +24,10 @@ export default Vue.extend({
background var(--mfmTitleBg) background var(--mfmTitleBg)
border-radius 4px border-radius 4px
>>> .code
margin 8px 0
>>> .quote >>> .quote
margin 8px margin 8px
padding 6px 0 6px 12px padding 6px 0 6px 12px
color var(--mfmQuote) color var(--mfmQuote)
border-left solid 3px var(--mfmQuoteLine) border-left solid 3px var(--mfmQuoteLine)
>>> code
padding 4px 8px
margin 0 0.5em
font-size 80%
color #525252
background rgba(0, 0, 0, 0.05)
border-radius 2px
>>> pre > code
padding 16px
margin 0
>>> [data-is-me]:after
content "you"
padding 0 4px
margin-left 4px
font-size 80%
color var(--primaryForeground)
background var(--primary)
border-radius 4px
</style> </style>

View File

@ -1,21 +1,23 @@
<template> <template>
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom"> <mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
<span slot="header"> <span slot="header" class="jqiaciqv">
<span :class="$style.title">{{ $t('choose-prompt') }}</span> <span class="title">{{ $t('choose-prompt') }}</span>
<span :class="$style.count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span> <span class="count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span>
</span> </span>
<div class="rqsvbumu">
<x-drive <x-drive
ref="browser" ref="browser"
:class="$style.browser" class="browser"
:multiple="multiple" :multiple="multiple"
@selected="onSelected" @selected="onSelected"
@change-selection="onChangeSelection" @change-selection="onChangeSelection"
/> />
<div :class="$style.footer"> <div class="footer">
<button :class="$style.upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button> <button class="upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button>
<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button> <ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button>
<button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</button> <ui-button inline primary :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</ui-button>
</div>
</div> </div>
</mk-window> </mk-window>
</template> </template>
@ -60,23 +62,31 @@ export default Vue.extend({
}); });
</script> </script>
<style lang="stylus" module> <style lang="stylus" scoped>
.title .jqiaciqv
.title
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px
.count .count
margin-left 8px margin-left 8px
opacity 0.7 opacity 0.7
.browser .rqsvbumu
height calc(100% - 72px) display flex
flex-direction column
height 100%
.footer .browser
height 72px flex 1
background var(--primaryLighten95) overflow auto
.upload .footer
padding 16px
background var(--desktopPostFormBg)
text-align right
.upload
display inline-block display inline-block
position absolute position absolute
top 8px top 8px
@ -115,65 +125,4 @@ export default Vue.extend({
border 2px solid var(--primaryAlpha03) border 2px solid var(--primaryAlpha03)
border-radius 8px border-radius 8px
.ok
.cancel
display block
position absolute
bottom 16px
cursor pointer
padding 0
margin 0
width 120px
height 40px
font-size 1em
outline none
border-radius 4px
&:focus
&:after
content ""
pointer-events none
position absolute
top -5px
right -5px
bottom -5px
left -5px
border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
opacity 0.7
cursor default
.ok
right 16px
color var(--primaryForeground)
background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
border solid 1px var(--primaryLighten15)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
border-color var(--primary)
&:active:not(:disabled)
background var(--primary)
border-color var(--primary)
.cancel
right 148px
color #888
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
border solid 1px #e2e2e2
&:hover
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
border-color #dcdcdc
&:active
background #ececec
border-color #dcdcdc
</style> </style>

View File

@ -1,17 +1,19 @@
<template> <template>
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom"> <mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
<span slot="header"> <span slot="header">
<span :class="$style.title">{{ $t('choose-prompt') }}</span> <span>{{ $t('choose-prompt') }}</span>
</span> </span>
<div class="hllkpxxu">
<x-drive <x-drive
ref="browser" ref="browser"
:class="$style.browser" class="browser"
:multiple="false" :multiple="false"
/> />
<div :class="$style.footer"> <div class="footer">
<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button> <ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button>
<button :class="$style.ok" @click="ok">{{ $t('ok') }}</button> <ui-button inline @click="ok" primary>{{ $t('ok') }}</ui-button>
</div>
</div> </div>
</mk-window> </mk-window>
</template> </template>
@ -36,79 +38,19 @@ export default Vue.extend({
}); });
</script> </script>
<style lang="stylus" module> <style lang="stylus" scoped>
.hllkpxxu
display flex
flex-direction column
height 100%
.browser
flex 1
overflow auto
.title .footer
> [data-icon] padding 16px
margin-right 4px background var(--desktopPostFormBg)
text-align right
.browser
height calc(100% - 72px)
.footer
height 72px
background var(--primaryLighten95)
.ok
.cancel
display block
position absolute
bottom 16px
cursor pointer
padding 0
margin 0
width 120px
height 40px
font-size 1em
outline none
border-radius 4px
&:focus
&:after
content ""
pointer-events none
position absolute
top -5px
right -5px
bottom -5px
left -5px
border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
opacity 0.7
cursor default
.ok
right 16px
color var(--primaryForeground)
background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
border solid 1px var(--primaryLighten15)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
border-color var(--primary)
&:active:not(:disabled)
background var(--primary)
border-color var(--primary)
.cancel
right 148px
color #888
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
border solid 1px #e2e2e2
&:hover
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
border-color #dcdcdc
&:active
background #ececec
border-color #dcdcdc
</style> </style>

View File

@ -9,7 +9,6 @@ import subNoteContent from './sub-note-content.vue';
import window from './window.vue'; import window from './window.vue';
import noteFormWindow from './post-form-window.vue'; import noteFormWindow from './post-form-window.vue';
import renoteFormWindow from './renote-form-window.vue'; import renoteFormWindow from './renote-form-window.vue';
import mediaImage from './media-image.vue';
import mediaVideo from './media-video.vue'; import mediaVideo from './media-video.vue';
import notifications from './notifications.vue'; import notifications from './notifications.vue';
import noteForm from './post-form.vue'; import noteForm from './post-form.vue';
@ -32,7 +31,6 @@ Vue.component('mk-sub-note-content', subNoteContent);
Vue.component('mk-window', window); Vue.component('mk-window', window);
Vue.component('mk-post-form-window', noteFormWindow); Vue.component('mk-post-form-window', noteFormWindow);
Vue.component('mk-renote-form-window', renoteFormWindow); Vue.component('mk-renote-form-window', renoteFormWindow);
Vue.component('mk-media-image', mediaImage);
Vue.component('mk-media-video', mediaVideo); Vue.component('mk-media-video', mediaVideo);
Vue.component('mk-notifications', notifications); Vue.component('mk-notifications', notifications);
Vue.component('mk-post-form', noteForm); Vue.component('mk-post-form', noteForm);

View File

@ -1,81 +0,0 @@
<template>
<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
<div>
<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b>
<span>{{ $t('click-to-show') }}</span>
</div>
</div>
<a class="lcjomzwbohoelkxsnuqjiaccdbdfiazy" v-else
:href="image.url"
@click.prevent="onClick"
:style="style"
:title="image.name"
></a>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import ImageViewer from '../../../common/views/components/image-viewer.vue';
export default Vue.extend({
i18n: i18n('desktop/views/components/media-image.vue'),
props: {
image: {
type: Object,
required: true
},
raw: {
default: false
}
},
data() {
return {
hide: true
};
},
computed: {
style(): any {
return {
'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.thumbnailUrl})`
};
}
},
methods: {
onClick() {
this.$root.new(ImageViewer, {
image: this.image
});
}
}
});
</script>
<style lang="stylus" scoped>
.lcjomzwbohoelkxsnuqjiaccdbdfiazy
display block
cursor zoom-in
overflow hidden
width 100%
height 100%
background-position center
background-size contain
background-repeat no-repeat
.ldwbgwstjsdgcjruamauqdrffetqudry
display flex
justify-content center
align-items center
background #111
color #fff
> div
display table-cell
text-align center
font-size 12px
> *
display block
</style>

View File

@ -3,7 +3,6 @@ import Vue from 'vue';
import ui from './ui.vue'; import ui from './ui.vue';
import note from './note.vue'; import note from './note.vue';
import notes from './notes.vue'; import notes from './notes.vue';
import mediaImage from './media-image.vue';
import mediaVideo from './media-video.vue'; import mediaVideo from './media-video.vue';
import notePreview from './note-preview.vue'; import notePreview from './note-preview.vue';
import subNoteContent from './sub-note-content.vue'; import subNoteContent from './sub-note-content.vue';
@ -24,7 +23,6 @@ import postForm from './post-form.vue';
Vue.component('mk-ui', ui); Vue.component('mk-ui', ui);
Vue.component('mk-note', note); Vue.component('mk-note', note);
Vue.component('mk-notes', notes); Vue.component('mk-notes', notes);
Vue.component('mk-media-image', mediaImage);
Vue.component('mk-media-video', mediaVideo); Vue.component('mk-media-video', mediaVideo);
Vue.component('mk-note-preview', notePreview); Vue.component('mk-note-preview', notePreview);
Vue.component('mk-sub-note-content', subNoteContent); Vue.component('mk-sub-note-content', subNoteContent);

View File

@ -3,6 +3,8 @@
html html
--primary #fb4e4e --primary #fb4e4e
--link #fb4e4e
--linkTapHighlight #fb4e4eb3
body body
margin 0 margin 0

View File

@ -100,20 +100,6 @@ export default class Reversi {
return count(WHITE, this.board); return count(WHITE, this.board);
} }
/**
*
*/
public get blackP() {
return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.blackCount / (this.blackCount + this.whiteCount);
}
/**
*
*/
public get whiteP() {
return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.whiteCount / (this.blackCount + this.whiteCount);
}
public transformPosToXy(pos: number): number[] { public transformPosToXy(pos: number): number[] {
const x = pos % this.mapWidth; const x = pos % this.mapWidth;
const y = Math.floor(pos / this.mapWidth); const y = Math.floor(pos / this.mapWidth);

View File

@ -55,6 +55,18 @@ export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteU
return el; return el;
}, },
spin(token) {
const el = doc.createElement('i');
appendChildren(token.children, el);
return el;
},
flip(token) {
const el = doc.createElement('span');
appendChildren(token.children, el);
return el;
},
blockCode(token) { blockCode(token) {
const pre = doc.createElement('pre'); const pre = doc.createElement('pre');
const inner = doc.createElement('code'); const inner = doc.createElement('code');

View File

@ -91,6 +91,7 @@ const mfm = P.createLanguage({
root: r => P.alt( root: r => P.alt(
r.big, r.big,
r.small, r.small,
r.spin,
r.bold, r.bold,
r.strike, r.strike,
r.italic, r.italic,
@ -101,6 +102,7 @@ const mfm = P.createLanguage({
r.hashtag, r.hashtag,
r.emoji, r.emoji,
r.blockCode, r.blockCode,
r.flip,
r.inlineCode, r.inlineCode,
r.quote, r.quote,
r.mathInline, r.mathInline,
@ -123,6 +125,7 @@ const mfm = P.createLanguage({
r.hashtag, r.hashtag,
r.emoji, r.emoji,
r.mathInline, r.mathInline,
r.spin,
r.text r.text
).atLeast(1).tryParse(x), {})), ).atLeast(1).tryParse(x), {})),
//#endregion //#endregion
@ -141,6 +144,15 @@ const mfm = P.createLanguage({
).atLeast(1).tryParse(x), {})), ).atLeast(1).tryParse(x), {})),
//#endregion //#endregion
//#region Spin
spin: r =>
P.regexp(/<spin>(.+?)<\/spin>/, 1)
.map(x => createTree('spin', P.alt(
r.emoji,
r.text
).atLeast(1).tryParse(x), {})),
//#endregion
//#region Block code //#region Block code
blockCode: r => blockCode: r =>
newline.then( newline.then(
@ -163,6 +175,7 @@ const mfm = P.createLanguage({
r.hashtag, r.hashtag,
r.url, r.url,
r.link, r.link,
r.flip,
r.emoji, r.emoji,
r.text r.text
).atLeast(1).tryParse(x), {})), ).atLeast(1).tryParse(x), {})),
@ -174,6 +187,7 @@ const mfm = P.createLanguage({
.map(x => createTree('center', P.alt( .map(x => createTree('center', P.alt(
r.big, r.big,
r.small, r.small,
r.spin,
r.bold, r.bold,
r.strike, r.strike,
r.italic, r.italic,
@ -184,6 +198,7 @@ const mfm = P.createLanguage({
r.mathInline, r.mathInline,
r.url, r.url,
r.link, r.link,
r.flip,
r.text r.text
).atLeast(1).tryParse(x), {})), ).atLeast(1).tryParse(x), {})),
//#endregion //#endregion
@ -217,6 +232,23 @@ const mfm = P.createLanguage({
}), }),
//#endregion //#endregion
//#region Flip
flip: r =>
P.regexp(/<flip>(.+?)<\/flip>/, 1)
.map(x => createTree('flip', P.alt(
r.big,
r.small,
r.spin,
r.bold,
r.strike,
r.link,
r.italic,
r.motion,
r.emoji,
r.text
).atLeast(1).tryParse(x), {})),
//#endregion
//#region Inline code //#region Inline code
inlineCode: r => inlineCode: r =>
P.regexp(/`([^´\n]+?)`/, 1) P.regexp(/`([^´\n]+?)`/, 1)
@ -242,6 +274,7 @@ const mfm = P.createLanguage({
r.hashtag, r.hashtag,
r.url, r.url,
r.link, r.link,
r.flip,
r.emoji, r.emoji,
r.text r.text
).atLeast(1).tryParse(x), {})), ).atLeast(1).tryParse(x), {})),
@ -262,6 +295,7 @@ const mfm = P.createLanguage({
return createTree('link', P.alt( return createTree('link', P.alt(
r.big, r.big,
r.small, r.small,
r.spin,
r.bold, r.bold,
r.strike, r.strike,
r.italic, r.italic,
@ -311,6 +345,7 @@ const mfm = P.createLanguage({
.map(x => createTree('motion', P.alt( .map(x => createTree('motion', P.alt(
r.bold, r.bold,
r.small, r.small,
r.spin,
r.strike, r.strike,
r.italic, r.italic,
r.mention, r.mention,
@ -318,6 +353,7 @@ const mfm = P.createLanguage({
r.emoji, r.emoji,
r.url, r.url,
r.link, r.link,
r.flip,
r.mathInline, r.mathInline,
r.text r.text
).atLeast(1).tryParse(x), {})), ).atLeast(1).tryParse(x), {})),
@ -356,6 +392,7 @@ const mfm = P.createLanguage({
r.hashtag, r.hashtag,
r.url, r.url,
r.link, r.link,
r.flip,
r.emoji, r.emoji,
r.text r.text
).atLeast(1).tryParse(x), {})), ).atLeast(1).tryParse(x), {})),
@ -365,18 +402,20 @@ const mfm = P.createLanguage({
title: r => title: r =>
newline.then(P((input, i) => { newline.then(P((input, i) => {
const text = input.substr(i); const text = input.substr(i);
const match = text.match(/^((【|\[)(.+?)(】|]))(\n|$)/); const match = text.match(/^([【\[]([^【\[】\]\n]+?)[】\]])(\n|$)/);
if (!match) return P.makeFailure(i, 'not a title'); if (!match) return P.makeFailure(i, 'not a title');
const q = match[1].trim().substring(1, match[1].length - 1); const q = match[2].trim();
const contents = P.alt( const contents = P.alt(
r.big, r.big,
r.small, r.small,
r.spin,
r.bold, r.bold,
r.strike, r.strike,
r.italic, r.italic,
r.motion, r.motion,
r.url, r.url,
r.link, r.link,
r.flip,
r.mention, r.mention,
r.hashtag, r.hashtag,
r.emoji, r.emoji,

View File

@ -1,343 +0,0 @@
import { capitalize, toUpperCase } from '../prelude/string';
function escape(text: string) {
return text
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;');
}
// 文字数が多い順にソートします
// そうしないと、「function」という文字列が与えられたときに「func」が先にマッチしてしまう可能性があるためです
const _keywords = [
'true',
'false',
'null',
'nil',
'undefined',
'void',
'var',
'const',
'let',
'mut',
'dim',
'if',
'then',
'else',
'switch',
'match',
'case',
'default',
'for',
'each',
'in',
'while',
'loop',
'continue',
'break',
'do',
'goto',
'next',
'end',
'sub',
'throw',
'try',
'catch',
'finally',
'enum',
'delegate',
'function',
'func',
'fun',
'fn',
'return',
'yield',
'async',
'await',
'require',
'include',
'import',
'imports',
'export',
'exports',
'from',
'as',
'using',
'use',
'internal',
'module',
'namespace',
'where',
'select',
'struct',
'union',
'new',
'delete',
'this',
'super',
'base',
'class',
'interface',
'abstract',
'static',
'public',
'private',
'protected',
'virtual',
'partial',
'override',
'extends',
'implements',
'constructor'
];
const keywords = _keywords
.concat(_keywords.map(capitalize))
.concat(_keywords.map(toUpperCase))
.sort((a, b) => b.length - a.length);
const symbols = [
'=',
'+',
'-',
'*',
'/',
'%',
'~',
'^',
'&',
'|',
'>',
'<',
'!',
'?'
];
type Token = {
html: string
next: number
};
type Element = (code: string, i: number, source: string) => (Token | null);
const elements: Element[] = [
// comment
code => {
if (code.substr(0, 2) != '//') return null;
const match = code.match(/^\/\/(.+?)(\n|$)/);
if (!match) return null;
const comment = match[0];
return {
html: `<span class="comment">${escape(comment)}</span>`,
next: comment.length
};
},
// block comment
code => {
const match = code.match(/^\/\*([\s\S]+?)\*\//);
if (!match) return null;
return {
html: `<span class="comment">${escape(match[0])}</span>`,
next: match[0].length
};
},
// string
code => {
if (!/^['"`]/.test(code)) return null;
const begin = code[0];
let str = begin;
let thisIsNotAString = false;
for (let i = 1; i < code.length; i++) {
const char = code[i];
if (char == '\\') {
str += char;
str += code[i + 1] || '';
i++;
continue;
} else if (char == begin) {
str += char;
break;
} else if (char == '\n' || i == (code.length - 1)) {
thisIsNotAString = true;
break;
} else {
str += char;
}
}
if (thisIsNotAString) {
return null;
} else {
return {
html: `<span class="string">${escape(str)}</span>`,
next: str.length
};
}
},
// regexp
code => {
if (code[0] != '/') return null;
let regexp = '';
let thisIsNotARegexp = false;
for (let i = 1; i < code.length; i++) {
const char = code[i];
if (char == '\\') {
regexp += char;
regexp += code[i + 1] || '';
i++;
continue;
} else if (char == '/') {
break;
} else if (char == '\n' || i == (code.length - 1)) {
thisIsNotARegexp = true;
break;
} else {
regexp += char;
}
}
if (thisIsNotARegexp) return null;
if (regexp == '') return null;
if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null;
return {
html: `<span class="regexp">/${escape(regexp)}/</span>`,
next: regexp.length + 2
};
},
// label
code => {
if (code[0] != '@') return null;
const match = code.match(/^@([a-zA-Z_-]+?)\n/);
if (!match) return null;
const label = match[0];
return {
html: `<span class="label">${label}</span>`,
next: label.length
};
},
// number
(code, i, source) => {
const prev = source[i - 1];
if (prev && /[a-zA-Z]/.test(prev)) return null;
if (!/^[\-\+]?[0-9\.]+/.test(code)) return null;
const match = code.match(/^[\-\+]?[0-9\.]+/)[0];
if (match) {
return {
html: `<span class="number">${match}</span>`,
next: match.length
};
} else {
return null;
}
},
// nan
(code, i, source) => {
const prev = source[i - 1];
if (prev && /[a-zA-Z]/.test(prev)) return null;
if (code.substr(0, 3) == 'NaN') {
return {
html: `<span class="nan">NaN</span>`,
next: 3
};
} else {
return null;
}
},
// method
code => {
const match = code.match(/^([a-zA-Z_-]+?)\(/);
if (!match) return null;
if (match[1] == '-') return null;
return {
html: `<span class="method">${match[1]}</span>`,
next: match[1].length
};
},
// property
(code, i, source) => {
const prev = source[i - 1];
if (prev != '.') return null;
const match = code.match(/^[a-zA-Z0-9_-]+/);
if (!match) return null;
return {
html: `<span class="property">${match[0]}</span>`,
next: match[0].length
};
},
// keyword
(code, i, source) => {
const prev = source[i - 1];
if (prev && /[a-zA-Z]/.test(prev)) return null;
const match = keywords.filter(k => code.substr(0, k.length) == k)[0];
if (match) {
if (/^[a-zA-Z]/.test(code.substr(match.length))) return null;
return {
html: `<span class="keyword ${match}">${match}</span>`,
next: match.length
};
} else {
return null;
}
},
// symbol
code => {
const match = symbols.filter(s => code[0] == s)[0];
if (match) {
return {
html: `<span class="symbol">${match}</span>`,
next: 1
};
} else {
return null;
}
}
];
// TODO: specify lang
export default (source: string, lang?: string): string => {
let code = source;
let html = '';
let i = 0;
function push(token: Token) {
html += token.html;
code = code.substr(token.next);
i += token.next;
}
while (code != '') {
const parsed = elements.some(el => {
const e = el(code, i, source);
if (e) {
push(e);
return true;
} else {
return false;
}
});
if (!parsed) {
push({
html: escape(code[0]),
next: 1
});
}
}
return html;
};

View File

@ -1,5 +1,6 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import Note from "../../../models/note"; import Note from "../../../models/note";
import User, { isRemoteUser, isLocalUser } from "../../../models/user";
/** /**
* Get valied note for API processing * Get valied note for API processing
@ -16,3 +17,44 @@ export async function getValiedNote(noteId: mongo.ObjectID) {
return note; return note;
} }
/**
* Get user for API processing
*/
export async function getUser(userId: mongo.ObjectID) {
const user = await User.findOne({
_id: userId
});
if (user == null) {
throw 'user not found';
}
return user;
}
/**
* Get remote user for API processing
*/
export async function getRemoteUser(userId: mongo.ObjectID) {
const user = await getUser(userId);
if (!isRemoteUser(user)) {
throw 'user is not a remote user';
}
return user;
}
/**
* Get local user for API processing
*/
export async function getLocalUser(userId: mongo.ObjectID) {
const user = await getUser(userId);
if (!isLocalUser(user)) {
throw 'user is not a local user';
}
return user;
}

View File

@ -0,0 +1,36 @@
import * as mongo from 'mongodb';
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define';
import { getRemoteUser } from '../../common/getters';
import { updatePerson } from '../../../../remote/activitypub/models/person';
export const meta = {
desc: {
'ja-JP': '指定されたリモートユーザーの情報を更新します。',
'en-US': 'Update specified remote user information.'
},
requireCredential: true,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to update'
}
},
}
};
export default define(meta, (ps) => new Promise((res, rej) => {
updatePersonById(ps.userId).then(() => res(), e => rej(e));
}));
async function updatePersonById(userId: mongo.ObjectID) {
const user = await getRemoteUser(userId);
await updatePerson(user.uri);
}

View File

@ -2,6 +2,7 @@ import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import User from '../../../../models/user'; import User from '../../../../models/user';
import AbuseUserReport from '../../../../models/abuse-user-report'; import AbuseUserReport from '../../../../models/abuse-user-report';
import { publishAdminStream } from '../../../../stream';
export const meta = { export const meta = {
desc: { desc: {
@ -47,12 +48,31 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
return rej('cannot report admin'); return rej('cannot report admin');
} }
await AbuseUserReport.insert({ const report = await AbuseUserReport.insert({
createdAt: new Date(), createdAt: new Date(),
userId: user._id, userId: user._id,
reporterId: me._id, reporterId: me._id,
comment: ps.comment comment: ps.comment
}); });
// Publish event to moderators
setTimeout(async () => {
const moderators = await User.find({
$or: [{
isAdmin: true
}, {
isModerator: true
}]
});
for (const moderator of moderators) {
publishAdminStream(moderator._id, 'newAbuseUserReport', {
id: report._id,
userId: report.userId,
reporterId: report.reporterId,
comment: report.comment
});
}
}, 1);
res(); res();
})); }));

View File

@ -0,0 +1,16 @@
import autobind from 'autobind-decorator';
import Channel from '../channel';
export default class extends Channel {
public readonly chName = 'admin';
public static shouldShare = true;
public static requireCredential = true;
@autobind
public async init(params: any) {
// Subscribe admin stream
this.subscriber.on(`adminStream:${this.user._id}`, data => {
this.send(data);
});
}
}

View File

@ -11,6 +11,7 @@ import messagingIndex from './messaging-index';
import drive from './drive'; import drive from './drive';
import hashtag from './hashtag'; import hashtag from './hashtag';
import apLog from './ap-log'; import apLog from './ap-log';
import admin from './admin';
import gamesReversi from './games/reversi'; import gamesReversi from './games/reversi';
import gamesReversiGame from './games/reversi-game'; import gamesReversiGame from './games/reversi-game';
@ -28,6 +29,7 @@ export default {
drive, drive,
hashtag, hashtag,
apLog, apLog,
admin,
gamesReversi, gamesReversi,
gamesReversiGame gamesReversiGame
}; };

View File

@ -377,10 +377,12 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
if (note.visibility == 'specified') { if (note.visibility == 'specified') {
for (const u of visibleUsers) { for (const u of visibleUsers) {
if (!u._id.equals(user._id)) {
publishHomeTimelineStream(u._id, detailPackedNote); publishHomeTimelineStream(u._id, detailPackedNote);
publishHybridTimelineStream(u._id, detailPackedNote); publishHybridTimelineStream(u._id, detailPackedNote);
} }
} }
}
} else { } else {
// Publish event to myself's stream // Publish event to myself's stream
publishHomeTimelineStream(note.userId, noteObj); publishHomeTimelineStream(note.userId, noteObj);

View File

@ -30,12 +30,25 @@ export default async function(user: IUser, note: INote) {
text: null, text: null,
tags: [], tags: [],
fileIds: [], fileIds: [],
renoteId: null,
poll: null, poll: null,
geo: null, geo: null,
cw: null cw: null
} }
}); });
if (note.renoteId) {
Note.update({ _id: note.renoteId }, {
$inc: {
renoteCount: -1,
score: -1
},
$pull: {
_quoteIds: note._id
}
});
}
publishNoteStream(note._id, 'deleted', { publishNoteStream(note._id, 'deleted', {
deletedAt: deletedAt deletedAt: deletedAt
}); });

View File

@ -87,6 +87,10 @@ class Publisher {
public publishApLogStream = (log: any): void => { public publishApLogStream = (log: any): void => {
this.publish('apLog', null, log); this.publish('apLog', null, log);
} }
public publishAdminStream = (userId: ID, type: string, value?: any): void => {
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
} }
const publisher = new Publisher(); const publisher = new Publisher();
@ -107,3 +111,4 @@ export const publishHybridTimelineStream = publisher.publishHybridTimelineStream
export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream; export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
export const publishHashtagStream = publisher.publishHashtagStream; export const publishHashtagStream = publisher.publishHashtagStream;
export const publishApLogStream = publisher.publishApLogStream; export const publishApLogStream = publisher.publishApLogStream;
export const publishAdminStream = publisher.publishAdminStream;

View File

@ -24,9 +24,7 @@ if (!acct.match(/^\w+@\w/)) {
console.log(`resync ${acct}`); console.log(`resync ${acct}`);
main(acct).then(() => { main(acct).then(() => {
console.log('success'); console.log('Done');
process.exit(0);
}).catch(e => { }).catch(e => {
console.warn(e); console.warn(e);
process.exit(1);
}); });

View File

@ -152,9 +152,19 @@ describe('MFM', () => {
it('can be analyzed', () => { it('can be analyzed', () => {
const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr'); const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
assert.deepStrictEqual(tokens, [ assert.deepStrictEqual(tokens, [
leaf('mention', { acct: '@himawari', canonical: '@himawari', username: 'himawari', host: null }), leaf('mention', {
acct: '@himawari',
canonical: '@himawari',
username: 'himawari',
host: null
}),
text(' '), text(' '),
leaf('mention', { acct: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }), leaf('mention', {
acct: '@hima_sub@namori.net',
canonical: '@hima_sub@namori.net',
username: 'hima_sub',
host: 'namori.net'
}),
text(' お腹ペコい '), text(' お腹ペコい '),
leaf('emoji', { name: 'cat' }), leaf('emoji', { name: 'cat' }),
text(' '), text(' '),
@ -234,6 +244,24 @@ describe('MFM', () => {
]); ]);
}); });
it('flip', () => {
const tokens = analyze('<flip>foo</flip>');
assert.deepStrictEqual(tokens, [
tree('flip', [
text('foo')
], {}),
]);
});
it('spin', () => {
const tokens = analyze('<spin>:foo:</spin>');
assert.deepStrictEqual(tokens, [
tree('spin', [
leaf('emoji', { name: 'foo' })
], {}),
]);
});
describe('motion', () => { describe('motion', () => {
it('by triple brackets', () => { it('by triple brackets', () => {
const tokens = analyze('(((foo)))'); const tokens = analyze('(((foo)))');
@ -280,7 +308,12 @@ describe('MFM', () => {
it('local', () => { it('local', () => {
const tokens = analyze('@himawari foo'); const tokens = analyze('@himawari foo');
assert.deepStrictEqual(tokens, [ assert.deepStrictEqual(tokens, [
leaf('mention', { acct: '@himawari', canonical: '@himawari', username: 'himawari', host: null }), leaf('mention', {
acct: '@himawari',
canonical: '@himawari',
username: 'himawari',
host: null
}),
text(' foo') text(' foo')
]); ]);
}); });
@ -288,7 +321,12 @@ describe('MFM', () => {
it('remote', () => { it('remote', () => {
const tokens = analyze('@hima_sub@namori.net foo'); const tokens = analyze('@hima_sub@namori.net foo');
assert.deepStrictEqual(tokens, [ assert.deepStrictEqual(tokens, [
leaf('mention', { acct: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }), leaf('mention', {
acct: '@hima_sub@namori.net',
canonical: '@hima_sub@namori.net',
username: 'hima_sub',
host: 'namori.net'
}),
text(' foo') text(' foo')
]); ]);
}); });
@ -296,7 +334,12 @@ describe('MFM', () => {
it('remote punycode', () => { it('remote punycode', () => {
const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah foo'); const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah foo');
assert.deepStrictEqual(tokens, [ assert.deepStrictEqual(tokens, [
leaf('mention', { acct: '@hima_sub@xn--q9j5bya.xn--zckzah', canonical: '@hima_sub@なもり.テスト', username: 'hima_sub', host: 'xn--q9j5bya.xn--zckzah' }), leaf('mention', {
acct: '@hima_sub@xn--q9j5bya.xn--zckzah',
canonical: '@hima_sub@なもり.テスト',
username: 'hima_sub',
host: 'xn--q9j5bya.xn--zckzah'
}),
text(' foo') text(' foo')
]); ]);
}); });
@ -309,11 +352,26 @@ describe('MFM', () => {
const tokens2 = analyze('@a\n@b\n@c'); const tokens2 = analyze('@a\n@b\n@c');
assert.deepStrictEqual(tokens2, [ assert.deepStrictEqual(tokens2, [
leaf('mention', { acct: '@a', canonical: '@a', username: 'a', host: null }), leaf('mention', {
acct: '@a',
canonical: '@a',
username: 'a',
host: null
}),
text('\n'), text('\n'),
leaf('mention', { acct: '@b', canonical: '@b', username: 'b', host: null }), leaf('mention', {
acct: '@b',
canonical: '@b',
username: 'b',
host: null
}),
text('\n'), text('\n'),
leaf('mention', { acct: '@c', canonical: '@c', username: 'c', host: null }) leaf('mention', {
acct: '@c',
canonical: '@c',
username: 'c',
host: null
})
]); ]);
const tokens3 = analyze('**x**@a'); const tokens3 = analyze('**x**@a');
@ -321,24 +379,31 @@ describe('MFM', () => {
tree('bold', [ tree('bold', [
text('x') text('x')
], {}), ], {}),
leaf('mention', { acct: '@a', canonical: '@a', username: 'a', host: null }) leaf('mention', {
acct: '@a',
canonical: '@a',
username: 'a',
host: null
})
]); ]);
const tokens4 = analyze('@\n@v\n@veryverylongusername' /* \n@toolongtobeasamention */); const tokens4 = analyze('@\n@v\n@veryverylongusername');
assert.deepStrictEqual(tokens4, [ assert.deepStrictEqual(tokens4, [
text('@\n'), text('@\n'),
leaf('mention', { acct: '@v', canonical: '@v', username: 'v', host: null }), leaf('mention', {
acct: '@v',
canonical: '@v',
username: 'v',
host: null
}),
text('\n'), text('\n'),
leaf('mention', { acct: '@veryverylongusername', canonical: '@veryverylongusername', username: 'veryverylongusername', host: null }), leaf('mention', {
// text('\n@toolongtobeasamention') acct: '@veryverylongusername',
canonical: '@veryverylongusername',
username: 'veryverylongusername',
host: null
}),
]); ]);
/*
const tokens5 = analyze('@domain_is@valid.example.com\n@domain_is@.invalid\n@domain_is@invali.d\n@domain_is@invali.d\n@domain_is@-invalid.com\n@domain_is@invalid-.com');
assert.deepStrictEqual([
leaf('mention', { acct: '@domain_is@valid.example.com', canonical: '@domain_is@valid.example.com', username: 'domain_is', host: 'valid.example.com' }),
text('\n@domain_is@.invalid\n@domain_is@invali.d\n@domain_is@invali.d\n@domain_is@-invalid.com\n@domain_is@invalid-.com')
], tokens5);
*/
}); });
}); });
@ -905,6 +970,20 @@ describe('MFM', () => {
text('after') text('after')
]); ]);
}); });
it('ignore multiple title blocks', () => {
const tokens = analyze('【foo】bar【baz】');
assert.deepStrictEqual(tokens, [
text('【foo】bar【baz】')
]);
});
it('disallow linebreak in title', () => {
const tokens = analyze('【foo\nbar】');
assert.deepStrictEqual(tokens, [
text('【foo\nbar】')
]);
});
}); });
describe('center', () => { describe('center', () => {