Merge pull request #10833 from misskey-dev/develop

* refactor(frontend): use css modules

* feat: 投稿したコンテンツのAIによる学習を軽減するオプションを追加

Resolve #10819

* enhance(backend): publicReactionsをデフォルトtrueに

* 念のためnoimageaiもつける

* add X-Robots-Tag: noai

* Update ja-JP.yml

* fix(frontend): ブラーエフェクトを有効にしている状態で高負荷になる問題を修正

* enhance(backend): graceful shutdown for job queue and refactor

* fix(backend): テスト時は一部のサービスを停止

* fix test

* New Crowdin updates (#10815)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* refactor

* bump

* refactor(frontend): use css module

* refactor(frontend): use css module

* delete unused component

* センシティブワードを正規表現、CWにも適用するように (#10688)

* cwにセンシティブが効いてない

* CWが無いときにTextを見るように

* 比較演算子間違えた

* とりあえずチェック

* 正規表現対応

* /test/giにも対応

* matchでしなくてもいいのでは感

* レビュー修正

* Update packages/backend/src/core/NoteCreateService.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update packages/backend/src/core/NoteCreateService.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* 修正

* wipかも

* wordsでスペース区切りのものできたかも

* なんか動いたかも

* test作成

* 文言の修正

* 修正

* note参照

---------

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update CHANGELOG.md

* New Crowdin updates (#10823)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* ci: fix typo

* fix(frontend): より明確な説明にしたのとtypo修正

* fix typo

* fix(frontend): カラーバーがリプライには表示されないのを修正

* fix(frontend): チャンネル内の検索ボックスが挙動不審な問題を修正

Fix #10793

* enhance(backend): ノートのハッシュタグもMeilisearchに突っ込むように

今後ハッシュタグ検索とか実装するときのため

* feat(frontend): ユーザー指定ノート検索

* fix(frontend): fix retention chart rendering

* Update about-misskey.vue

* meta: Remove @rinsuki from reviewer-lottery (#10830)

* New Crowdin updates (#10824)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Thai)

* enhance(frontend): アカウント初期設定ウィザードにプライバシー設定を追加

* Update CHANGELOG.md

* fix(backend): ひとつのMeilisearchサーバーを複数のMisskeyサーバーで使えない問題を修正

* fix MkUserSetupDialog.Privacy.vue

* ci: skip non-Japanese locale on TurboSnap

* ci: notify on changes for push events

* ci: fix missing branch

* Update basic.cy.js

* [ci skip] New Crowdin updates (#10834)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Arabic)

* 🎨

* 🎨

* enhance(frontend): add retention line chart

* update deps

* refactor

* fix(frontend): Pageにおいて画像ブロックに画像を設定できない問題を修正

Fix #10837

---------

Co-authored-by: nenohi <kimutipartylove@gmail.com>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: rinsuki <428rinsuki+git@gmail.com>
This commit is contained in:
syuilo 2023-05-12 12:41:53 +09:00 committed by GitHub
commit de6348e8a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 2309 additions and 1844 deletions

View file

@ -103,6 +103,7 @@ redis:
# port: 7700
# apiKey: ''
# ssl: true
# index: ''
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────

View file

@ -103,6 +103,7 @@ redis:
# port: 7700
# apiKey: ''
# ssl: true
# index: ''
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────

View file

@ -103,6 +103,7 @@ redis:
# port: 7700
# apiKey: ''
# ssl: true
# index: ''
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────

View file

@ -6,5 +6,4 @@ groups:
- syuilo
- acid-chicken
- EbiseLutica
- rinsuki
- tamaina

View file

@ -20,7 +20,7 @@ jobs:
fetch-depth: 0
submodules: true
- name: Checkout HEAD
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
run: git checkout ${{ github.head_ref }}
- name: Install pnpm
uses: pnpm/action-setup@v2
@ -41,12 +41,12 @@ jobs:
- name: Build storybook
run: pnpm --filter frontend build-storybook
- name: Publish to Chromatic
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/master'
run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Publish to Chromatic
if: github.event_name != 'pull_request' && github.ref != 'refs/heads/master'
if: github.event_name != 'pull_request_target' && github.ref != 'refs/heads/master'
id: chromatic_push
run: |
DIFF="${{ github.event.before }} HEAD"
@ -57,11 +57,15 @@ jobs:
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
fi
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static $(echo "$CHROMATIC_PARAMETER")
if pnpm --filter frontend chromatic -d storybook-static $(echo "$CHROMATIC_PARAMETER"); then
echo "success=true" >> $GITHUB_OUTPUT
else
echo "success=false" >> $GITHUB_OUTPUT
fi
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Publish to Chromatic
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
id: chromatic_pull_request
run: |
DIFF="${{ github.base_ref }} HEAD"
@ -75,9 +79,21 @@ jobs:
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static $(echo "$CHROMATIC_PARAMETER")
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Notify that Chromatic detects changes
uses: actions/github-script@v6.4.0
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
})
- name: Notify that Chromatic will skip testing
uses: actions/github-script@v6.4.0
if: github.event_name == 'pull_request' && steps.chromatic_pull_request.outputs.skip == 'true'
if: github.event_name == 'pull_request_target' && steps.chromatic_pull_request.outputs.skip == 'true'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View file

@ -12,6 +12,30 @@
-->
## 13.12.2
## NOTE
Meilisearchの設定に`index`が必要になりました。値はMisskeyサーバーのホスト名にすることをお勧めします(アルファベット、ハイフン、アンダーバーのみ使用可能)。例: `misskey-io`
過去に作成された`notes`インデックスは、`<index名>---notes`にリネームが必要です。例: `misskey-io---notes`
### General
- 投稿したコンテンツのAIによる学習を軽減するオプションを追加
### Client
- ユーザーを指定してのノート検索が可能に
- アカウント初期設定ウィザードにプライバシー設定を追加
- リテンション率チャートに折れ線グラフを追加
- Fix: ブラーエフェクトを有効にしている状態で高負荷になる問題を修正
- Fix: Pageにおいて画像ブロックに画像を設定できない問題を修正
- Fix: カラーバーがリプライには表示されないのを修正
- Fix: チャンネル内の検索ボックスが挙動不審な問題を修正
- Fix: リテンションチャートのレンダリングを修正
- Fix: リアクションエフェクトのレンダリングの問題を修正
### Server
- センシティブワードの登録にAnd、正規表現が使用できるようになりました。
- Fix: ひとつのMeilisearchサーバーを複数のMisskeyサーバーで使えない問題を修正
## 13.12.1
### Client

View file

@ -124,6 +124,7 @@ redis:
# port: 7700
# apiKey: ''
# ssl: true
# index: ''
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────

View file

@ -171,6 +171,10 @@ describe('After user signed in', () => {
cy.get('[data-cy-user-setup-continue]').click();
// プライバシー設定
cy.get('[data-cy-user-setup-continue]').click();
// フォローはスキップ
cy.get('[data-cy-user-setup-continue]').click();

View file

@ -19,6 +19,7 @@ noNotes: "لم يُعثر على أية ملاحظات"
noNotifications: "ليس هناك أية اشعارات"
instance: "مثيل الخادم"
settings: "الاعدادات"
notificationSettings: "إعدادات الإشعارات"
basicSettings: "الاعدادات الأساسية"
otherSettings: "إعدادات أخرى"
openInWindow: "افتح في نافذة جديدة"
@ -127,6 +128,7 @@ unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟"
suspendConfirm: "أمتأكد من تعليق الحساب؟"
unsuspendConfirm: "أمتأكد من إلغاء تعليق؟"
selectList: "اختر قائمة"
selectChannel: "اختر قناة"
selectAntenna: "اختر هوائيًا"
selectWidget: "اختر ودجة"
editWidgets: "عدّل الودجات"
@ -250,6 +252,9 @@ noMoreHistory: "لا يوجد المزيد من التاريخ"
startMessaging: "ابدأ محادثة"
nUsersRead: "قرأه {n}"
agreeTo: "اوافق على {0}"
agree: "أقبل"
basicNotesBeforeCreateAccount: "ملاحظات مهمة"
termsOfService: "شروط الخدمة"
start: "البداية"
home: "الرئيسي"
remoteUserCaution: "هذه المعلومات قد لا تكون مكتملة بما أن المستخدم من مثيل بعيد."
@ -379,6 +384,8 @@ about: "عن"
aboutMisskey: "عن Misskey"
administrator: "المدير"
token: "الرمز المميز"
2fa: "الاستيثاق بعاملَيْن"
totp: "تطبيق استيثاق"
moderator: "مشرِف"
moderation: "الإشراف"
nUsersMentioned: "{n} مستخدمين أُشير إليهم"
@ -506,6 +513,7 @@ userSuspended: "عُلق هذا المستخدم."
userSilenced: "كُتم هذا المستخدم."
yourAccountSuspendedTitle: "هذا الحساب معلق"
yourAccountSuspendedDescription: "عُلق الحساب بسبب انتهاك شروط خدمة المثيل و ما شابه. إذا أردت معرفة التفصيل تواصل مع مدير المثيل. رجاءً لا تنشئ حساب جديد."
accountDeleted: "حُذف الحساب"
menu: "القائمة"
divider: "فاصل"
addItem: "إضافة عنصر"

View file

@ -990,6 +990,7 @@ rolesAssignedToMe: "Mir zugewiesene Rollen"
resetPasswordConfirm: "Wirklich Passwort zurücksetzen?"
sensitiveWords: "Sensible Wörter"
sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden."
sensitiveWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden."
notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar."
license: "Lizenz"
unfavoriteConfirm: "Wirklich aus Favoriten entfernen?"
@ -1038,10 +1039,16 @@ thisChannelArchived: "Dieser Kanal wurde archiviert."
displayOfNote: "Anzeige von Notizen"
initialAccountSetting: "Kontoeinrichtung"
youFollowing: "Gefolgt"
preventAiLearning: "Verwendung in machinellem Lernen (Generative bzw. Prediktive AI/KI) ablehnen"
preventAiLearningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (Generative bzw. Prediktive AI/KI) zu verwenden. Dies wird durch das Hinzufügen einer \"noai\"-Flag in der HTML-Antwort des jeweiligen Inhalts erreicht. Da diese Flag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich."
options: "Optionen"
specifyUser: "Spezifischer Benutzer"
_initialAccountSetting:
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
letsFillYourProfile: "Lass uns zuerst dein Profil einrichten."
profileSetting: "Profileinstellungen"
privacySetting: "Privatsphäreneinstellungen"
theseSettingsCanEditLater: "Diese Einstellungen kannst du jederzeit ändern."
youCanEditMoreSettingsInSettingsPageLater: "In den Einstellungen findest du noch viele weitere Optionen. Schau dort später mal vorbei."
followUsers: "Folge zuerst ein paar Nutzern, um deine Chronik zu füllen."

View file

@ -990,6 +990,7 @@ rolesAssignedToMe: "Roles assigned to me"
resetPasswordConfirm: "Really reset your password?"
sensitiveWords: "Sensitive words"
sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks."
sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression."
notesSearchNotAvailable: "Note search is unavailable."
license: "License"
unfavoriteConfirm: "Really remove from favorites?"
@ -1036,20 +1037,26 @@ channelArchiveConfirmTitle: "Really archive {name}?"
channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore."
thisChannelArchived: "This channel has been archived."
displayOfNote: "Note display"
initialAccountSetting: "Profile configuration"
initialAccountSetting: "Profile setup"
youFollowing: "Followed"
preventAiLearning: "Reject usage in Machine Learning (Generative AI)"
preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored."
options: "Options"
specifyUser: "Specific user"
_initialAccountSetting:
accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile."
letsFillYourProfile: "First, let's set up your profile."
profileSetting: "Profile settings"
privacySetting: "Privacy settings"
theseSettingsCanEditLater: "You can always change these settings later."
youCanEditMoreSettingsInSettingsPageLater: "There are many more settings you can configure from the \"Settings\" page. Be sure to visit it later."
followUsers: "Try following some users that interest you to build up your timeline."
pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device."
initialAccountSettingCompleted: "Profile configuration complete!"
initialAccountSettingCompleted: "Profile setup complete!"
haveFun: "Enjoy {name}!"
ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}."
skipAreYouSure: "Really skip profile configuration?"
skipAreYouSure: "Really skip profile setup?"
_serverRules:
description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended."
_accountMigration:
@ -1321,7 +1328,7 @@ _role:
isConditionalRole: "This is a conditional role."
isPublic: "Public role"
descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users."
options: "Role options"
options: "Options"
policies: "Policies"
baseRole: "Role template"
useBaseValue: "Use role template value"

View file

@ -993,6 +993,7 @@ accountMigration: "Migración de cuenta"
accountMoved: "Este usuario se ha mudado a una nueva cuenta:"
horizontal: "Horizontal"
youFollowing: "Siguiendo"
options: "Opción"
_accountMigration:
moveFrom: "Trasladar de otra cuenta a ésta"
moveFromLabel: "Cuenta desde la que se realiza el traslado:"

View file

@ -452,6 +452,7 @@ native: "Natif"
disableDrawer: "Les menus ne s'affichent pas dans le tiroir"
noHistory: "Pas d'historique"
signinHistory: "Historique de connexion"
enableAdvancedMfm: "Activer la MFM avancée"
doing: "En cours..."
category: "Catégorie"
tags: "Étiquettes"
@ -846,6 +847,7 @@ rateLimitExceeded: "Limite de taux dépassée"
cropImage: "Recadrer l'image"
cropImageAsk: "Voulez-vous recadrer cette image ?"
cropYes: "Rogner"
cropNo: "Utiliser en l'état"
file: "Fichiers"
recentNHours: "Dernières {n} heures"
recentNDays: "Derniers {n} jours"
@ -912,6 +914,7 @@ color: "Couleur"
manageCustomEmojis: "Gestion des émojis personnalisés"
preset: "Préréglage"
selectFromPresets: "Sélectionner à partir des préréglages"
thisPostMayBeAnnoyingCancel: "Annuler"
license: "Licence"
video: "Vidéo"
videos: "Vidéos"
@ -925,6 +928,7 @@ leftBottom: "En bas à gauche"
rightBottom: "En bas à droite"
vertical: "Vertical"
horizontal: "Latéral"
serverRules: "Règles du serveur"
youFollowing: "Abonné·e"
_achievements:
_types:
@ -934,10 +938,13 @@ _achievements:
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
_login3:
title: "Débutant "
description: "Se connecter pour un total de 3 jours"
_login7:
title: "Débutant Ⅱ"
description: "Se connecter pour un total de 7 jours"
_login15:
title: "Débutant Ⅲ"
description: "Se connecter pour un total de 15 jours"
_login30:
description: "Se connecter pour un total de 30 jours"
@ -945,6 +952,22 @@ _achievements:
description: "Se connecter pour un total de 60 jours"
_login100:
description: "Se connecter pour un total de 100 jours"
_login200:
description: "Se connecter pour un total de 200 jours"
_login300:
description: "Se connecter pour un total de 300 jours"
_login400:
description: "Se connecter pour un total de 400 jours"
_login500:
description: "Se connecter pour un total de 500 jours"
_login600:
description: "Se connecter pour un total de 600 jours"
_login700:
description: "Se connecter pour un total de 700 jours"
_login800:
description: "Se connecter pour un total de 800 jours"
_login900:
description: "Se connecter pour un total de 900 jours"
_login1000:
flavor: "Merci d'utiliser Misskey !"
_markedAsCat:
@ -954,8 +977,12 @@ _achievements:
title: "Beaucoup d'amis"
_followers10:
title: "Abonnez-moi !"
_iLoveMisskey:
title: "Jadore Misskey"
_viewInstanceChart:
title: "Analyste"
_loggedInOnBirthday:
title: "Joyeux Anniversaire !"
_loggedInOnNewYearsDay:
title: "Bonne année !"
_role:

View file

@ -955,6 +955,7 @@ disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi
disableFederationOk: "Matikan federasi"
horizontal: "Horisontal"
youFollowing: "Mengikuti"
options: "Opsi peran"
_achievements:
earnedAt: "Terbuka pada"
_types:

View file

@ -1020,6 +1020,7 @@ pleaseConfirmBelowBeforeSignup: "Ai sensi del regolamento EU 679/2016 GDPR, auto
pleaseAgreeAllToContinue: "Per continuare, occorre selezionare ed essere d'accordo su tutto."
continue: "Continua"
youFollowing: "Seguiti"
options: "Opzioni del ruolo"
_serverRules:
description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio."
_accountMigration:

View file

@ -689,7 +689,7 @@ no: "いいえ"
driveFilesCount: "ドライブのファイル数"
driveUsage: "ドライブ使用量"
noCrawle: "クローラーによるインデックスを拒否"
noCrawleDescription: "検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要します。"
noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要します。"
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
@ -832,6 +832,8 @@ breakFollow: "フォロワーを解除"
breakFollowConfirm: "フォロワー解除しますか?"
itsOn: "オンになっています"
itsOff: "オフになっています"
on: "オン"
off: "オフ"
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
unread: "未読"
filter: "フィルタ"
@ -990,6 +992,7 @@ rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
notesSearchNotAvailable: "ノート検索は利用できません。"
license: "ライセンス"
unfavoriteConfirm: "お気に入り解除しますか?"
@ -1038,12 +1041,17 @@ thisChannelArchived: "このチャンネルはアーカイブされています
displayOfNote: "ノートの表示"
initialAccountSetting: "初期設定"
youFollowing: "フォロー中"
preventAiLearning: "生成AIによる学習を拒否"
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
options: "オプション"
specifyUser: "ユーザー指定"
_initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!"
letsStartAccountSetup: "アカウントの初期設定を行いましょう。"
letsFillYourProfile: "まずはあなたのプロフィールを設定しましょう。"
profileSetting: "プロフィール設定"
privacySetting: "プライバシー設定"
theseSettingsCanEditLater: "これらの設定は後から変更できます。"
youCanEditMoreSettingsInSettingsPageLater: "この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。"
followUsers: "タイムラインを構築するため、気になるユーザーをフォローしてみましょう。"

View file

@ -689,7 +689,7 @@ no: "あかん"
driveFilesCount: "ドライブのファイル数"
driveUsage: "ドライブ使用量やで"
noCrawle: "クローラーによるインデックスを拒否するで"
noCrawleDescription: "検索エンジンにあんたのユーザーページ、ート、Pagesとかのコンテンツを登録(インデックス)せんように頼むで。邪魔すんねんやったら帰って〜。"
noCrawleDescription: "検索エンジンにあんたのユーザーページ、ート、Pagesとかのコンテンツを登録(インデックス)せぇへんように頼むで。"
lockedAccountInfo: "フォローを承認制にしとっても、ノートの公開範囲を「フォロワー」にせぇへん限り、誰でもあんたのノートを見れるで。"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで"
loadRawImages: "添付画像のサムネイルをオリジナル画質にするで"
@ -990,6 +990,7 @@ rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワード作り直すんでええな?"
sensitiveWords: "けったいな単語"
sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。"
sensitiveWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。"
notesSearchNotAvailable: "ノート検索は使われへんで。"
license: "ライセンス"
unfavoriteConfirm: "ほんまに気に入らんの?"
@ -1038,10 +1039,16 @@ thisChannelArchived: "このチャンネル、アーカイブされとるで。"
displayOfNote: "ノートの表示"
initialAccountSetting: "初期設定"
youFollowing: "フォロー中やで"
preventAiLearning: "生成AIの学習に使わんといて"
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
options: "オプション"
specifyUser: "ユーザー指定"
_initialAccountSetting:
accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。"
letsFillYourProfile: "最初はあんたのプロフィールを設定しよか。"
profileSetting: "プロフィール設定"
privacySetting: "プライバシー設定"
theseSettingsCanEditLater: "この設定はあとから変えれるで。"
youCanEditMoreSettingsInSettingsPageLater: "これ以外にもいろんな設定を「設定」ページからできるで。後で確認してみてな。"
followUsers: "タイムラインを構築するために、気になるユーザーをフォローしてみ。"
@ -1060,11 +1067,12 @@ _accountMigration:
moveTo: "このアカウントをさらのアカウントに引っ越すで"
moveToLabel: "引っ越し先のアカウント:"
moveCannotBeUndone: "アカウントを移行すると、取り消すことはできへんくなります。"
moveAccountDescription: "この操作は戻されへんで。まず引っ越し先のアカウントでこのアカウントへのエイリアスが作れたか確認してきなはれや。エイリアスができてたら、引っ越し先のアカウントをこんな風に入力してくれへんか?:@person@instance.com"
moveAccountDescription: "おニューのアカウントに移行すんで。\n ・フォロワーがおニューの方を勝手にフォローすんで。\n ・このアカウントからのフォローはまるまる全部解除されんで。\n ・このアカウントでート作れへんようになるで。\n\nフォロワーの移行は勝手にこっちでやっとくけど、フォローの移行は自分でしてや。移行前にこのアカウントでフォローエクスポートして、移行したあとすぐにおニューのところでインポートしてくれな。\nリストとかミュート、あとブロックもおんなじや。自分で移行してな。\n\nこの説明はこのサーバー、つまりMisskey v13.12.0から後の仕様や。Mastodonとか他のActivityPubソフトやとちょっと挙動が違うこともあんで。"
moveAccountHowTo: "アカウントの引っ越しには、まず引っ越し先のアカウントで自分のアカウントに対しエイリアスを作成しなはれや。\nエイリアス作成した後、引っ越し先のアカウントを次のように入力してくれへんか:@username@server.example.com"
startMigration: "引っ越しする"
migrationConfirm: "ほんまにこのアカウントを {account} に引っ越すんか?一回引っ越してもうたら取り消されへんし、二度とこのアカウントを元に戻されへんくなるで。\nそれと、引っ越し先のアカウントでエイリアスが作れたかちゃんと確認しーや"
movedAndCannotBeUndone: "\nアカウントはもう引っ越されてます。\n引っ越しを取り消すことはできまへん。"
postMigrationNote: "このアカウントからのフォロー解除は移行操作から丸一日経ったら実行されんで。\nこのアカウントのフォロー・フォロワー数はどっちも0や。フォローの解除はされへんから、あんたのフォロワーはこのアカウントのフォロワー向けの投稿をこの後も見れるで。"
movedTo: "引っ越し先のアカウント:"
_achievements:
earnedAt: "貰った日ぃ"

View file

@ -1038,6 +1038,7 @@ thisChannelArchived: "이 채널은 아카이브되었습니다."
displayOfNote: "노트 표시"
initialAccountSetting: "초기 설정"
youFollowing: "팔로잉"
options: "옵션"
_initialAccountSetting:
accountCreated: "계정 생성이 완료되었습니다!"
letsStartAccountSetup: "계정의 초기 설정을 진행합니다."
@ -1240,6 +1241,7 @@ _achievements:
title: "잠깐 쉬어"
description: "클라이언트를 시작하고 30분이 경과하였습니다"
_client60min:
title: "No \"Miss\" in Misskey"
description: "클라이언트를 시작하고 60분이 경과하였습니다"
_noteDeletedWithin1min:
title: "있었는데요 없었습니다"

View file

@ -278,6 +278,7 @@ video: "Video"
videos: "Videoer"
continue: "Fortsett"
youFollowing: "Følger"
options: "Alternativ"
_initialAccountSetting:
theseSettingsCanEditLater: "Du kan endre disse innstillingene senere."
_achievements:

View file

@ -1006,6 +1006,7 @@ videos: "Видео"
dataSaver: "Экономия трафика"
horizontal: "Сбоку"
youFollowing: "Подписки"
options: "Настройки ролей"
_achievements:
earnedAt: "Разблокировано в"
_types:

View file

@ -1031,6 +1031,7 @@ preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว
preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร"
createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
youFollowing: "ติดตามแล้ว"
options: "ตัวเลือกบทบาท"
_serverRules:
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
_accountMigration:

View file

@ -1030,6 +1030,13 @@ continue: "继续"
preservedUsernames: "保留的用户名"
createNoteFromTheFile: "从文件创建帖子"
youFollowing: "正在关注"
options: "选项"
_initialAccountSetting:
accountCreated: "账户创建完成了!"
letsStartAccountSetup: "来进行帐户的初始设置吧。"
letsFillYourProfile: "首先,来设定你的个人档案吧!"
profileSetting: "个人资料设置"
theseSettingsCanEditLater: "也可以在稍后修改这里的设置。"
_serverRules:
description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。"
_accountMigration:
@ -1038,8 +1045,11 @@ _accountMigration:
moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com"
moveTo: "把这个账户迁移到新的账户"
moveToLabel: "迁移后的账户"
moveCannotBeUndone: "一旦迁移账户,就无法撤销。"
moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com"
startMigration: "迁移"
migrationConfirm: "确定要把此账户迁移到{account}吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时请确认迁移后的账户已创造别名。"
movedAndCannotBeUndone: "该账户已被迁移。\n迁移操作无法撤销。"
movedTo: "迁移后的账户"
_achievements:
earnedAt: "达成时间"
@ -1572,6 +1582,9 @@ _time:
minute: "分"
hour: "小时"
day: "日"
_timelineTutorial:
step3_1: "将想说的话发出去了吗?"
step3_2: "太棒了!现在你可以在你的时间线中看到刚刚发布的帖子了。"
_2fa:
alreadyRegistered: "此设备已被注册"
registerTOTP: "开始设置认证应用"

View file

@ -990,6 +990,7 @@ rolesAssignedToMe: "指派給自己的角色"
resetPasswordConfirm: "重設密碼?"
sensitiveWords: "敏感詞"
sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。"
sensitiveWordsDescription2: "用空格分隔關鍵詞構成AND格式用斜線包圍關鍵字構成正規表達式。"
notesSearchNotAvailable: "無法使用搜尋貼文功能。"
license: "授權"
unfavoriteConfirm: "要取消收錄我的最愛嗎?"
@ -1036,7 +1037,24 @@ channelArchiveConfirmTitle: "要封存{name}嗎?"
channelArchiveConfirmDescription: "封存以後,在頻道列表與搜索結果中不會顯示,也無法發布新的貼文。"
thisChannelArchived: "這個頻道已被封存。"
displayOfNote: "顯示貼文"
youFollowing: "關注中"
initialAccountSetting: "初始設定"
youFollowing: "追隨中"
preventAiLearning: "拒絕接受產生式AI的學習"
preventAiLearningDescription: "要求外部的文章產生AI或圖像產生AI不以發布的貼文和圖像等內容為學習對象。這是透過在HTML響應中包含noai旗標來實現的但不能完全防止AI的學習因為這要看該AI是否遵守這個要求。"
options: "選項"
_initialAccountSetting:
accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。"
letsFillYourProfile: "首先,來設定您的個人檔案吧。"
profileSetting: "個人檔案設定"
theseSettingsCanEditLater: "這裡的設定可以在之後變更。"
youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。"
followUsers: "為了構築時間軸,試著追蹤您感興趣的使用者吧。"
pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。"
initialAccountSettingCompleted: "初始設定完成了!"
haveFun: "盡情享受{name}吧!"
ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。"
skipAreYouSure: "要略過初始設定嗎?"
_serverRules:
description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。"
_accountMigration:
@ -1464,7 +1482,7 @@ _channel:
removeBanner: "移除橫幅圖像"
featured: "熱門貼文"
owned: "管理中"
following: "關注中"
following: "追隨中"
usersCount: "有{n}人參與"
notesCount: "有{n}個貼文"
nameAndDescription: "名稱與說明"
@ -1586,6 +1604,16 @@ _time:
minute: "分鐘"
hour: "小時"
day: "日"
_timelineTutorial:
title: "Misskey的使用方法"
step1_1: "這個畫面是「時間軸」。發布到{name}的「貼文」按照時間順序顯示。"
step1_2: "時間軸有多種類型,例如在「首頁時間軸」中流動的是您追蹤的人的貼文;而在「本地時間軸」流動的是{name}全體的貼文。"
step2_1: "試試看,發布個貼文吧!按畫面上鉛筆圖示的按鈕開啟表格。"
step2_2: "初次貼文的內容,建議包括自我介紹以及「開始使用{name}」。"
step3_1: "貼文發出去了嗎?"
step3_2: "如果你的貼文出現在時間軸上,就代表發文成功。"
step4_1: "可以對貼文標記「反應」。"
step4_2: "點擊貼文的「+」圖示,即可選擇喜好的表情符號來標記反應。"
_2fa:
alreadyRegistered: "此設備已經被註冊過了"
registerTOTP: "開始設定驗證應用程式"

View file

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.12.1",
"version": "13.12.2",
"codename": "nasubi",
"repository": {
"type": "git",
@ -56,11 +56,11 @@
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.59.2",
"@typescript-eslint/parser": "5.59.2",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"cross-env": "7.0.3",
"cypress": "12.11.0",
"eslint": "8.39.0",
"cypress": "12.12.0",
"eslint": "8.40.0",
"start-server-and-test": "2.0.0"
},
"optionalDependencies": {

View file

@ -0,0 +1,11 @@
export class PreventAiLarning1683682889948 {
name = 'PreventAiLarning1683682889948'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "preventAiLarning" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "preventAiLarning"`);
}
}

View file

@ -0,0 +1,11 @@
export class PublicReactionsDefaultTrue1683683083083 {
name = 'PublicReactionsDefaultTrue1683683083083'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "publicReactions" SET DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "publicReactions" SET DEFAULT false`);
}
}

View file

@ -0,0 +1,11 @@
export class FixTypo1683789676867 {
name = 'FixTypo1683789676867'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" RENAME COLUMN "preventAiLarning" TO "preventAiLearning"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" RENAME COLUMN "preventAiLearning" TO "preventAiLarning"`);
}
}

View file

@ -58,7 +58,7 @@
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.1",
"@fastify/http-proxy": "9.0.0",
"@fastify/http-proxy": "9.1.0",
"@fastify/multipart": "7.6.0",
"@fastify/static": "6.10.1",
"@fastify/view": "7.4.1",
@ -89,11 +89,11 @@
"escape-regexp": "0.0.1",
"fastify": "4.17.0",
"feed": "4.2.2",
"file-type": "18.3.0",
"file-type": "18.4.0",
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "12.6.0",
"happy-dom": "9.10.2",
"happy-dom": "9.16.0",
"hpagent": "1.2.0",
"ioredis": "5.3.2",
"ip-cidr": "3.1.0",
@ -110,11 +110,11 @@
"ms": "3.0.0-canary.1",
"nested-property": "4.0.0",
"node-fetch": "3.3.1",
"nodemailer": "6.9.1",
"nodemailer": "6.9.2",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"os-utils": "0.0.14",
"otpauth": "9.1.1",
"otpauth": "9.1.2",
"parse5": "7.1.2",
"pg": "8.10.0",
"private-ip": "3.0.0",
@ -149,7 +149,7 @@
"tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.15",
"typeorm": "0.3.16",
"typescript": "5.0.4",
"ulid": "2.3.0",
"unzipper": "0.10.11",
@ -178,7 +178,7 @@
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1",
"@types/node": "18.16.3",
"@types/node": "20.1.3",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1",
@ -191,7 +191,7 @@
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.9.0",
"@types/semver": "7.3.13",
"@types/semver": "7.5.0",
"@types/sharp": "0.32.0",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/tinycolor2": "1.4.3",
@ -202,11 +202,11 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.59.2",
"@typescript-eslint/parser": "5.59.2",
"aws-sdk-client-mock": "^2.1.1",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"aws-sdk-client-mock": "2.1.1",
"cross-env": "7.0.3",
"eslint": "8.39.0",
"eslint": "8.40.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.5.0",

View file

@ -18,10 +18,12 @@ export async function server() {
const serverService = app.get(ServerService);
await serverService.launch();
app.get(ChartManagementService).start();
app.get(JanitorService).start();
app.get(QueueStatsService).start();
app.get(ServerStatsService).start();
if (process.env.NODE_ENV !== 'test') {
app.get(ChartManagementService).start();
app.get(JanitorService).start();
app.get(QueueStatsService).start();
app.get(ServerStatsService).start();
}
return app;
}

View file

@ -62,6 +62,7 @@ export type Source = {
port: string;
apiKey: string;
ssl?: boolean;
index: string;
};
proxy?: string;

View file

@ -3,6 +3,7 @@ import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import RE2 from 're2';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
@ -238,7 +239,8 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.channel != null) data.localOnly = true;
if (data.visibility === 'public' && data.channel == null) {
if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) {
const sensitiveWords = (await this.metaService.fetch()).sensitiveWords;
if (this.isSensitive(data, sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
@ -670,6 +672,31 @@ export class NoteCreateService implements OnApplicationShutdown {
// Register to search database
this.index(note);
}
@bindThis
private isSensitive(note: Option, sensitiveWord: string[]): boolean {
if (sensitiveWord.length > 0) {
const text = note.cw ?? note.text ?? '';
if (text === '') return false;
const matched = sensitiveWord.some(filter => {
// represents RegExp
const regexp = filter.match(/^\/(.+)\/(.*)$/);
// This should never happen due to input sanitisation.
if (!regexp) {
const words = filter.split(' ');
return words.every(keyword => text.includes(keyword));
}
try {
return new RE2(regexp[1], regexp[2]).test(text);
} catch (err) {
// This should never happen due to input sanitisation.
return false;
}
});
if (matched) return true;
}
return false;
}
@bindThis
private incRenoteCount(renote: Note) {

View file

@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { setTimeout } from 'node:timers/promises';
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
import Bull from 'bull';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
@ -41,9 +42,9 @@ export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type DeliverQueue = Bull.Queue<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue<DbJobData<keyof DbJobMap>>;
export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue<ObjectStorageJobData>;
export type ObjectStorageQueue = Bull.Queue;
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
const $system: Provider = {
@ -118,4 +119,36 @@ const $webhookDeliver: Provider = {
$webhookDeliver,
],
})
export class QueueModule {}
export class QueueModule implements OnApplicationShutdown {
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) {}
async onApplicationShutdown(signal: string): Promise<void> {
if (process.env.NODE_ENV === 'test') {
// XXX:
// Shutting down the existing connections causes errors on Jest as
// Misskey has asynchronous postgres/redis connections that are not
// awaited.
// Let's wait for some random time for them to finish.
await setTimeout(5000);
}
await Promise.all([
this.systemQueue.close(),
this.endedPollNotificationQueue.close(),
this.deliverQueue.close(),
this.inboxQueue.close(),
this.dbQueue.close(),
this.relationshipQueue.close(),
this.objectStorageQueue.close(),
this.webhookDeliverQueue.close(),
]);
}
}

View file

@ -68,7 +68,7 @@ export class SearchService {
private idService: IdService,
) {
if (meilisearch) {
this.meilisearchNoteIndex = meilisearch.index('notes');
this.meilisearchNoteIndex = meilisearch.index(`${config.meilisearch!.index}---notes`);
this.meilisearchNoteIndex.updateSettings({
searchableAttributes: [
'text',
@ -82,6 +82,7 @@ export class SearchService {
'userId',
'userHost',
'channelId',
'tags',
],
typoTolerance: {
enabled: false,
@ -107,6 +108,7 @@ export class SearchService {
channelId: note.channelId,
cw: note.cw,
text: note.text,
tags: note.tags,
}], {
primaryKey: 'id',
});

View file

@ -445,6 +445,7 @@ export class UserEntityService implements OnModuleInit {
carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed,
noCrawle: profile!.noCrawle,
preventAiLearning: profile!.preventAiLearning,
isExplorable: user.isExplorable,
isDeleted: user.isDeleted,
hideOnlineStatus: user.hideOnlineStatus,

View file

@ -76,7 +76,7 @@ export class UserProfile {
public emailNotificationTypes: string[];
@Column('boolean', {
default: false,
default: true,
})
public publicReactions: boolean;
@ -147,6 +147,11 @@ export class UserProfile {
})
public noCrawle: boolean;
@Column('boolean', {
default: true,
})
public preventAiLearning: boolean;
@Column('boolean', {
default: false,
})

View file

@ -302,7 +302,11 @@ export const packedMeDetailedOnlySchema = {
},
noCrawle: {
type: 'boolean',
nullable: true, optional: false,
nullable: false, optional: false,
},
preventAiLearning: {
type: 'boolean',
nullable: false, optional: false,
},
isExplorable: {
type: 'boolean',

View file

@ -1,69 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js';
import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js';
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js';
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
import type Bull from 'bull';
@Injectable()
export class DbQueueProcessorsService {
constructor(
@Inject(DI.config)
private config: Config,
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
private exportNotesProcessorService: ExportNotesProcessorService,
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
private exportFollowingProcessorService: ExportFollowingProcessorService,
private exportMutingProcessorService: ExportMutingProcessorService,
private exportBlockingProcessorService: ExportBlockingProcessorService,
private exportUserListsProcessorService: ExportUserListsProcessorService,
private exportAntennasProcessorService: ExportAntennasProcessorService,
private importFollowingProcessorService: ImportFollowingProcessorService,
private importMutingProcessorService: ImportMutingProcessorService,
private importBlockingProcessorService: ImportBlockingProcessorService,
private importUserListsProcessorService: ImportUserListsProcessorService,
private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService,
private importAntennasProcessorService: ImportAntennasProcessorService,
private deleteAccountProcessorService: DeleteAccountProcessorService,
) {
}
@bindThis
public start(q: Bull.Queue): void {
q.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done));
q.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done));
q.process('exportNotes', (job, done) => this.exportNotesProcessorService.process(job, done));
q.process('exportFavorites', (job, done) => this.exportFavoritesProcessorService.process(job, done));
q.process('exportFollowing', (job, done) => this.exportFollowingProcessorService.process(job, done));
q.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done));
q.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done));
q.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done));
q.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done));
q.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done));
q.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job));
q.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done));
q.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done));
q.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job));
q.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done));
q.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done));
q.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done));
q.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job));
}
}

View file

@ -1,25 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ObjectStorageQueueProcessorsService {
constructor(
@Inject(DI.config)
private config: Config,
private deleteFileProcessorService: DeleteFileProcessorService,
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
) {
}
@bindThis
public start(q: Bull.Queue): void {
q.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job));
q.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done));
}
}

View file

@ -3,14 +3,10 @@ import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js';
import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js';
import { DbQueueProcessorsService } from './DbQueueProcessorsService.js';
import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js';
import { ObjectStorageQueueProcessorsService } from './ObjectStorageQueueProcessorsService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
import { SystemQueueProcessorsService } from './SystemQueueProcessorsService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
@ -68,10 +64,6 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeleteFileProcessorService,
CleanRemoteFilesProcessorService,
RelationshipProcessorService,
SystemQueueProcessorsService,
ObjectStorageQueueProcessorsService,
DbQueueProcessorsService,
RelationshipQueueProcessorsService,
WebhookDeliverProcessorService,
EndedPollNotificationProcessorService,
DeliverProcessorService,

View file

@ -5,15 +5,36 @@ import type Logger from '@/logger.js';
import { QueueService } from '@/core/QueueService.js';
import { bindThis } from '@/decorators.js';
import { getJobInfo } from './get-job-info.js';
import { SystemQueueProcessorsService } from './SystemQueueProcessorsService.js';
import { ObjectStorageQueueProcessorsService } from './ObjectStorageQueueProcessorsService.js';
import { DbQueueProcessorsService } from './DbQueueProcessorsService.js';
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js';
import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js';
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js';
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js';
import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js';
@Injectable()
export class QueueProcessorService {
@ -25,14 +46,35 @@ export class QueueProcessorService {
private queueLoggerService: QueueLoggerService,
private queueService: QueueService,
private systemQueueProcessorsService: SystemQueueProcessorsService,
private objectStorageQueueProcessorsService: ObjectStorageQueueProcessorsService,
private dbQueueProcessorsService: DbQueueProcessorsService,
private relationshipQueueProcessorsService: RelationshipQueueProcessorsService,
private webhookDeliverProcessorService: WebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
private deliverProcessorService: DeliverProcessorService,
private inboxProcessorService: InboxProcessorService,
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
private exportNotesProcessorService: ExportNotesProcessorService,
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
private exportFollowingProcessorService: ExportFollowingProcessorService,
private exportMutingProcessorService: ExportMutingProcessorService,
private exportBlockingProcessorService: ExportBlockingProcessorService,
private exportUserListsProcessorService: ExportUserListsProcessorService,
private exportAntennasProcessorService: ExportAntennasProcessorService,
private importFollowingProcessorService: ImportFollowingProcessorService,
private importMutingProcessorService: ImportMutingProcessorService,
private importBlockingProcessorService: ImportBlockingProcessorService,
private importUserListsProcessorService: ImportUserListsProcessorService,
private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService,
private importAntennasProcessorService: ImportAntennasProcessorService,
private deleteAccountProcessorService: DeleteAccountProcessorService,
private deleteFileProcessorService: DeleteFileProcessorService,
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
private relationshipProcessorService: RelationshipProcessorService,
private tickChartsProcessorService: TickChartsProcessorService,
private resyncChartsProcessorService: ResyncChartsProcessorService,
private cleanChartsProcessorService: CleanChartsProcessorService,
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
private cleanProcessorService: CleanProcessorService,
) {
this.logger = this.queueLoggerService.logger;
}
@ -119,14 +161,6 @@ export class QueueProcessorService {
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job));
this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job));
this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done));
this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job));
this.dbQueueProcessorsService.start(this.queueService.dbQueue);
this.relationshipQueueProcessorsService.start(this.queueService.relationshipQueue);
this.objectStorageQueueProcessorsService.start(this.queueService.objectStorageQueue);
this.queueService.systemQueue.add('tickCharts', {
}, {
repeat: { cron: '55 * * * *' },
@ -163,6 +197,46 @@ export class QueueProcessorService {
removeOnComplete: true,
});
this.systemQueueProcessorsService.start(this.queueService.systemQueue);
this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job));
this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job));
this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done));
this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job));
this.queueService.dbQueue.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done));
this.queueService.dbQueue.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done));
this.queueService.dbQueue.process('exportNotes', (job, done) => this.exportNotesProcessorService.process(job, done));
this.queueService.dbQueue.process('exportFavorites', (job, done) => this.exportFavoritesProcessorService.process(job, done));
this.queueService.dbQueue.process('exportFollowing', (job, done) => this.exportFollowingProcessorService.process(job, done));
this.queueService.dbQueue.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done));
this.queueService.dbQueue.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done));
this.queueService.dbQueue.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done));
this.queueService.dbQueue.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done));
this.queueService.dbQueue.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done));
this.queueService.dbQueue.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job));
this.queueService.dbQueue.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done));
this.queueService.dbQueue.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done));
this.queueService.dbQueue.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job));
this.queueService.dbQueue.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done));
this.queueService.dbQueue.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done));
this.queueService.dbQueue.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done));
this.queueService.dbQueue.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job));
this.queueService.objectStorageQueue.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job));
this.queueService.objectStorageQueue.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done));
{
const maxJobs = this.config.relashionshipJobConcurrency ?? 16;
this.queueService.relationshipQueue.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job));
this.queueService.relationshipQueue.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job));
this.queueService.relationshipQueue.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job));
this.queueService.relationshipQueue.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job));
}
this.queueService.systemQueue.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done));
this.queueService.systemQueue.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done));
this.queueService.systemQueue.process('cleanCharts', (job, done) => this.cleanChartsProcessorService.process(job, done));
this.queueService.systemQueue.process('aggregateRetention', (job, done) => this.aggregateRetentionProcessorService.process(job, done));
this.queueService.systemQueue.process('checkExpiredMutings', (job, done) => this.checkExpiredMutingsProcessorService.process(job, done));
this.queueService.systemQueue.process('clean', (job, done) => this.cleanProcessorService.process(job, done));
}
}

View file

@ -1,26 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
import type Bull from 'bull';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
@Injectable()
export class RelationshipQueueProcessorsService {
constructor(
@Inject(DI.config)
private config: Config,
private relationshipProcessorService: RelationshipProcessorService,
) {
}
@bindThis
public start(q: Bull.Queue): void {
const maxJobs = this.config.relashionshipJobConcurrency ?? 16;
q.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job));
q.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job));
q.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job));
q.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job));
}
}

View file

@ -1,37 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import type Bull from 'bull';
@Injectable()
export class SystemQueueProcessorsService {
constructor(
@Inject(DI.config)
private config: Config,
private tickChartsProcessorService: TickChartsProcessorService,
private resyncChartsProcessorService: ResyncChartsProcessorService,
private cleanChartsProcessorService: CleanChartsProcessorService,
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
private cleanProcessorService: CleanProcessorService,
) {
}
@bindThis
public start(q: Bull.Queue): void {
q.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done));
q.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done));
q.process('cleanCharts', (job, done) => this.cleanChartsProcessorService.process(job, done));
q.process('aggregateRetention', (job, done) => this.aggregateRetentionProcessorService.process(job, done));
q.process('checkExpiredMutings', (job, done) => this.checkExpiredMutingsProcessorService.process(job, done));
q.process('clean', (job, done) => this.cleanProcessorService.process(job, done));
}
}

View file

@ -68,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
emailVerified: profile.emailVerified,
autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle,
preventAiLearning: profile.preventAiLearning,
alwaysMarkNsfw: profile.alwaysMarkNsfw,
autoSensitive: profile.autoSensitive,
carefulBot: profile.carefulBot,

View file

@ -98,7 +98,7 @@ export const meta = {
message: 'This feature is restricted by your role.',
code: 'RESTRICTED_BY_ROLE',
id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
}
},
},
res: {
@ -138,6 +138,7 @@ export const paramDef = {
carefulBot: { type: 'boolean' },
autoAcceptFollowed: { type: 'boolean' },
noCrawle: { type: 'boolean' },
preventAiLearning: { type: 'boolean' },
isBot: { type: 'boolean' },
isCat: { type: 'boolean' },
showTimelineReplies: { type: 'boolean' },
@ -242,6 +243,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;

View file

@ -35,8 +35,8 @@ import { RoleService } from '@/core/RoleService.js';
import manifest from './manifest.json' assert { type: 'json' };
import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js';
import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
import { ClientLoggerService } from './ClientLoggerService.js';
import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@ -423,6 +423,10 @@ export class ClientServerService {
: [];
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
return await reply.view('user', {
user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
@ -467,6 +471,10 @@ export class ClientServerService {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
return await reply.view('note', {
note: _note,
profile,
@ -506,6 +514,10 @@ export class ClientServerService {
} else {
reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
}
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
return await reply.view('page', {
page: _page,
profile,
@ -530,6 +542,10 @@ export class ClientServerService {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
return await reply.view('flash', {
flash: _flash,
profile,
@ -554,6 +570,10 @@ export class ClientServerService {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
return await reply.view('clip', {
clip: _clip,
profile,
@ -576,6 +596,10 @@ export class ClientServerService {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
return await reply.view('gallery-post', {
post: _post,
profile,

View file

@ -21,6 +21,9 @@ block og
block meta
if profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noimageai')
meta(name='robots' content='noai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)

View file

@ -21,6 +21,9 @@ block og
block meta
if profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noimageai')
meta(name='robots' content='noai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)

View file

@ -21,6 +21,9 @@ block og
block meta
if user.host || profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noimageai')
meta(name='robots' content='noai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)

View file

@ -22,6 +22,9 @@ block og
block meta
if user.host || isRenote || profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noimageai')
meta(name='robots' content='noai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)

View file

@ -21,6 +21,9 @@ block og
block meta
if profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noimageai')
meta(name='robots' content='noai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)

View file

@ -20,6 +20,9 @@ block og
block meta
if user.host || profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noimageai')
meta(name='robots' content='noai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)

View file

@ -541,6 +541,61 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
test('センシティブな投稿はhomeになる (単語指定)', async () => {
const sensitive = await api('admin/update-meta', {
sensitiveWords: [
"test",
]
}, alice);
assert.strictEqual(sensitive.status, 204);
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
text: 'hogetesthuge',
}, alice);
assert.strictEqual(note1.status, 200);
assert.strictEqual(note1.body.createdNote.visibility, 'home');
});
test('センシティブな投稿はhomeになる (正規表現)', async () => {
const sensitive = await api('admin/update-meta', {
sensitiveWords: [
"/Test/i",
]
}, alice);
assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', {
text: 'hogetesthuge',
}, alice);
assert.strictEqual(note2.status, 200);
assert.strictEqual(note2.body.createdNote.visibility, 'home');
});
test('センシティブな投稿はhomeになる (スペースアンド)', async () => {
const sensitive = await api('admin/update-meta', {
sensitiveWords: [
"Test hoge"
]
}, alice);
assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', {
text: 'hogeTesthuge',
}, alice);
assert.strictEqual(note2.status, 200);
assert.strictEqual(note2.body.createdNote.visibility, 'home');
});
});
describe('notes/delete', () => {

View file

@ -145,6 +145,7 @@ describe('ユーザー', () => {
carefulBot: user.carefulBot,
autoAcceptFollowed: user.autoAcceptFollowed,
noCrawle: user.noCrawle,
preventAiLearning: user.preventAiLearning,
isExplorable: user.isExplorable,
isDeleted: user.isDeleted,
hideOnlineStatus: user.hideOnlineStatus,
@ -370,7 +371,7 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.pinnedNotes, []);
assert.strictEqual(response.pinnedPageId, null);
assert.strictEqual(response.pinnedPage, null);
assert.strictEqual(response.publicReactions, false);
assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.ffVisibility, 'public');
assert.strictEqual(response.twoFactorEnabled, false);
assert.strictEqual(response.usePasswordLessLogin, false);
@ -390,6 +391,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.carefulBot, false);
assert.strictEqual(response.autoAcceptFollowed, true);
assert.strictEqual(response.noCrawle, false);
assert.strictEqual(response.preventAiLearning, true);
assert.strictEqual(response.isExplorable, true);
assert.strictEqual(response.isDeleted, false);
assert.strictEqual(response.hideOnlineStatus, false);
@ -462,6 +464,8 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ autoAcceptFollowed: false }) },
{ parameters: (): object => ({ noCrawle: true }) },
{ parameters: (): object => ({ noCrawle: false }) },
{ parameters: (): object => ({ preventAiLearning: false }) },
{ parameters: (): object => ({ preventAiLearning: true }) },
{ parameters: (): object => ({ isBot: true }) },
{ parameters: (): object => ({ isBot: false }) },
{ parameters: (): object => ({ isCat: true }) },

View file

@ -45,7 +45,7 @@ fs.readFile(
micromatch(Array.from(modules), [
'../../assets/**',
'../../fluent-emojis/**',
'../../locales/**',
'../../locales/ja-JP.yml',
'../../misskey-assets/**',
'assets/**',
'public/**',

View file

@ -17,13 +17,13 @@
"@discordapp/twemoji": "14.1.2",
"@rollup/plugin-alias": "5.0.0",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.13.2",
"@tabler/icons-webfont": "2.17.0",
"@vitejs/plugin-vue": "4.2.1",
"@vue-macros/reactivity-transform": "^0.3.5",
"@vue/compiler-sfc": "3.2.47",
"@vitejs/plugin-vue": "4.2.2",
"@vue-macros/reactivity-transform": "0.3.6",
"@vue/compiler-sfc": "3.3.1",
"autosize": "5.0.2",
"blurhash": "2.0.5",
"broadcast-channel": "4.20.2",
@ -34,14 +34,14 @@
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1",
"chromatic": "6.17.3",
"compare-versions": "5.0.1",
"chromatic": "6.17.4",
"compare-versions": "5.0.3",
"cropperjs": "2.0.0-beta.2",
"date-fns": "2.30.0",
"escape-regexp": "0.0.1",
"eventemitter3": "5.0.1",
"gsap": "3.11.5",
"idb-keyval": "6.2.0",
"idb-keyval": "6.2.1",
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
@ -53,7 +53,7 @@
"punycode": "2.3.0",
"querystring": "0.2.1",
"rndstr": "1.0.0",
"rollup": "3.21.3",
"rollup": "3.21.6",
"s-age": "1.1.2",
"sanitize-html": "2.10.0",
"sass": "1.62.1",
@ -70,40 +70,40 @@
"typescript": "5.0.4",
"uuid": "9.0.0",
"vanilla-tilt": "1.8.0",
"vite": "4.3.4",
"vue": "3.2.47",
"vite": "4.3.5",
"vue": "3.3.1",
"vue-plyr": "7.0.0",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next"
},
"devDependencies": {
"@storybook/addon-actions": "7.0.7",
"@storybook/addon-essentials": "7.0.7",
"@storybook/addon-interactions": "7.0.7",
"@storybook/addon-links": "7.0.7",
"@storybook/addon-storysource": "7.0.7",
"@storybook/addons": "7.0.7",
"@storybook/blocks": "7.0.7",
"@storybook/core-events": "7.0.7",
"@storybook/addon-actions": "7.0.10",
"@storybook/addon-essentials": "7.0.10",
"@storybook/addon-interactions": "7.0.10",
"@storybook/addon-links": "7.0.10",
"@storybook/addon-storysource": "7.0.10",
"@storybook/addons": "7.0.10",
"@storybook/blocks": "7.0.10",
"@storybook/core-events": "7.0.10",
"@storybook/jest": "0.1.0",
"@storybook/manager-api": "7.0.7",
"@storybook/preview-api": "7.0.7",
"@storybook/react": "7.0.7",
"@storybook/react-vite": "7.0.7",
"@storybook/manager-api": "7.0.10",
"@storybook/preview-api": "7.0.10",
"@storybook/react": "7.0.10",
"@storybook/react-vite": "7.0.10",
"@storybook/testing-library": "0.1.0",
"@storybook/theming": "7.0.7",
"@storybook/types": "7.0.7",
"@storybook/vue3": "7.0.7",
"@storybook/vue3-vite": "7.0.7",
"@storybook/theming": "7.0.10",
"@storybook/types": "7.0.10",
"@storybook/vue3": "7.0.10",
"@storybook/vue3-vite": "7.0.10",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/vue": "7.0.0",
"@types/escape-regexp": "0.0.1",
"@types/estree": "1.0.1",
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@types/matter-js": "0.18.2",
"@types/gulp-rename": "2.0.2",
"@types/matter-js": "0.18.3",
"@types/micromatch": "4.0.2",
"@types/node": "18.16.3",
"@types/node": "20.1.3",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "2.9.0",
"@types/seedrandom": "3.0.5",
@ -113,19 +113,19 @@
"@types/uuid": "9.0.1",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.59.2",
"@typescript-eslint/parser": "5.59.2",
"@vitest/coverage-c8": "0.30.1",
"@vue/runtime-core": "3.2.47",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"@vitest/coverage-c8": "0.31.0",
"@vue/runtime-core": "3.3.1",
"astring": "1.8.4",
"chokidar-cli": "3.0.0",
"cross-env": "7.0.3",
"cypress": "12.11.0",
"eslint": "8.39.0",
"cypress": "12.12.0",
"eslint": "8.40.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.11.0",
"eslint-plugin-vue": "9.12.0",
"fast-glob": "3.2.12",
"happy-dom": "9.10.2",
"happy-dom": "9.16.0",
"micromatch": "3.1.10",
"msw": "1.2.1",
"msw-storybook-addon": "1.8.0",
@ -133,13 +133,13 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"start-server-and-test": "2.0.0",
"storybook": "7.0.7",
"storybook": "7.0.10",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.2",
"vitest": "0.30.1",
"vitest": "0.31.0",
"vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.1.1",
"vue-tsc": "1.6.3"
"vue-eslint-parser": "9.2.1",
"vue-tsc": "1.6.4"
}
}

View file

@ -1,144 +0,0 @@
<template>
<div
class="ziffeoms"
:class="{ disabled, checked }"
>
<input
ref="input"
type="checkbox"
:disabled="disabled"
@keydown.enter="toggle"
>
<span ref="button" v-adaptive-border v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle">
<i class="check ti ti-check"></i>
</span>
<span class="label">
<!-- TODO: 無名slotの方は廃止 -->
<span @click="toggle"><slot name="label"></slot><slot></slot></span>
<p class="caption"><slot name="caption"></slot></p>
</span>
</div>
</template>
<script lang="ts" setup>
import { toRefs, Ref } from 'vue';
import * as os from '@/os';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { i18n } from '@/i18n';
const props = defineProps<{
modelValue: boolean | Ref<boolean>;
disabled?: boolean;
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', v: boolean): void;
}>();
let button = $shallowRef<HTMLElement>();
const checked = toRefs(props).modelValue;
const toggle = () => {
if (props.disabled) return;
emit('update:modelValue', !checked.value);
if (!checked.value) {
const rect = button.getBoundingClientRect();
const x = rect.left + (button.offsetWidth / 2);
const y = rect.top + (button.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y, particle: false }, {}, 'end');
}
};
</script>
<style lang="scss" scoped>
.ziffeoms {
position: relative;
display: flex;
transition: all 0.2s ease;
> * {
user-select: none;
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 23px;
height: 23px;
outline: none;
background: var(--panel);
border: solid 1px var(--panel);
border-radius: 4px;
cursor: pointer;
transition: inherit;
> .check {
margin: auto;
opacity: 0;
color: var(--fgOnAccent);
font-size: 13px;
transform: scale(0.5);
transition: all 0.2s ease;
}
}
&:hover {
> .button {
border-color: var(--inputBorderHover) !important;
}
}
> .label {
margin-left: 12px;
margin-top: 2px;
display: block;
transition: inherit;
color: var(--fg);
> span {
display: block;
line-height: 20px;
cursor: pointer;
transition: inherit;
}
> .caption {
margin: 8px 0 0 0;
color: var(--fgTransparentWeak);
font-size: 0.85em;
&:empty {
display: none;
}
}
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--accent) !important;
border-color: var(--accent) !important;
> .check {
opacity: 1;
transform: scale(1);
}
}
}
}
</style>

View file

@ -52,9 +52,12 @@
<MkFoldableSection class="item">
<template #header>Retention rate</template>
<div class="_panel" :class="$style.retention">
<div class="_panel" :class="$style.retentionHeatmap">
<MkRetentionHeatmap/>
</div>
<div class="_panel" :class="$style.retentionLine">
<MkRetentionLineChart/>
</div>
</MkFoldableSection>
<MkFoldableSection class="item">
@ -86,6 +89,7 @@ import { i18n } from '@/i18n';
import MkHeatmap from '@/components/MkHeatmap.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
import { initChart } from '@/scripts/init-chart';
initChart();
@ -202,7 +206,12 @@ onMounted(() => {
margin-bottom: 16px;
}
.retention {
.retentionHeatmap {
padding: 16px;
margin-bottom: 16px;
}
.retentionLine {
padding: 16px;
margin-bottom: 16px;
}

View file

@ -1,15 +1,15 @@
<template>
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
<div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
<div ref="headerEl" class="header">
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
<span class="title">
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
<div ref="headerEl" :class="$style.header">
<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
<span :class="$style.title">
<slot name="header"></slot>
</span>
<button v-if="!withOkButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
<button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
<button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
</div>
<div class="body">
<div :class="$style.body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
@ -81,8 +81,8 @@ defineExpose({
});
</script>
<style lang="scss" scoped>
.ebkgoccj {
<style lang="scss" module>
.root {
margin: auto;
overflow: hidden;
display: flex;
@ -96,51 +96,52 @@ defineExpose({
--root-margin: 16px;
}
> .header {
$height: 46px;
$height-narrow: 42px;
display: flex;
flex-shrink: 0;
background: var(--windowHeader);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
--headerHeight: 46px;
--headerHeightNarrow: 42px;
}
> button {
height: $height;
width: $height;
.header {
display: flex;
flex-shrink: 0;
background: var(--windowHeader);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
@media (max-width: 500px) {
height: $height-narrow;
width: $height-narrow;
}
}
.headerButton {
height: var(--headerHeight);
width: var(--headerHeight);
> .title {
flex: 1;
line-height: $height;
padding-left: 32px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
@media (max-width: 500px) {
line-height: $height-narrow;
padding-left: 16px;
}
}
> button + .title {
padding-left: 0;
}
}
> .body {
flex: 1;
overflow: auto;
background: var(--panel);
container-type: size;
@media (max-width: 500px) {
height: var(--headerHeightNarrow);
width: var(--headerHeightNarrow);
}
}
.title {
flex: 1;
line-height: var(--headerHeight);
padding-left: 32px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
@media (max-width: 500px) {
line-height: var(--headerHeightNarrow);
padding-left: 16px;
}
}
.headerButton + .title {
padding-left: 0;
}
.body {
flex: 1;
overflow: auto;
background: var(--panel);
container-type: size;
}
</style>

View file

@ -1,6 +1,7 @@
<template>
<div :class="[$style.root, { [$style.children]: depth > 1 }]">
<div :class="$style.main">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
<div :class="$style.body">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
@ -62,6 +63,7 @@ if (props.detail) {
.root {
padding: 16px 32px;
font-size: 0.9em;
position: relative;
&.children {
padding: 10px 0 0 16px;
@ -73,6 +75,16 @@ if (props.detail) {
display: flex;
}
.colorBar {
position: absolute;
top: 8px;
left: 8px;
width: 5px;
height: calc(100% - 8px);
border-radius: 999px;
pointer-events: none;
}
.avatar {
flex-shrink: 0;
display: block;

View file

@ -1,8 +1,7 @@
<template>
<div
v-adaptive-border
class="novjtctn"
:class="{ disabled, checked }"
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
@ -10,11 +9,12 @@
<input
type="radio"
:disabled="disabled"
:class="$style.input"
>
<span class="button">
<span :class="$style.button">
<span></span>
</span>
<span class="label"><slot></slot></span>
<span :class="$style.label"><slot></slot></span>
</div>
</template>
@ -39,8 +39,8 @@ function toggle(): void {
}
</script>
<style lang="scss" scoped>
.novjtctn {
<style lang="scss" module>
.root {
position: relative;
display: inline-block;
text-align: left;
@ -53,17 +53,11 @@ function toggle(): void {
border-radius: 6px;
font-size: 90%;
transition: all 0.2s;
> * {
user-select: none;
}
user-select: none;
&.disabled {
opacity: 0.6;
&, * {
cursor: not-allowed !important;
}
cursor: not-allowed !important;
}
&:hover {
@ -74,10 +68,7 @@ function toggle(): void {
background-color: var(--accentedBg) !important;
border-color: var(--accentedBg) !important;
color: var(--accent);
&, * {
cursor: default !important;
}
cursor: default !important;
> .button {
border-color: var(--accent);
@ -89,44 +80,44 @@ function toggle(): void {
}
}
}
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: absolute;
width: 14px;
height: 14px;
background: none;
border: solid 2px var(--inputBorder);
border-radius: 100%;
transition: inherit;
.button {
position: absolute;
width: 14px;
height: 14px;
background: none;
border: solid 2px var(--inputBorder);
border-radius: 100%;
transition: inherit;
&:after {
content: '';
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
> .label {
margin-left: 28px;
&:after {
content: '';
display: block;
line-height: 20px;
cursor: pointer;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
.label {
margin-left: 28px;
display: block;
line-height: 20px;
cursor: pointer;
}
</style>

View file

@ -40,7 +40,7 @@ async function renderChart() {
let raw = await os.api('retention', { });
raw = raw.slice(0, maxDays);
raw = raw.slice(0, maxDays + 1);
const data = [];
for (const record of raw) {
@ -90,8 +90,13 @@ async function renderChart() {
borderRadius: 3,
backgroundColor(c) {
const value = c.dataset.data[c.dataIndex].v;
const a = value / max(c.dataset.data[c.dataIndex].y);
return alpha(color, a);
const m = max(c.dataset.data[c.dataIndex].y);
if (m === 0) {
return alpha(color, 0);
} else {
const a = value / m;
return alpha(color, a);
}
},
fill: true,
width(c) {
@ -129,6 +134,10 @@ async function renderChart() {
autoSkip: false,
callback: (value, index, values) => value,
},
title: {
display: true,
text: 'Days later',
},
},
y: {
type: 'time',
@ -166,7 +175,12 @@ async function renderChart() {
},
label(context) {
const v = context.dataset.data[context.dataIndex];
return [`Active: ${v.v} (${Math.round((v.v / max(v.y)) * 100)}%)`];
const m = max(v.y);
if (m === 0) {
return [`Active: ${v.v} (-%)`];
} else {
return [`Active: ${v.v} (${Math.round((v.v / m) * 100)}%)`];
}
},
},
//mode: 'index',

View file

@ -0,0 +1,130 @@
<template>
<canvas ref="chartEl"></canvas>
</template>
<script lang="ts" setup>
import { onMounted, shallowRef } from 'vue';
import { Chart } from 'chart.js';
import tinycolor from 'tinycolor2';
import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
import * as os from '@/os';
initChart();
const chartEl = shallowRef<HTMLCanvasElement>(null);
const { handler: externalTooltipHandler } = useChartTooltip();
let chartInstance: Chart;
const getYYYYMMDD = (date: Date) => {
const y = date.getFullYear().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const d = date.getDate().toString().padStart(2, '0');
return `${y}/${m}/${d}`;
};
const getDate = (ymd: string) => {
const [y, m, d] = ymd.split('-').map(x => parseInt(x, 10));
const date = new Date(y, m + 1, d, 0, 0, 0, 0);
return date;
};
onMounted(async () => {
let raw = await os.api('retention', { });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
const color = accent.toHex();
chartInstance = new Chart(chartEl.value, {
type: 'line',
data: {
labels: [],
datasets: raw.map((record, i) => ({
label: getYYYYMMDD(new Date(record.createdAt)),
pointRadius: 0,
borderWidth: 2,
borderJoinStyle: 'round',
borderColor: alpha(color, Math.min(1, (raw.length - (i - 1)) / raw.length)),
fill: false,
tension: 0.4,
data: [{
x: '0',
y: 100,
d: getYYYYMMDD(new Date(record.createdAt)),
}, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
x: (i + 1).toString(),
y: (v / record.users) * 100,
d: getYYYYMMDD(new Date(record.createdAt)),
}))],
})),
},
options: {
aspectRatio: 2.5,
layout: {
padding: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
},
scales: {
x: {
title: {
display: true,
text: 'Days later',
},
},
y: {
title: {
display: true,
text: 'Rate (%)',
},
ticks: {
callback: (value, index, values) => value + '%',
},
},
},
interaction: {
intersect: false,
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
callbacks: {
title(context) {
const v = context[0].dataset.data[context[0].dataIndex];
return `${v.x} days later`;
},
label(context) {
const v = context.dataset.data[context.dataIndex];
const p = Math.round(v.y) + '%';
return `${v.d} ${p}`;
},
},
mode: 'index',
animation: {
duration: 0,
},
external: externalTooltipHandler,
},
},
},
plugins: [chartVLine(vLineColor)],
});
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,13 +1,13 @@
<template>
<div class="vblkjoeq">
<div class="label" @click="focus"><slot name="label"></slot></div>
<div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show">
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
<div>
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
<div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show">
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
<select
ref="inputEl"
v-model="v"
v-adaptive-border
class="select"
:class="$style.inputCore"
:disabled="disabled"
:required="required"
:readonly="readonly"
@ -18,9 +18,9 @@
>
<slot></slot>
</select>
<div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
<div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
</div>
<div class="caption"><slot name="caption"></slot></div>
<div :class="$style.caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
@ -169,121 +169,116 @@ function show(ev: MouseEvent) {
}
</script>
<style lang="scss" scoped>
.vblkjoeq {
> .label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
<style lang="scss" module>
.label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
&:empty {
display: none;
&:empty {
display: none;
}
}
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
&:empty {
display: none;
}
}
.input {
position: relative;
cursor: pointer;
&.inline {
display: inline-block;
margin: 0;
}
&.focused {
> .inputCore {
border-color: var(--accent) !important;
//box-shadow: 0 0 0 4px var(--focus);
}
}
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
&.disabled {
opacity: 0.7;
&:empty {
display: none;
&,
> .inputCore {
cursor: not-allowed !important;
}
}
> .input {
position: relative;
cursor: pointer;
&:hover {
> .select {
border-color: var(--inputBorderHover) !important;
}
}
> .select {
appearance: none;
-webkit-appearance: none;
display: block;
height: v-bind("height + 'px'");
width: 100%;
margin: 0;
padding: 0 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
color: var(--fg);
background: var(--panel);
border: solid 1px var(--panel);
border-radius: 6px;
outline: none;
box-shadow: none;
box-sizing: border-box;
cursor: pointer;
transition: border-color 0.1s ease-out;
pointer-events: none;
user-select: none;
}
> .prefix,
> .suffix {
display: flex;
align-items: center;
position: absolute;
z-index: 1;
top: 0;
padding: 0 12px;
font-size: 1em;
height: v-bind("height + 'px'");
pointer-events: none;
&:empty {
display: none;
}
> * {
display: inline-block;
min-width: 16px;
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
> .prefix {
left: 0;
padding-right: 6px;
}
> .suffix {
right: 0;
padding-left: 6px;
}
&.inline {
display: inline-block;
margin: 0;
}
&.focused {
> select {
border-color: var(--accent) !important;
}
}
&.disabled {
opacity: 0.7;
&, * {
cursor: not-allowed !important;
}
&:hover {
> .inputCore {
border-color: var(--inputBorderHover) !important;
}
}
}
</style>
<style lang="scss" module>
.inputCore {
appearance: none;
-webkit-appearance: none;
display: block;
height: v-bind("height + 'px'");
width: 100%;
margin: 0;
padding: 0 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
color: var(--fg);
background: var(--panel);
border: solid 1px var(--panel);
border-radius: 6px;
outline: none;
box-shadow: none;
box-sizing: border-box;
transition: border-color 0.1s ease-out;
cursor: pointer;
pointer-events: none;
user-select: none;
}
.prefix,
.suffix {
display: flex;
align-items: center;
position: absolute;
z-index: 1;
top: 0;
padding: 0 12px;
font-size: 1em;
height: v-bind("height + 'px'");
min-width: 16px;
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
box-sizing: border-box;
pointer-events: none;
&:empty {
display: none;
}
}
.prefix {
left: 0;
padding-right: 6px;
}
.suffix {
right: 0;
padding-left: 6px;
}
.chevron {
transition: transform 0.1s ease-out;
}

View file

@ -1,21 +1,19 @@
<template>
<div
class="ziffeomt"
:class="{ disabled, checked }"
>
<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]">
<input
ref="input"
type="checkbox"
:disabled="disabled"
:class="$style.input"
@keydown.enter="toggle"
>
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle">
<div class="knob"></div>
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
<div :class="$style.knob"></div>
</span>
<span class="label">
<span :class="$style.body">
<!-- TODO: 無名slotの方は廃止 -->
<span @click="toggle"><slot name="label"></slot><slot></slot></span>
<p class="caption"><slot name="caption"></slot></p>
<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
<p :class="$style.caption"><slot name="caption"></slot></p>
</span>
</div>
</template>
@ -45,52 +43,12 @@ const toggle = () => {
};
</script>
<style lang="scss" scoped>
.ziffeomt {
<style lang="scss" module>
.root {
position: relative;
display: flex;
transition: all 0.2s ease;
> * {
user-select: none;
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 32px;
height: 23px;
outline: none;
background: var(--switchOffBg);
background-clip: content-box;
border: solid 1px var(--switchOffBg);
border-radius: 999px;
cursor: pointer;
transition: inherit;
user-select: none;
> .knob {
position: absolute;
top: 3px;
left: 3px;
width: 15px;
height: 15px;
background: var(--switchOffFg);
border-radius: 999px;
transition: all 0.2s ease;
}
}
user-select: none;
&:hover {
> .button {
@ -98,31 +56,6 @@ const toggle = () => {
}
}
> .label {
margin-left: 12px;
margin-top: 2px;
display: block;
transition: inherit;
color: var(--fg);
> span {
display: block;
line-height: 20px;
cursor: pointer;
transition: inherit;
}
> .caption {
margin: 8px 0 0 0;
color: var(--fgTransparentWeak);
font-size: 0.85em;
&:empty {
display: none;
}
}
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
@ -140,4 +73,66 @@ const toggle = () => {
}
}
}
.input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 32px;
height: 23px;
outline: none;
background: var(--switchOffBg);
background-clip: content-box;
border: solid 1px var(--switchOffBg);
border-radius: 999px;
cursor: pointer;
transition: inherit;
user-select: none;
}
.knob {
position: absolute;
top: 3px;
left: 3px;
width: 15px;
height: 15px;
background: var(--switchOffFg);
border-radius: 999px;
transition: all 0.2s ease;
}
.body {
margin-left: 12px;
margin-top: 2px;
display: block;
transition: inherit;
color: var(--fg);
}
.label {
display: block;
line-height: 20px;
cursor: pointer;
transition: inherit;
}
.caption {
margin: 8px 0 0 0;
color: var(--fgTransparentWeak);
font-size: 0.85em;
&:empty {
display: none;
}
}
</style>

View file

@ -40,10 +40,6 @@ import * as os from '@/os';
import { $i } from '@/account';
import MkPagination from '@/components/MkPagination.vue';
const emit = defineEmits<{
(ev: 'done'): void;
}>();
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {

View file

@ -0,0 +1,31 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import MkUserSetupDialog_Privacy from './MkUserSetupDialog.Privacy.vue';
export const Default = {
render(args) {
return {
components: {
MkUserSetupDialog_Privacy,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkUserSetupDialog_Privacy v-bind="props" />',
};
},
args: {
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkUserSetupDialog_Privacy>;

View file

@ -0,0 +1,64 @@
<template>
<div class="_gaps">
<MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
<MkFolder>
<template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
<template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts.hideOnlineStatus }}</template>
<template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts.noCrawle }}</template>
<template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts.preventAiLearning }}</template>
<template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
</MkFolder>
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os';
import { $i } from '@/account';
let isLocked = ref(false);
let hideOnlineStatus = ref(false);
let noCrawle = ref(false);
let preventAiLearning = ref(true);
watch([isLocked, hideOnlineStatus, noCrawle, preventAiLearning], () => {
os.api('i/update', {
isLocked: !!isLocked.value,
hideOnlineStatus: !!hideOnlineStatus.value,
noCrawle: !!noCrawle.value,
preventAiLearning: !!preventAiLearning.value,
});
});
</script>
<style lang="scss" module>
</style>

View file

@ -37,10 +37,6 @@ import { chooseFileFromPc } from '@/scripts/select-file';
import * as os from '@/os';
import { $i } from '@/account';
const emit = defineEmits<{
(ev: 'done'): void;
}>();
const name = ref('');
const description = ref('');

View file

@ -7,9 +7,17 @@
@close="close(true)"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.initialAccountSetting }}</template>
<template v-if="page === 1" #header>{{ i18n.ts._initialAccountSetting.profileSetting }}</template>
<template v-else-if="page === 2" #header>{{ i18n.ts._initialAccountSetting.privacySetting }}</template>
<template v-else-if="page === 3" #header>{{ i18n.ts.follow }}</template>
<template v-else-if="page === 4" #header>{{ i18n.ts.pushNotification }}</template>
<template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
<div style="overflow-x: clip;">
<div :class="$style.progressBar">
<div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div>
</div>
<Transition
mode="out-in"
:enter-active-class="$style.transition_x_enterActive"
@ -40,12 +48,22 @@
<template v-else-if="page === 2">
<div style="height: 100cqh; overflow: auto;">
<MkSpacer :margin-min="20" :margin-max="28">
<XFollow/>
<XPrivacy/>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 3">
<div style="height: 100cqh; overflow: auto;">
<MkSpacer :margin-min="20" :margin-max="28">
<XFollow/>
</MkSpacer>
<div :class="$style.pageFooter">
<MkButton primary rounded gradate style="margin: 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</template>
<template v-else-if="page === 4">
<div :class="$style.centerPage">
<MkSpacer :margin-min="20" :margin-max="28">
<div class="_gaps" style="text-align: center;">
@ -58,7 +76,7 @@
</MkSpacer>
</div>
</template>
<template v-else-if="page === 4">
<template v-else-if="page === 5">
<div :class="$style.centerPage">
<MkSpacer :margin-min="20" :margin-max="28">
<div class="_gaps" style="text-align: center;">
@ -87,6 +105,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { host } from '@/config';
@ -134,6 +153,21 @@ async function close(skip: boolean) {
transform: translateX(-50px);
}
.progressBar {
position: absolute;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 4px;
}
.progressBarValue {
height: 100%;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
transition: all 0.5s cubic-bezier(0,.5,.5,1);
}
.centerPage {
display: flex;
justify-content: center;
@ -142,4 +176,14 @@ async function close(skip: boolean) {
padding-bottom: 30px;
box-sizing: border-box;
}
.pageFooter {
position: sticky;
bottom: 0;
left: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View file

@ -32,6 +32,7 @@
<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
</div>
</template>
<script lang="ts">
export type Widget = {
name: string;
@ -42,6 +43,7 @@ export type DefaultStoredWidget = {
place: string | null;
} & Widget;
</script>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { v4 as uuid } from 'uuid';

View file

@ -2,7 +2,7 @@
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
<img :class="$style.inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
<div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]">
<div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft">
<div v-if="false" :class="$style.layer">
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
@ -154,24 +154,6 @@ watch(() => props.user.avatarBlurhash, () => {
padding: 50%;
pointer-events: none;
&.mask {
-webkit-mask:
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') center / 50% 50%,
linear-gradient(#fff, #fff);
-webkit-mask-composite: destination-out, source-over;
mask:
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') exclude center / 50% 50%,
linear-gradient(#fff, #fff); // polyfill of `image(#fff)`
> .earLeft {
animation: eartightleft 6s infinite;
}
> .earRight {
animation: eartightright 6s infinite;
}
}
> .earLeft,
> .earRight {
contain: strict;

View file

@ -1,6 +1,6 @@
<template>
<div class="lzyxtsnt">
<ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
<div>
<ImgWithBlurhash v-if="image" style="max-width: 100%;" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
</div>
</template>
@ -17,11 +17,3 @@ const props = defineProps<{
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
</script>
<style lang="scss" scoped>
.lzyxtsnt {
> img {
max-width: 100%;
}
}
</style>

View file

@ -238,6 +238,7 @@ const patrons = [
'ずも',
'binvinyl',
'渡志郎',
'ぷーざ',
];
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));

View file

@ -27,7 +27,7 @@
<MkTextarea v-model="sensitiveWords">
<template #label>{{ i18n.ts.sensitiveWords }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
</div>
</FormSuspense>

View file

@ -46,7 +46,7 @@
</MkInput>
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
</div>
<MkNotes v-if="searchPagination" :key="searchQuery" :pagination="searchPagination"/>
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
</div>
</div>
</MkSpacer>
@ -93,6 +93,7 @@ let channel = $ref(null);
let favorited = $ref(false);
let searchQuery = $ref('');
let searchPagination = $ref();
let searchKey = $ref('');
const featuredPagination = $computed(() => ({
endpoint: 'notes/featured' as const,
limit: 10,
@ -149,10 +150,12 @@ async function search() {
endpoint: 'notes/search',
limit: 10,
params: {
query: searchQuery,
query: query,
channelId: channel.id,
},
};
searchKey = query;
}
const headerActions = $computed(() => {

View file

@ -28,9 +28,9 @@
<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
<div class="vxjfqztj">
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`">{{ tag.tag }}</MkA>
<div>
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px; font-weight: bold;">{{ tag.tag }}</MkA>
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px;">{{ tag.tag }}</MkA>
</div>
</MkFoldableSection>
@ -132,15 +132,3 @@ os.api('hashtags/list', {
tagsRemote = tags;
});
</script>
<style lang="scss" scoped>
.vxjfqztj {
> * {
margin-right: 16px;
&.local {
font-weight: bold;
}
}
}
</style>

View file

@ -37,7 +37,7 @@ async function choose() {
file = fileResponse[0];
emit('update:modelValue', {
...props.modelValue,
fileId: fileResponse.id,
fileId: file.id,
});
});
}

View file

@ -0,0 +1,98 @@
<template>
<div class="_gaps">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkFolder>
<template #label>{{ i18n.ts.options }}</template>
<MkFolder>
<template #label>{{ i18n.ts.specifyUser }}</template>
<template v-if="user" #suffix>@{{ user.username }}</template>
<div style="text-align: center;" class="_gaps">
<div v-if="user">@{{ user.username }}</div>
<div>
<MkButton v-if="user == null" primary rounded inline @click="selectUser">{{ i18n.ts.selectUser }}</MkButton>
<MkButton v-else danger rounded inline @click="user = null">{{ i18n.ts.remove }}</MkButton>
</div>
</div>
</MkFolder>
</MkFolder>
<div>
<MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton>
</div>
</div>
<MkFoldableSection v-if="notePagination">
<template #header>{{ i18n.ts.searchResult }}</template>
<MkNotes :key="key" :pagination="notePagination"/>
</MkFoldableSection>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue';
import MkNotes from '@/components/MkNotes.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n';
import * as os from '@/os';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { $i } from '@/account';
import { instance } from '@/instance';
import MkInfo from '@/components/MkInfo.vue';
import { useRouter } from '@/router';
import MkFolder from '@/components/MkFolder.vue';
const router = useRouter();
let key = $ref(0);
let searchQuery = $ref('');
let searchOrigin = $ref('combined');
let notePagination = $ref();
let user = $ref(null);
function selectUser() {
os.selectUser().then(_user => {
user = _user;
});
}
async function search() {
const query = searchQuery.toString().trim();
if (query == null || query === '') return;
if (query.startsWith('https://')) {
const promise = os.api('ap/show', {
uri: query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise;
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
}
return;
}
notePagination = {
endpoint: 'notes/search',
limit: 10,
params: {
query: searchQuery,
userId: user ? user.id : null,
},
};
key++;
}
</script>

View file

@ -0,0 +1,77 @@
<template>
<div class="_gaps">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkRadios v-model="searchOrigin" @update:model-value="search()">
<option value="combined">{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.local }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
</MkRadios>
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
<MkFoldableSection v-if="userPagination">
<template #header>{{ i18n.ts.searchResult }}</template>
<MkUserList :key="key" :pagination="userPagination"/>
</MkFoldableSection>
</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, onMounted } from 'vue';
import MkUserList from '@/components/MkUserList.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n';
import * as os from '@/os';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { $i } from '@/account';
import { instance } from '@/instance';
import MkInfo from '@/components/MkInfo.vue';
import { useRouter } from '@/router';
const router = useRouter();
let key = $ref('');
let searchQuery = $ref('');
let searchOrigin = $ref('combined');
let userPagination = $ref();
async function search() {
const query = searchQuery.toString().trim();
if (query == null || query === '') return;
if (query.startsWith('https://')) {
const promise = os.api('ap/show', {
uri: query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise;
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
}
return;
}
userPagination = {
endpoint: 'users/search',
limit: 10,
params: {
query: searchQuery,
origin: searchOrigin,
},
};
key = query;
}
</script>

View file

@ -1,133 +1,38 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="tab === 'note'" :content-max="800">
<div v-if="notesSearchAvailable" class="_gaps">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
<MkFoldableSection v-if="notePagination">
<template #header>{{ i18n.ts.searchResult }}</template>
<MkNotes :key="key" :pagination="notePagination"/>
</MkFoldableSection>
<MkSpacer v-if="tab === 'note'" :content-max="800">
<div v-if="notesSearchAvailable">
<XNote/>
</div>
<div v-else>
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
</div>
</MkSpacer>
<MkSpacer v-else-if="tab === 'user'" :content-max="800">
<div class="_gaps">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkRadios v-model="searchOrigin" @update:model-value="search()">
<option value="combined">{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.local }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
</MkRadios>
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
<MkFoldableSection v-if="userPagination">
<template #header>{{ i18n.ts.searchResult }}</template>
<MkUserList :key="key" :pagination="userPagination"/>
</MkFoldableSection>
</div>
<MkSpacer v-else-if="tab === 'user'" :content-max="800">
<XUser/>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue';
import MkNotes from '@/components/MkNotes.vue';
import MkUserList from '@/components/MkUserList.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import { computed, defineAsyncComponent, onMounted } from 'vue';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import * as os from '@/os';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { $i } from '@/account';
import { instance } from '@/instance';
import MkInfo from '@/components/MkInfo.vue';
import { useRouter } from '@/router';
const router = useRouter();
const XNote = defineAsyncComponent(() => import('./search.note.vue'));
const XUser = defineAsyncComponent(() => import('./search.user.vue'));
const props = defineProps<{
query: string;
channel?: string;
type?: string;
origin?: string;
}>();
let key = $ref('');
let tab = $ref('note');
let searchQuery = $ref('');
let searchOrigin = $ref('combined');
let notePagination = $ref();
let userPagination = $ref();
const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes));
onMounted(() => {
tab = props.type ?? 'note';
searchQuery = props.query ?? '';
searchOrigin = props.origin ?? 'combined';
});
async function search() {
const query = searchQuery.toString().trim();
if (query == null || query === '') return;
if (query.startsWith('https://')) {
const promise = os.api('ap/show', {
uri: query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise;
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
}
return;
}
if (tab === 'note') {
notePagination = {
endpoint: 'notes/search',
limit: 10,
params: {
query: searchQuery,
channelId: props.channel,
},
};
} else if (tab === 'user') {
userPagination = {
endpoint: 'users/search',
limit: 10,
params: {
query: searchQuery,
origin: searchOrigin,
},
};
}
key = query;
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => [{
@ -141,7 +46,7 @@ const headerTabs = $computed(() => [{
}]);
definePageMetadata(computed(() => ({
title: searchQuery ? i18n.t('searchWith', { q: searchQuery }) : i18n.ts.search,
title: i18n.ts.search,
icon: 'ti ti-search',
})));
</script>

View file

@ -24,6 +24,10 @@
{{ i18n.ts.noCrawle }}
<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
</MkSwitch>
<MkSwitch v-model="preventAiLearning" @update:model-value="save()">
{{ i18n.ts.preventAiLearning }}<span class="_beta">{{ i18n.ts.beta }}</span>
<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
</MkSwitch>
<MkSwitch v-model="isExplorable" @update:model-value="save()">
{{ i18n.ts.makeExplorable }}
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
@ -71,6 +75,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
let isLocked = $ref($i.isLocked);
let autoAcceptFollowed = $ref($i.autoAcceptFollowed);
let noCrawle = $ref($i.noCrawle);
let preventAiLearning = $ref($i.preventAiLearning);
let isExplorable = $ref($i.isExplorable);
let hideOnlineStatus = $ref($i.hideOnlineStatus);
let publicReactions = $ref($i.publicReactions);
@ -86,6 +91,7 @@ function save() {
isLocked: !!isLocked,
autoAcceptFollowed: !!autoAcceptFollowed,
noCrawle: !!noCrawle,
preventAiLearning: !!preventAiLearning,
isExplorable: !!isExplorable,
hideOnlineStatus: !!hideOnlineStatus,
publicReactions: !!publicReactions,

View file

@ -24,9 +24,9 @@
"@swc/jest": "0.2.26",
"@types/jest": "29.5.1",
"@types/node": "18.16.3",
"@typescript-eslint/eslint-plugin": "5.59.2",
"@typescript-eslint/parser": "5.59.2",
"eslint": "8.39.0",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"eslint": "8.40.0",
"jest": "29.5.0",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.4.0",

View file

@ -14,9 +14,9 @@
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "5.59.2",
"@typescript-eslint/parser": "5.59.5",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
"eslint": "8.39.0",
"eslint": "8.40.0",
"eslint-plugin-import": "2.27.5",
"typescript": "5.0.4"
}

File diff suppressed because it is too large Load diff