Merge branch 'develop' into math-block
This commit is contained in:
commit
1af1638e2b
48 changed files with 791 additions and 852 deletions
|
@ -5,12 +5,20 @@ unreleased
|
|||
----------
|
||||
* 返信するときにCWを維持するかどうか設定できるように
|
||||
* 外部サービス認証情報の配信
|
||||
* 管理画面のモデレーションのUIを強化
|
||||
* 管理画面からリモートユーザーの情報を更新できるように
|
||||
* 回転構文の追加
|
||||
* 左右反転構文の追加
|
||||
* シンタックスハイライトの強化
|
||||
* 引用投稿を削除したとき単なるRenoteとしてタイムラインに残る問題を修正
|
||||
* イタリック構文の判定の改善
|
||||
* タイトル構文の判定の改善
|
||||
* テーマが反映されないことがある問題を修正
|
||||
* ホームにフォロワー限定投稿が表示されない問題を修正
|
||||
* 返信一覧を取得すると非公開投稿も取得されてしまう問題を修正
|
||||
* メンション一覧を取得すると非公開投稿も取得されてしまう問題を修正
|
||||
* 通知に非公開投稿が表示される問題を修正
|
||||
* ダイレクトで投稿すると100%の確率で表示が二重になる問題を修正
|
||||
* ウィジットの投稿フォームで投稿するとデフォルトの公開範囲が適用されない問題を修正
|
||||
|
||||
10.78.5
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
|
|
@ -807,8 +807,8 @@ desktop/views/components/settings.vue:
|
|||
timeline: "Timeline"
|
||||
show-my-renotes: "Show my renotes in the timeline"
|
||||
show-renoted-my-notes: "Show renoted posts of mine in timelines"
|
||||
show-local-renotes: "Show renoted local posts in timelines"
|
||||
show-maps: "Display a map to show the location"
|
||||
show-local-renotes: "Show renoted local posts in the timelines"
|
||||
show-maps: "Display a map to show location"
|
||||
remain-deleted-note: "Continue to show deleted posts"
|
||||
deck-column-align: "Deck column alignment"
|
||||
deck-column-align-center: "Center"
|
||||
|
@ -1135,15 +1135,22 @@ admin/views/users.vue:
|
|||
user-not-found: "User not found"
|
||||
lookup: "Look up"
|
||||
reset-password: "Reset password"
|
||||
reset-password-confirm: "Do you want to reset your password?"
|
||||
password-updated: "The password is now \"{password}\""
|
||||
suspend: "Suspend"
|
||||
suspend-confirm: "Do you want to suspend this account?"
|
||||
suspended: "Successfully suspended."
|
||||
unsuspend: "Unsuspend"
|
||||
unsuspend-confirm: "Are you sure you want to unsuspend this account?"
|
||||
unsuspended: "The user has successfully unsuspended."
|
||||
verify: "Verify account"
|
||||
verify-confirm: "Do you want this to be a verified account?"
|
||||
verified: "The account is now being verified"
|
||||
unverify: "Unverify account"
|
||||
unverify-confirm: "Do you want to remove the 'verified account' designation?"
|
||||
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:
|
||||
title: "Users"
|
||||
sort:
|
||||
|
|
|
@ -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."
|
||||
features: "Características"
|
||||
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-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
|
||||
ui: "Interfaz"
|
||||
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
|
|
@ -788,7 +788,7 @@ desktop/views/components/settings.vue:
|
|||
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
|
||||
deck-nav: "Deck sans tansitions"
|
||||
deck-nav-desc: "Vous obtenez une colonne temporaire sans transitions dans la page pendant la navigation, lors de l’utilisation du Deck."
|
||||
keep-cw: "CW保持"
|
||||
keep-cw: "Maintenir l'avertissement de contenu"
|
||||
keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。"
|
||||
deck-default: "Utiliser le Deck comme IU par défaut"
|
||||
display: "Affichage et design"
|
||||
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "Utilisateur non trouvé"
|
||||
lookup: "Recherche"
|
||||
reset-password: "Réinitialiser mot de passe"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "Le mot de passe est « {password} »"
|
||||
suspend: "Suspendre"
|
||||
suspend-confirm: "Désirez-vous suspendre ce compte ?"
|
||||
suspended: "Suspendu avec succès."
|
||||
unsuspend: "Suspension levée"
|
||||
unsuspend-confirm: "Souhaiteriez-vous ne plus suspendre ce compte ?"
|
||||
unsuspended: "La suspension de l’utilisateur a été levée avec succès"
|
||||
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é"
|
||||
unverify: "Enlever la vérification du compte"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "Ce compte n'est plus vérifié"
|
||||
update-remote-user: "Mettre à jour les informations de l’utilisateur·rice distant·e"
|
||||
remote-user-updated: "Les informations de l’utilisateur·rice distant·e ont étés mis à jour"
|
||||
users:
|
||||
title: "Utilisateurs"
|
||||
sort:
|
||||
|
@ -1469,7 +1476,7 @@ mobile/views/pages/settings.vue:
|
|||
notification-position-top: "en haut"
|
||||
behavior: "Comportement"
|
||||
fetch-on-scroll: "Chargement lors du défilement"
|
||||
keep-cw: "CW保持"
|
||||
keep-cw: "Garder l'avertissement de contenu"
|
||||
note-visibility: "Visibilité de la publication"
|
||||
default-note-visibility: "Visibilité par défaut"
|
||||
remember-note-visibility: "Se souvenir du mode de visibilité de la publication"
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
|
|
@ -730,10 +730,6 @@ desktop/views/components/drive.vue:
|
|||
upload: "ファイルをアップロード"
|
||||
url-upload: "URLからアップロード"
|
||||
|
||||
desktop/views/components/media-image.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
|
||||
desktop/views/components/media-video.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
|
@ -980,6 +976,10 @@ desktop/views/components/settings.2fa.vue:
|
|||
failed: "設定に失敗しました。トークンに誤りがないかご確認ください。"
|
||||
info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。"
|
||||
|
||||
common/views/components/media-image.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
|
||||
common/views/components/api-settings.vue:
|
||||
intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。"
|
||||
caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
|
||||
|
@ -1266,15 +1266,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
@ -1486,10 +1493,6 @@ mobile/views/components/drive.file-detail.vue:
|
|||
mark-as-sensitive: "閲覧注意に設定"
|
||||
unmark-as-sensitive: "閲覧注意を解除"
|
||||
|
||||
mobile/views/components/media-image.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
|
||||
mobile/views/components/media-video.vue:
|
||||
sensitive: "閲覧注意"
|
||||
click-to-show: "クリックして表示"
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つからへん!"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password} 」やで"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "사용자를 찾을 수 없습니다"
|
||||
lookup: "조회"
|
||||
reset-password: "암호 재설정"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "암호는 현재 \"{password}\" 입니다"
|
||||
suspend: "정지"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "정지하였습니다"
|
||||
unsuspend: "정지 해제"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "정지를 해제하였습니다"
|
||||
verify: "공식 계정으로 설정"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "공식 계정으로 설정하였습니다"
|
||||
unverify: "공식 계정 해제"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "공식 계정을 해제하였습니다"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "사용자"
|
||||
sort:
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "Nie znaleziono użytkownika"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "Użytkownicy"
|
||||
sort:
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
reset-password: "パスワードをリセット"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "パスワードは現在「{password}」です"
|
||||
suspend: "凍結"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "凍結しました"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
unverify: "公式アカウントを解除する"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "公式アカウントを解除しました"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "ユーザー"
|
||||
sort:
|
||||
|
|
|
@ -1134,15 +1134,22 @@ admin/views/users.vue:
|
|||
user-not-found: "用户不存在"
|
||||
lookup: "订阅"
|
||||
reset-password: "密码重置"
|
||||
reset-password-confirm: "パスワードをリセットしますか?"
|
||||
password-updated: "密码为「{password}」"
|
||||
suspend: "被冻结"
|
||||
suspend-confirm: "凍結しますか?"
|
||||
suspended: "成功冻结用户"
|
||||
unsuspend: "已解除冻结"
|
||||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "已成功解除用户冻结"
|
||||
verify: "认证用户"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "此账户已被认证"
|
||||
unverify: "解除账户认证"
|
||||
unverify-confirm: "公式アカウントを解除しますか?"
|
||||
unverified: "该帐户未经认证"
|
||||
update-remote-user: "リモートユーザー情報の更新"
|
||||
remote-user-updated: "リモートユーザー情報を更新しました"
|
||||
users:
|
||||
title: "用户"
|
||||
sort:
|
||||
|
|
|
@ -97,8 +97,8 @@
|
|||
"bootstrap-vue": "2.0.0-rc.11",
|
||||
"cafy": "12.0.0",
|
||||
"chai": "4.2.0",
|
||||
"chalk": "2.4.2",
|
||||
"chai-http": "4.2.1",
|
||||
"chalk": "2.4.2",
|
||||
"commander": "2.19.0",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "1.0.1",
|
||||
|
@ -178,13 +178,14 @@
|
|||
"parsimmon": "1.12.0",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"prismjs": "1.15.0",
|
||||
"progress-bar-webpack-plugin": "1.12.0",
|
||||
"promise-any": "0.2.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-sequential": "1.1.1",
|
||||
"pug": "2.0.3",
|
||||
"punycode": "2.1.1",
|
||||
"qrcode": "1.3.2",
|
||||
"qrcode": "1.3.3",
|
||||
"randomcolor": "0.5.3",
|
||||
"ratelimiter": "3.2.0",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
|
@ -230,6 +231,7 @@
|
|||
"vue-js-modal": "1.3.28",
|
||||
"vue-loader": "15.5.1",
|
||||
"vue-marquee-text-component": "1.1.1",
|
||||
"vue-prism-component": "1.1.1",
|
||||
"vue-router": "3.0.2",
|
||||
"vue-sequential-entrance": "1.1.3",
|
||||
"vue-style-loader": "4.1.2",
|
||||
|
|
82
src/client/app/admin/views/users.user.vue
Normal file
82
src/client/app/admin/views/users.user.vue
Normal 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>
|
|
@ -3,20 +3,27 @@
|
|||
<ui-card>
|
||||
<div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div>
|
||||
<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>
|
||||
</ui-input>
|
||||
<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
|
||||
<ui-horizon-group>
|
||||
<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
|
||||
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
|
||||
</ui-horizon-group>
|
||||
<ui-horizon-group>
|
||||
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
|
||||
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
|
||||
</ui-horizon-group>
|
||||
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
|
||||
<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
|
||||
|
||||
<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-horizon-group>
|
||||
<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
|
||||
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
|
||||
</ui-horizon-group>
|
||||
<ui-horizon-group>
|
||||
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
|
||||
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
|
||||
</ui-horizon-group>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
|
@ -47,29 +54,7 @@
|
|||
</ui-select>
|
||||
</ui-horizon-group>
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<div class="kofvwchc" v-for="user in users" :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>
|
||||
<x-user v-for="user in users" :user='user' :key="user.id"/>
|
||||
</sequential-entrance>
|
||||
<ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button>
|
||||
</section>
|
||||
|
@ -81,12 +66,15 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
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 XUser from './users.user.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('admin/views/users.vue'),
|
||||
|
||||
components: {
|
||||
XUser
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
|
@ -102,7 +90,7 @@ export default Vue.extend({
|
|||
offset: 0,
|
||||
users: [],
|
||||
existMore: false,
|
||||
faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey
|
||||
faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey, faSync
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -131,6 +119,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
methods: {
|
||||
/** テキストエリアのユーザーを解決する */
|
||||
async fetchUser() {
|
||||
try {
|
||||
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() {
|
||||
this.user = null;
|
||||
const user = await this.fetchUser();
|
||||
this.$root.api('admin/show-user', { userId: user.id }).then(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() {
|
||||
const user = await this.fetchUser();
|
||||
this.$root.api('admin/reset-password', { userId: user.id }).then(res => {
|
||||
if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return;
|
||||
|
||||
this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('password-updated', { password: res.password })
|
||||
|
@ -167,11 +167,12 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
async verifyUser() {
|
||||
if (!await this.getConfirmed(this.$t('verify-confirm'))) return;
|
||||
|
||||
this.verifying = true;
|
||||
|
||||
const process = async () => {
|
||||
const user = await this.fetchUser();
|
||||
await this.$root.api('admin/verify-user', { userId: user.id });
|
||||
await this.$root.api('admin/verify-user', { userId: this.user._id });
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('verified')
|
||||
|
@ -186,14 +187,17 @@ export default Vue.extend({
|
|||
});
|
||||
|
||||
this.verifying = false;
|
||||
|
||||
this.refreshUser();
|
||||
},
|
||||
|
||||
async unverifyUser() {
|
||||
if (!await this.getConfirmed(this.$t('unverify-confirm'))) return;
|
||||
|
||||
this.unverifying = true;
|
||||
|
||||
const process = async () => {
|
||||
const user = await this.fetchUser();
|
||||
await this.$root.api('admin/unverify-user', { userId: user.id });
|
||||
await this.$root.api('admin/unverify-user', { userId: this.user._id });
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('unverified')
|
||||
|
@ -208,14 +212,17 @@ export default Vue.extend({
|
|||
});
|
||||
|
||||
this.unverifying = false;
|
||||
|
||||
this.refreshUser();
|
||||
},
|
||||
|
||||
async suspendUser() {
|
||||
if (!await this.getConfirmed(this.$t('suspend-confirm'))) return;
|
||||
|
||||
this.suspending = true;
|
||||
|
||||
const process = async () => {
|
||||
const user = await this.fetchUser();
|
||||
await this.$root.api('admin/suspend-user', { userId: user.id });
|
||||
await this.$root.api('admin/suspend-user', { userId: this.user._id });
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('suspended')
|
||||
|
@ -230,14 +237,17 @@ export default Vue.extend({
|
|||
});
|
||||
|
||||
this.suspending = false;
|
||||
|
||||
this.refreshUser();
|
||||
},
|
||||
|
||||
async unsuspendUser() {
|
||||
if (!await this.getConfirmed(this.$t('unsuspend-confirm'))) return;
|
||||
|
||||
this.unsuspending = true;
|
||||
|
||||
const process = async () => {
|
||||
const user = await this.fetchUser();
|
||||
await this.$root.api('admin/unsuspend-user', { userId: user.id });
|
||||
await this.$root.api('admin/unsuspend-user', { userId: this.user._id });
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('unsuspended')
|
||||
|
@ -252,8 +262,32 @@ export default Vue.extend({
|
|||
});
|
||||
|
||||
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() {
|
||||
this.$root.api('admin/show-users', {
|
||||
state: this.state,
|
||||
|
@ -277,42 +311,12 @@ export default Vue.extend({
|
|||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.kofvwchc
|
||||
display flex
|
||||
padding 16px 0
|
||||
border-top solid 1px var(--faceDivider)
|
||||
.target
|
||||
margin-bottom 16px !important
|
||||
|
||||
> div:first-child
|
||||
> a
|
||||
> .avatar
|
||||
width 64px
|
||||
height 64px
|
||||
.user
|
||||
margin-top 32px
|
||||
|
||||
> 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
|
||||
> .actions
|
||||
margin-left 80px
|
||||
</style>
|
||||
|
|
|
@ -26,3 +26,8 @@
|
|||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
|
|
@ -72,47 +72,6 @@ body
|
|||
code
|
||||
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
|
||||
display block
|
||||
|
||||
|
|
|
@ -133,6 +133,7 @@ export default prop => ({
|
|||
|
||||
case 'deleted': {
|
||||
Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt);
|
||||
Vue.set(this.$_ns_target, 'renote', null);
|
||||
this.$_ns_target.text = null;
|
||||
this.$_ns_target.tags = [];
|
||||
this.$_ns_target.fileIds = [];
|
||||
|
|
30
src/client/app/common/views/components/code-core.vue
Normal file
30
src/client/app/common/views/components/code-core.vue
Normal 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>
|
28
src/client/app/common/views/components/code.vue
Normal file
28
src/client/app/common/views/components/code.vue
Normal 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>
|
|
@ -5,16 +5,21 @@
|
|||
<span>{{ $t('click-to-show') }}</span>
|
||||
</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>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import ImageViewer from '../../../common/views/components/image-viewer.vue';
|
||||
import ImageViewer from './image-viewer.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/components/media-image.vue'),
|
||||
i18n: i18n('common/views/components/media-image.vue'),
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
|
@ -58,6 +63,7 @@ export default Vue.extend({
|
|||
<style lang="stylus" scoped>
|
||||
.gqnyydlzavusgskkfvwvjiattxdzsqlf
|
||||
display block
|
||||
cursor zoom-in
|
||||
overflow hidden
|
||||
width 100%
|
||||
height 100%
|
|
@ -7,7 +7,7 @@
|
|||
<div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
|
||||
<template v-for="media in mediaList">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,10 +17,12 @@
|
|||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XBanner from './media-banner.vue';
|
||||
import XImage from './media-image.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XBanner
|
||||
XBanner,
|
||||
XImage
|
||||
},
|
||||
props: {
|
||||
mediaList: {
|
||||
|
|
|
@ -6,8 +6,8 @@ import MkUrl from './url.vue';
|
|||
import MkMention from './mention.vue';
|
||||
import { concat, sum } from '../../../../../prelude/array';
|
||||
import MkFormula from './formula.vue';
|
||||
import MkCode from './code.vue';
|
||||
import MkGoogle from './google.vue';
|
||||
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
|
||||
import { host } from '../../../config';
|
||||
import { preorderF, countNodesF } from '../../../../../prelude/tree';
|
||||
|
||||
|
@ -124,6 +124,25 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||
}, 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': {
|
||||
return [createElement(MkUrl, {
|
||||
key: Math.random(),
|
||||
|
@ -170,21 +189,22 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||
}
|
||||
|
||||
case 'blockCode': {
|
||||
return [createElement('pre', {
|
||||
class: 'code'
|
||||
}, [
|
||||
createElement('code', {
|
||||
domProps: {
|
||||
innerHTML: syntaxHighlight(token.node.props.code)
|
||||
}
|
||||
})
|
||||
])];
|
||||
return [createElement(MkCode, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
code: token.node.props.code,
|
||||
lang: token.node.props.lang,
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
case 'inlineCode': {
|
||||
return [createElement('code', {
|
||||
domProps: {
|
||||
innerHTML: syntaxHighlight(token.node.props.code)
|
||||
return [createElement(MkCode, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
code: token.node.props.code,
|
||||
lang: token.node.props.lang,
|
||||
inline: true
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
|
|
@ -24,34 +24,10 @@ export default Vue.extend({
|
|||
background var(--mfmTitleBg)
|
||||
border-radius 4px
|
||||
|
||||
>>> .code
|
||||
margin 8px 0
|
||||
|
||||
>>> .quote
|
||||
margin 8px
|
||||
padding 6px 0 6px 12px
|
||||
color var(--mfmQuote)
|
||||
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>
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
<template>
|
||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
|
||||
<span slot="header">
|
||||
<span :class="$style.title">{{ $t('choose-prompt') }}</span>
|
||||
<span :class="$style.count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span>
|
||||
<span slot="header" class="jqiaciqv">
|
||||
<span class="title">{{ $t('choose-prompt') }}</span>
|
||||
<span class="count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span>
|
||||
</span>
|
||||
|
||||
<x-drive
|
||||
ref="browser"
|
||||
:class="$style.browser"
|
||||
:multiple="multiple"
|
||||
@selected="onSelected"
|
||||
@change-selection="onChangeSelection"
|
||||
/>
|
||||
<div :class="$style.footer">
|
||||
<button :class="$style.upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button>
|
||||
<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
|
||||
<button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</button>
|
||||
<div class="rqsvbumu">
|
||||
<x-drive
|
||||
ref="browser"
|
||||
class="browser"
|
||||
:multiple="multiple"
|
||||
@selected="onSelected"
|
||||
@change-selection="onChangeSelection"
|
||||
/>
|
||||
<div class="footer">
|
||||
<button class="upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button>
|
||||
<ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button>
|
||||
<ui-button inline primary :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</mk-window>
|
||||
</template>
|
||||
|
@ -60,120 +62,67 @@ export default Vue.extend({
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" module>
|
||||
.title
|
||||
> [data-icon]
|
||||
margin-right 4px
|
||||
<style lang="stylus" scoped>
|
||||
.jqiaciqv
|
||||
.title
|
||||
> [data-icon]
|
||||
margin-right 4px
|
||||
|
||||
.count
|
||||
margin-left 8px
|
||||
opacity 0.7
|
||||
|
||||
.browser
|
||||
height calc(100% - 72px)
|
||||
|
||||
.footer
|
||||
height 72px
|
||||
background var(--primaryLighten95)
|
||||
|
||||
.upload
|
||||
display inline-block
|
||||
position absolute
|
||||
top 8px
|
||||
left 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 8px 4px 0 0
|
||||
width 40px
|
||||
height 40px
|
||||
font-size 1em
|
||||
color var(--primaryAlpha05)
|
||||
background transparent
|
||||
outline none
|
||||
border solid 1px transparent
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
background transparent
|
||||
border-color var(--primaryAlpha03)
|
||||
|
||||
&:active
|
||||
color var(--primaryAlpha06)
|
||||
background transparent
|
||||
border-color var(--primaryAlpha05)
|
||||
//box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid var(--primaryAlpha03)
|
||||
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
|
||||
.count
|
||||
margin-left 8px
|
||||
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)
|
||||
.rqsvbumu
|
||||
display flex
|
||||
flex-direction column
|
||||
height 100%
|
||||
|
||||
&:not(:disabled)
|
||||
font-weight bold
|
||||
.browser
|
||||
flex 1
|
||||
overflow auto
|
||||
|
||||
&:hover:not(:disabled)
|
||||
background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
|
||||
border-color var(--primary)
|
||||
.footer
|
||||
padding 16px
|
||||
background var(--desktopPostFormBg)
|
||||
text-align right
|
||||
|
||||
&:active:not(:disabled)
|
||||
background var(--primary)
|
||||
border-color var(--primary)
|
||||
.upload
|
||||
display inline-block
|
||||
position absolute
|
||||
top 8px
|
||||
left 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 8px 4px 0 0
|
||||
width 40px
|
||||
height 40px
|
||||
font-size 1em
|
||||
color var(--primaryAlpha05)
|
||||
background transparent
|
||||
outline none
|
||||
border solid 1px transparent
|
||||
border-radius 4px
|
||||
|
||||
.cancel
|
||||
right 148px
|
||||
color #888
|
||||
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
|
||||
border solid 1px #e2e2e2
|
||||
&:hover
|
||||
background transparent
|
||||
border-color var(--primaryAlpha03)
|
||||
|
||||
&:hover
|
||||
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
|
||||
border-color #dcdcdc
|
||||
&:active
|
||||
color var(--primaryAlpha06)
|
||||
background transparent
|
||||
border-color var(--primaryAlpha05)
|
||||
//box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset
|
||||
|
||||
&:active
|
||||
background #ececec
|
||||
border-color #dcdcdc
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid var(--primaryAlpha03)
|
||||
border-radius 8px
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<template>
|
||||
<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
|
||||
<span slot="header">
|
||||
<span :class="$style.title">{{ $t('choose-prompt') }}</span>
|
||||
<span>{{ $t('choose-prompt') }}</span>
|
||||
</span>
|
||||
|
||||
<x-drive
|
||||
ref="browser"
|
||||
:class="$style.browser"
|
||||
:multiple="false"
|
||||
/>
|
||||
<div :class="$style.footer">
|
||||
<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
|
||||
<button :class="$style.ok" @click="ok">{{ $t('ok') }}</button>
|
||||
<div class="hllkpxxu">
|
||||
<x-drive
|
||||
ref="browser"
|
||||
class="browser"
|
||||
:multiple="false"
|
||||
/>
|
||||
<div class="footer">
|
||||
<ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button>
|
||||
<ui-button inline @click="ok" primary>{{ $t('ok') }}</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</mk-window>
|
||||
</template>
|
||||
|
@ -36,79 +38,19 @@ export default Vue.extend({
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" module>
|
||||
<style lang="stylus" scoped>
|
||||
.hllkpxxu
|
||||
display flex
|
||||
flex-direction column
|
||||
height 100%
|
||||
|
||||
.browser
|
||||
flex 1
|
||||
overflow auto
|
||||
|
||||
.title
|
||||
> [data-icon]
|
||||
margin-right 4px
|
||||
|
||||
.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
|
||||
.footer
|
||||
padding 16px
|
||||
background var(--desktopPostFormBg)
|
||||
text-align right
|
||||
|
||||
</style>
|
||||
|
|
|
@ -9,7 +9,6 @@ import subNoteContent from './sub-note-content.vue';
|
|||
import window from './window.vue';
|
||||
import noteFormWindow from './post-form-window.vue';
|
||||
import renoteFormWindow from './renote-form-window.vue';
|
||||
import mediaImage from './media-image.vue';
|
||||
import mediaVideo from './media-video.vue';
|
||||
import notifications from './notifications.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-post-form-window', noteFormWindow);
|
||||
Vue.component('mk-renote-form-window', renoteFormWindow);
|
||||
Vue.component('mk-media-image', mediaImage);
|
||||
Vue.component('mk-media-video', mediaVideo);
|
||||
Vue.component('mk-notifications', notifications);
|
||||
Vue.component('mk-post-form', noteForm);
|
||||
|
|
|
@ -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>
|
|
@ -3,7 +3,6 @@ import Vue from 'vue';
|
|||
import ui from './ui.vue';
|
||||
import note from './note.vue';
|
||||
import notes from './notes.vue';
|
||||
import mediaImage from './media-image.vue';
|
||||
import mediaVideo from './media-video.vue';
|
||||
import notePreview from './note-preview.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-note', note);
|
||||
Vue.component('mk-notes', notes);
|
||||
Vue.component('mk-media-image', mediaImage);
|
||||
Vue.component('mk-media-video', mediaVideo);
|
||||
Vue.component('mk-note-preview', notePreview);
|
||||
Vue.component('mk-sub-note-content', subNoteContent);
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
html
|
||||
--primary #fb4e4e
|
||||
--link #fb4e4e
|
||||
--linkTapHighlight #fb4e4eb3
|
||||
|
||||
body
|
||||
margin 0
|
||||
|
|
|
@ -100,20 +100,6 @@ export default class Reversi {
|
|||
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[] {
|
||||
const x = pos % this.mapWidth;
|
||||
const y = Math.floor(pos / this.mapWidth);
|
||||
|
|
|
@ -55,6 +55,18 @@ export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteU
|
|||
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) {
|
||||
const pre = doc.createElement('pre');
|
||||
const inner = doc.createElement('code');
|
||||
|
|
|
@ -91,6 +91,7 @@ const mfm = P.createLanguage({
|
|||
root: r => P.alt(
|
||||
r.big,
|
||||
r.small,
|
||||
r.spin,
|
||||
r.bold,
|
||||
r.strike,
|
||||
r.italic,
|
||||
|
@ -101,6 +102,7 @@ const mfm = P.createLanguage({
|
|||
r.hashtag,
|
||||
r.emoji,
|
||||
r.blockCode,
|
||||
r.flip,
|
||||
r.inlineCode,
|
||||
r.quote,
|
||||
r.mathInline,
|
||||
|
@ -123,6 +125,7 @@ const mfm = P.createLanguage({
|
|||
r.hashtag,
|
||||
r.emoji,
|
||||
r.mathInline,
|
||||
r.spin,
|
||||
r.text
|
||||
).atLeast(1).tryParse(x), {})),
|
||||
//#endregion
|
||||
|
@ -141,6 +144,15 @@ const mfm = P.createLanguage({
|
|||
).atLeast(1).tryParse(x), {})),
|
||||
//#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
|
||||
blockCode: r =>
|
||||
newline.then(
|
||||
|
@ -163,6 +175,7 @@ const mfm = P.createLanguage({
|
|||
r.hashtag,
|
||||
r.url,
|
||||
r.link,
|
||||
r.flip,
|
||||
r.emoji,
|
||||
r.text
|
||||
).atLeast(1).tryParse(x), {})),
|
||||
|
@ -174,6 +187,7 @@ const mfm = P.createLanguage({
|
|||
.map(x => createTree('center', P.alt(
|
||||
r.big,
|
||||
r.small,
|
||||
r.spin,
|
||||
r.bold,
|
||||
r.strike,
|
||||
r.italic,
|
||||
|
@ -184,6 +198,7 @@ const mfm = P.createLanguage({
|
|||
r.mathInline,
|
||||
r.url,
|
||||
r.link,
|
||||
r.flip,
|
||||
r.text
|
||||
).atLeast(1).tryParse(x), {})),
|
||||
//#endregion
|
||||
|
@ -217,6 +232,23 @@ const mfm = P.createLanguage({
|
|||
}),
|
||||
//#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
|
||||
inlineCode: r =>
|
||||
P.regexp(/`([^´\n]+?)`/, 1)
|
||||
|
@ -242,6 +274,7 @@ const mfm = P.createLanguage({
|
|||
r.hashtag,
|
||||
r.url,
|
||||
r.link,
|
||||
r.flip,
|
||||
r.emoji,
|
||||
r.text
|
||||
).atLeast(1).tryParse(x), {})),
|
||||
|
@ -262,6 +295,7 @@ const mfm = P.createLanguage({
|
|||
return createTree('link', P.alt(
|
||||
r.big,
|
||||
r.small,
|
||||
r.spin,
|
||||
r.bold,
|
||||
r.strike,
|
||||
r.italic,
|
||||
|
@ -311,6 +345,7 @@ const mfm = P.createLanguage({
|
|||
.map(x => createTree('motion', P.alt(
|
||||
r.bold,
|
||||
r.small,
|
||||
r.spin,
|
||||
r.strike,
|
||||
r.italic,
|
||||
r.mention,
|
||||
|
@ -318,6 +353,7 @@ const mfm = P.createLanguage({
|
|||
r.emoji,
|
||||
r.url,
|
||||
r.link,
|
||||
r.flip,
|
||||
r.mathInline,
|
||||
r.text
|
||||
).atLeast(1).tryParse(x), {})),
|
||||
|
@ -356,6 +392,7 @@ const mfm = P.createLanguage({
|
|||
r.hashtag,
|
||||
r.url,
|
||||
r.link,
|
||||
r.flip,
|
||||
r.emoji,
|
||||
r.text
|
||||
).atLeast(1).tryParse(x), {})),
|
||||
|
@ -365,18 +402,20 @@ const mfm = P.createLanguage({
|
|||
title: r =>
|
||||
newline.then(P((input, 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');
|
||||
const q = match[1].trim().substring(1, match[1].length - 1);
|
||||
const q = match[2].trim();
|
||||
const contents = P.alt(
|
||||
r.big,
|
||||
r.small,
|
||||
r.spin,
|
||||
r.bold,
|
||||
r.strike,
|
||||
r.italic,
|
||||
r.motion,
|
||||
r.url,
|
||||
r.link,
|
||||
r.flip,
|
||||
r.mention,
|
||||
r.hashtag,
|
||||
r.emoji,
|
||||
|
|
|
@ -1,343 +0,0 @@
|
|||
import { capitalize, toUpperCase } from '../prelude/string';
|
||||
|
||||
function escape(text: string) {
|
||||
return text
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<');
|
||||
}
|
||||
|
||||
// 文字数が多い順にソートします
|
||||
// そうしないと、「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;
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import * as mongo from 'mongodb';
|
||||
import Note from "../../../models/note";
|
||||
import User, { isRemoteUser, isLocalUser } from "../../../models/user";
|
||||
|
||||
/**
|
||||
* Get valied note for API processing
|
||||
|
@ -16,3 +17,44 @@ export async function getValiedNote(noteId: mongo.ObjectID) {
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
36
src/server/api/endpoints/admin/update-remote-user.ts
Normal file
36
src/server/api/endpoints/admin/update-remote-user.ts
Normal 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);
|
||||
}
|
|
@ -2,6 +2,7 @@ import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
|
|||
import define from '../../define';
|
||||
import User from '../../../../models/user';
|
||||
import AbuseUserReport from '../../../../models/abuse-user-report';
|
||||
import { publishAdminStream } from '../../../../stream';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -47,12 +48,31 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||
return rej('cannot report admin');
|
||||
}
|
||||
|
||||
await AbuseUserReport.insert({
|
||||
const report = await AbuseUserReport.insert({
|
||||
createdAt: new Date(),
|
||||
userId: user._id,
|
||||
reporterId: me._id,
|
||||
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();
|
||||
}));
|
||||
|
|
16
src/server/api/stream/channels/admin.ts
Normal file
16
src/server/api/stream/channels/admin.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import messagingIndex from './messaging-index';
|
|||
import drive from './drive';
|
||||
import hashtag from './hashtag';
|
||||
import apLog from './ap-log';
|
||||
import admin from './admin';
|
||||
import gamesReversi from './games/reversi';
|
||||
import gamesReversiGame from './games/reversi-game';
|
||||
|
||||
|
@ -28,6 +29,7 @@ export default {
|
|||
drive,
|
||||
hashtag,
|
||||
apLog,
|
||||
admin,
|
||||
gamesReversi,
|
||||
gamesReversiGame
|
||||
};
|
||||
|
|
|
@ -377,8 +377,10 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
|
|||
|
||||
if (note.visibility == 'specified') {
|
||||
for (const u of visibleUsers) {
|
||||
publishHomeTimelineStream(u._id, detailPackedNote);
|
||||
publishHybridTimelineStream(u._id, detailPackedNote);
|
||||
if (!u._id.equals(user._id)) {
|
||||
publishHomeTimelineStream(u._id, detailPackedNote);
|
||||
publishHybridTimelineStream(u._id, detailPackedNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -30,12 +30,25 @@ export default async function(user: IUser, note: INote) {
|
|||
text: null,
|
||||
tags: [],
|
||||
fileIds: [],
|
||||
renoteId: null,
|
||||
poll: null,
|
||||
geo: null,
|
||||
cw: null
|
||||
}
|
||||
});
|
||||
|
||||
if (note.renoteId) {
|
||||
Note.update({ _id: note.renoteId }, {
|
||||
$inc: {
|
||||
renoteCount: -1,
|
||||
score: -1
|
||||
},
|
||||
$pull: {
|
||||
_quoteIds: note._id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
publishNoteStream(note._id, 'deleted', {
|
||||
deletedAt: deletedAt
|
||||
});
|
||||
|
|
|
@ -87,6 +87,10 @@ class Publisher {
|
|||
public publishApLogStream = (log: any): void => {
|
||||
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();
|
||||
|
@ -107,3 +111,4 @@ export const publishHybridTimelineStream = publisher.publishHybridTimelineStream
|
|||
export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
|
||||
export const publishHashtagStream = publisher.publishHashtagStream;
|
||||
export const publishApLogStream = publisher.publishApLogStream;
|
||||
export const publishAdminStream = publisher.publishAdminStream;
|
||||
|
|
|
@ -24,9 +24,7 @@ if (!acct.match(/^\w+@\w/)) {
|
|||
console.log(`resync ${acct}`);
|
||||
|
||||
main(acct).then(() => {
|
||||
console.log('success');
|
||||
process.exit(0);
|
||||
console.log('Done');
|
||||
}).catch(e => {
|
||||
console.warn(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
119
test/mfm.ts
119
test/mfm.ts
|
@ -152,9 +152,19 @@ describe('MFM', () => {
|
|||
it('can be analyzed', () => {
|
||||
const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
|
||||
assert.deepStrictEqual(tokens, [
|
||||
leaf('mention', { acct: '@himawari', canonical: '@himawari', username: 'himawari', host: null }),
|
||||
leaf('mention', {
|
||||
acct: '@himawari',
|
||||
canonical: '@himawari',
|
||||
username: 'himawari',
|
||||
host: null
|
||||
}),
|
||||
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(' お腹ペコい '),
|
||||
leaf('emoji', { name: 'cat' }),
|
||||
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', () => {
|
||||
it('by triple brackets', () => {
|
||||
const tokens = analyze('(((foo)))');
|
||||
|
@ -280,7 +308,12 @@ describe('MFM', () => {
|
|||
it('local', () => {
|
||||
const tokens = analyze('@himawari foo');
|
||||
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')
|
||||
]);
|
||||
});
|
||||
|
@ -288,7 +321,12 @@ describe('MFM', () => {
|
|||
it('remote', () => {
|
||||
const tokens = analyze('@hima_sub@namori.net foo');
|
||||
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')
|
||||
]);
|
||||
});
|
||||
|
@ -296,7 +334,12 @@ describe('MFM', () => {
|
|||
it('remote punycode', () => {
|
||||
const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah foo');
|
||||
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')
|
||||
]);
|
||||
});
|
||||
|
@ -309,11 +352,26 @@ describe('MFM', () => {
|
|||
|
||||
const tokens2 = analyze('@a\n@b\n@c');
|
||||
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'),
|
||||
leaf('mention', { acct: '@b', canonical: '@b', username: 'b', host: null }),
|
||||
leaf('mention', {
|
||||
acct: '@b',
|
||||
canonical: '@b',
|
||||
username: 'b',
|
||||
host: null
|
||||
}),
|
||||
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');
|
||||
|
@ -321,24 +379,31 @@ describe('MFM', () => {
|
|||
tree('bold', [
|
||||
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, [
|
||||
text('@\n'),
|
||||
leaf('mention', { acct: '@v', canonical: '@v', username: 'v', host: null }),
|
||||
leaf('mention', {
|
||||
acct: '@v',
|
||||
canonical: '@v',
|
||||
username: 'v',
|
||||
host: null
|
||||
}),
|
||||
text('\n'),
|
||||
leaf('mention', { acct: '@veryverylongusername', canonical: '@veryverylongusername', username: 'veryverylongusername', host: null }),
|
||||
// text('\n@toolongtobeasamention')
|
||||
leaf('mention', {
|
||||
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')
|
||||
]);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
|
Loading…
Reference in a new issue