Merge branch 'develop' into emoji-re

This commit is contained in:
tamaina 2023-01-22 12:07:38 +00:00
commit a8b19f4aa8
113 changed files with 5114 additions and 565 deletions

View file

@ -16,16 +16,16 @@ jobs:
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: misskey/misskey images: misskey/misskey
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub - name: Build and Push to Docker Hub
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: true

View file

@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: misskey/misskey images: misskey/misskey
tags: | tags: |
@ -26,12 +26,12 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub - name: Build and Push to Docker Hub
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: true

View file

@ -9,6 +9,56 @@
You should also include the user name that made the change. You should also include the user name that made the change.
--> -->
## 13.1.7 (2023/01/22)
### Improvements
- 新たな実績を追加
- MFMにscaleタグを追加
## 13.1.4 (2023/01/22)
### Improvements
- 新たな実績を追加
### Bugfixes
- Client: ローカリゼーション更新時にリロードが繰り返されることがあるのを修正
## 13.1.3 (2023/01/22)
### Bugfixes
- Client: リアクションのカスタム絵文字の表示の問題を修正
## 13.1.2 (2023/01/22)
### Bugfixes
- Client: リアクションのカスタム絵文字の表示の問題を修正
## 13.1.1 (2023/01/22)
### Improvements
- ローカルのカスタム絵文字を表示する際のパフォーマンスを改善
- Client: 瞬間的に大量の実績を解除した際の挙動を改善
### Bugfixes
- Client: アップデート時にローカリゼーションデータが更新されないことがあるのを修正
## 13.1.0 (2023/01/21)
### Improvements
- 実績機能
- Playのプリセットを追加
- Playのscriptの文字数制限を緩和
- AiScript GUIの強化
- リアクション一覧詳細ダイアログを表示できるように
- 存在しないカスタム絵文字をテキストで表示するように
- Alt text in image viewer
- ジョブキューのプロセスとWebサーバーのプロセスを分離
### Bugfixes
- playを削除する手段がなかったのを修正
- The … button on notes does nothing when not logged in
- twitterと連携するときに autwh is not a function になるのを修正
## 13.0.0 (2023/01/16) ## 13.0.0 (2023/01/16)
### TL;DR ### TL;DR
@ -29,16 +79,20 @@ You should also include the user name that made the change.
- Node.js 18.x or later is required - Node.js 18.x or later is required
- PostgreSQL 15.x is required - PostgreSQL 15.x is required
- Misskey not using 15 specific features at 13.0.0, but may do so in the future. - Misskey not using 15 specific features at 13.0.0, but may do so in the future.
- Docker環境でPostgreSQLのアップデートを行う際のガイドはこちら: https://github.com/misskey-dev/misskey/pull/9641#issue-1536336620
- Elasticsearchのサポートが削除されました - Elasticsearchのサポートが削除されました
- 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます - 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます
- Yarnからpnpmに移行されました - Yarnからpnpmに移行されました
corepackの有効化を推奨します: `sudo corepack enable`
- インスタンスブロックはサブドメインにも適用されるようになります - インスタンスブロックはサブドメインにも適用されるようになります
- ロールの導入に伴い、いくつかの機能がロールと統合されました - ロールの導入に伴い、いくつかの機能がロールと統合されました
- モデレーターはロールに統合されました。今までのモデレーター情報は失われるため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。 - モデレーターはロールに統合されました。今までのモデレーター情報は失われるため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。
- サイレンスはロールに統合されました。今までのユーザーは恩赦されるため、予めサイレンス一覧を記録しておくのをおすすめします。 - サイレンスはロールに統合されました。今までのユーザーは恩赦されるため、予めサイレンス一覧を記録しておくのをおすすめします。
- ユーザーごとのドライブ容量設定はロールに統合されました。 - ユーザーごとのドライブ容量設定はロールに統合されました。
- インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールドライブ容量を編集してください。 - インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールもしくはコンディショナルロールでドライブ容量を編集してください。
- LTL/GTLの解放状態はロールに統合されました。 - LTL/GTLの解放状態はロールに統合されました。
- Dockerの実行をrootで行わないようにしました。Dockerかつオブジェクトストレージを使用していない場合は`chown -hR 991.991 ./files`を実行してください。
https://github.com/misskey-dev/misskey/pull/9560
#### For users #### For users
- ノートのウォッチ機能が削除されました - ノートのウォッチ機能が削除されました

View file

@ -6,6 +6,8 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
build-essential build-essential
RUN corepack enable
WORKDIR /misskey WORKDIR /misskey
COPY ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] COPY ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
@ -14,7 +16,6 @@ COPY ["packages/backend/package.json", "./packages/backend/"]
COPY ["packages/frontend/package.json", "./packages/frontend/"] COPY ["packages/frontend/package.json", "./packages/frontend/"]
COPY ["packages/sw/package.json", "./packages/sw/"] COPY ["packages/sw/package.json", "./packages/sw/"]
RUN npm i -g pnpm
RUN pnpm i --frozen-lockfile RUN pnpm i --frozen-lockfile
COPY . ./ COPY . ./
@ -34,10 +35,10 @@ RUN apt-get update \
ffmpeg tini \ ffmpeg tini \
&& apt-get -y clean \ && apt-get -y clean \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& corepack enable \
&& groupadd -g "${GID}" misskey \ && groupadd -g "${GID}" misskey \
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey
RUN npm i -g pnpm
USER misskey USER misskey
WORKDIR /misskey WORKDIR /misskey

View file

@ -24,7 +24,7 @@ services:
redis: redis:
restart: always restart: always
image: redis:4.0-alpine image: redis:7-alpine
networks: networks:
- internal_network - internal_network
volumes: volumes:
@ -36,7 +36,7 @@ services:
db: db:
restart: always restart: always
image: postgres:12.2-alpine image: postgres:15-alpine
networks: networks:
- internal_network - internal_network
env_file: env_file:

View file

@ -108,6 +108,7 @@ clickToShow: "اضغط للعرض"
sensitive: "محتوى حساس" sensitive: "محتوى حساس"
add: "إضافة" add: "إضافة"
reaction: "التفاعلات" reaction: "التفاعلات"
reactions: "التفاعلات"
reactionSetting: "التفاعلات المراد عرضها في منتقي التفاعلات." reactionSetting: "التفاعلات المراد عرضها في منتقي التفاعلات."
reactionSettingDescription2: "اسحب لترتيب ، انقر للحذف ، استخدم \"+\" للإضافة." reactionSettingDescription2: "اسحب لترتيب ، انقر للحذف ، استخدم \"+\" للإضافة."
rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات" rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات"

View file

@ -107,6 +107,7 @@ clickToShow: "দেখার জন্য ক্লিক করুন"
sensitive: "সংবেদনশীল বিষয়বস্তু" sensitive: "সংবেদনশীল বিষয়বস্তু"
add: "যুক্ত করুন" add: "যুক্ত করুন"
reaction: "প্রতিক্রিয়া" reaction: "প্রতিক্রিয়া"
reactions: "প্রতিক্রিয়া"
reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে" reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে"
reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।" reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।"
rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন" rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন"

View file

@ -108,6 +108,7 @@ clickToShow: "Fes clic per mostrar"
sensitive: "NSFW" sensitive: "NSFW"
add: "Afegir" add: "Afegir"
reaction: "Reaccions" reaction: "Reaccions"
reactions: "Reaccions"
reactionSetting: "Reaccions a mostrar al selector de reaccions" reactionSetting: "Reaccions a mostrar al selector de reaccions"
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir." reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes" rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"

View file

@ -105,6 +105,7 @@ clickToShow: "Klikněte pro zobrazení"
sensitive: "NSFW" sensitive: "NSFW"
add: "Přidat" add: "Přidat"
reaction: "Reakce" reaction: "Reakce"
reactions: "Reakce"
reactionSettingDescription2: "Přetažením změníte pořadí, kliknutím smažete, zmáčkněte \"+\" k přidání" reactionSettingDescription2: "Přetažením změníte pořadí, kliknutím smažete, zmáčkněte \"+\" k přidání"
rememberNoteVisibility: "Zapamatovat nastavení zobrazení poznámky" rememberNoteVisibility: "Zapamatovat nastavení zobrazení poznámky"
attachCancel: "Odstranit přílohu" attachCancel: "Odstranit přílohu"

View file

@ -110,6 +110,7 @@ clickToShow: "Zum Anzeigen anklicken"
sensitive: "NSFW" sensitive: "NSFW"
add: "Hinzufügen" add: "Hinzufügen"
reaction: "Reaktionen" reaction: "Reaktionen"
reactions: "Reaktionen"
reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen" reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen"
reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen" reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen"
rememberNoteVisibility: "Notizsichtbarkeit merken" rememberNoteVisibility: "Notizsichtbarkeit merken"
@ -931,10 +932,243 @@ undefined: "Undefiniert"
assign: "Zuweisen" assign: "Zuweisen"
unassign: "Entfernen" unassign: "Entfernen"
color: "Farbe" color: "Farbe"
manageCustomEmojis: "Benutzerdefinierte Emojis verwalten" manageCustomEmojis: "Kann benutzerdefinierte Emojis verwalten"
youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht." youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht."
cannotPerformTemporary: "Vorübergehend nicht verfügbar" cannotPerformTemporary: "Vorübergehend nicht verfügbar"
cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut." cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut."
preset: "Vorlage"
selectFromPresets: "Aus Vorlagen wählen"
achievements: "Errungenschaften"
_achievements:
earnedAt: "Freigeschaltet am"
_types:
_notes1:
title: "Hallo Misskey!"
description: "Sende deine erste Notiz"
flavor: "Hab eine schöne Zeit mit Misskey!"
_notes10:
title: "Ein paar Notizen"
description: "10 Notizen gesendet"
_notes100:
title: "Viele Notizen"
description: "100 Notizen gesendet"
_notes500:
title: "Überschüttet mit Notizen"
description: "500 Notizen gesendet"
_notes1000:
title: "Berg an Notizen"
description: "1.000 Notizen gesendet"
_notes5000:
title: "Überquellende Notizen"
description: "5.000 Notizen gesendet"
_notes10000:
title: "Supernotiz"
description: "10.000 Notizen gesendet"
_notes20000:
title: "Brauche... mehr... Notizen"
description: "20.000 Notizen gesendet"
_notes30000:
title: "Notizen, Notizen, Notizen"
description: "30.000 Notizen gesendet"
_notes40000:
title: "Notizfabrik"
description: "40.000 Notizen gesendet"
_notes50000:
title: "Planet der Notizen"
description: "50.000 Notizen gesendet"
_notes60000:
title: "Notizquasar"
description: "60.000 Notizen gesendet"
_notes70000:
title: "Schwarzes Notizloch"
description: "70.000 Notizen gesendet"
_notes80000:
title: "Notizgalaxie"
description: "80.000 Notizen gesendet"
_notes90000:
title: "Notizversum"
description: "90.000 Notizen gesendet"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "100.000 Notizen gesendet"
flavor: "Du hast wirklich viel zu sagen."
_login3:
title: "Anfänger "
description: "An 3 Tagen eingeloggt"
flavor: "Nenn' mich ab heute Misskist"
_login7:
title: "Anfänger Ⅱ"
description: "An 7 Tagen eingeloggt"
flavor: "Na, eingewöht?"
_login15:
title: "Anfänger Ⅲ"
description: "An 15 Tagen eingeloggt"
_login30:
title: "Misskist "
description: "An 30 Tagen eingeloggt"
_login60:
title: "Misskist Ⅱ"
description: "An 60 Tagen eingeloggt"
_login100:
title: "Misskist Ⅲ"
description: "An 100 Tagen eingeloggt"
flavor: "Violent Misskist"
_login200:
title: "Stammbesucher "
description: "An 200 Tagen eingeloggt"
_login300:
title: "Stammbesucher Ⅱ"
description: "An 300 Tagen eingeloggt"
_login400:
title: "Stammbesucher Ⅲ"
description: "An 400 Tagen eingeloggt"
_login500:
title: "Veteran "
description: "An 500 Tagen eingeloggt"
flavor: "Meine Kameraden, ich liebe sie, die Notizen."
_login600:
title: "Veteran Ⅱ"
description: "An 600 Tagen eingeloggt"
_login700:
title: "Veteran Ⅲ"
description: "An 700 Tagen eingeloggt"
_login800:
title: "Meister der Notizen "
description: "An 800 Tagen eingeloggt"
_login900:
title: "Meister der Notizen Ⅱ"
description: "An 900 Tagen eingeloggt"
_login1000:
title: "Meister der Notizen Ⅲ"
description: "An 1000 Tagen eingeloggt"
flavor: "Danke, dass du Misskey nutzt!"
_noteClipped1:
title: "Muss... clippen..."
description: "Die erste Notiz geclippt"
_noteFavorited1:
title: "Sternengucker"
description: "Eine Notiz als Favorit markiert"
_myNoteFavorited1:
title: "Sternensucher"
description: "Ein anderer Benutzer hat eine deiner Notizen als Favoriten markiert"
_profileFilled:
title: "Perfekte Vorbereitung"
description: "Fülle dein Profil aus"
_markedAsCat:
title: "Ich der Kater"
description: "Markiere dein Konto als Katze"
flavor: "Einen Namen bekommst du später. "
_following1:
title: "Das Folgen beginnt"
description: "Du folgst deiner ersten Person"
_following10:
title: "Folge ihnen... folge ihnen..."
description: "Du folgst über 10 Leuten"
_following50:
title: "Viele Freunde"
description: "Du folgst über 50 Leuten"
_following100:
title: "100 Freunde"
description: "Du folgst über 100 Leuten"
_following300:
title: "Freundeüberschuss"
description: "Du folgst über 300 Leuten"
_followers1:
title: "Der erste Follower"
description: "Du hast deinen ersten Follower erhalten"
_followers10:
title: "Mir nach!"
description: "Die Anzahl deiner Follower hat 10 überschritten"
_followers50:
title: "Wirrwarr"
description: "Die Anzahl deiner Follower hat 50 überschritten"
_followers100:
title: "Beliebt"
description: "Die Anzahl deiner Follower hat 100 überschritten"
_followers300:
title: "Stellt euch bitte in einer Reihe auf"
description: "Die Anzahl deiner Follower hat 300 überschritten"
_followers500:
title: "Funkmast"
description: "Die Anzahl deiner Follower hat 500 überschritten"
_followers1000:
title: "Influencer"
description: "Die Anzahl deiner Follower hat 1000 überschritten"
_collectAchievements30:
title: "Sammler der Errungenschaften"
description: "Schalte 30 Errungenschaften frei"
_viewAchievements3min:
title: "Fan von Errungenschaften"
description: "Schau dir die Liste deiner Errungenschaften für mindestens 3 Minuten an"
_iLoveMisskey:
title: "I Love Misskey"
description: "Sende \"I ❤ #Misskey\""
flavor: "Danke, dass du Misskey verwendest! - vom Entwicklerteam"
_client30min:
title: "Kleine Pause"
description: "Seit dem Öffnen deines Clients sind 30 Minuten vergangen"
_noteDeletedWithin1min:
title: "Ups"
description: "Lösche eine Notiz innerhalb von 1 Minute nachdem sie gesendet wurde"
_postedAtLateNight:
title: "Nachtaktiv"
description: "Sende mitten in der Nacht eine Notiz"
flavor: "Geh bald schlafen."
_postedAt0min0sec:
title: "Zeitansage"
description: "Sende um 00:00 eine Notiz"
flavor: "Klick Klick Klick Dooong"
_selfQuote:
title: "Selbstzitat"
description: "Zitiere eine eigene Notiz"
_htl20npm:
title: "Fließende Chronik"
description: "Deine Startseitenchronik erreicht eine Geschwindigkeit von 20 npm (Notizen pro Minute)"
_outputHelloWorldOnScratchpad:
title: "Hallo Welt!"
description: "Gib \"hello world\" in der Testumgebung aus"
_open3windows:
title: "Splitscreen"
description: "Habe zur gleichen Zeit mindestens 3 Fenster offen"
_driveFolderCircularReference:
title: "Zyklischer Verweis"
description: "Versuche, in Drive einen Zirkelbezug von Ordnern herzustellen"
_reactWithoutRead:
title: "Hast du das wirklich gelesen?"
description: "Reagiere auf eine Notiz mit mindestens 100 Zeichen innerhalb von 3 Sekunden der Erstellung der Notiz"
_clickedClickHere:
title: "Klicke hier"
description: "Du hast hier geklickt"
_justPlainLucky:
title: "Pures Glück"
description: "Kann alle 10 Sekunden mit einer Warscheinlichkeit von 0.01% erhalten werden"
_setNameToSyuilo:
title: "Gottkomplex"
description: "Setze deinen Namen auf \"syuilo\""
_passedSinceAccountCreated1:
title: "Einjahresjubiläum"
description: "Seit der Erstellung deines Kontos ist 1 Jahr vergangen"
_passedSinceAccountCreated2:
title: "Zweijahresjubiläum"
description: "Seit der Erstellung deines Kontos sind 2 Jahre vergangen"
_passedSinceAccountCreated3:
title: "Dreijahresjubiläum"
description: "Seit der Erstellung deines Kontos sind 3 Jahre vergangen"
_loggedInOnBirthday:
title: "Alles Gute Zum Geburtstag"
description: "Logge dich an deinem Geburtstag ein"
_loggedInOnNewYearsDay:
title: "Frohes Neujahr"
description: "Logge dich am Neujahrstag ein"
flavor: "Auf ein weiteres tolles Jahr in dieser Instanz"
_cookieClicked:
title: "Ein Spiel, in dem du auf einen Keks klickst"
description: "Den Keks geklickt"
flavor: "Bist du hier richtig?"
_brainDiver:
title: "Brain Diver"
description: "Sende den Link zu Brain Diver"
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "Rolle erstellen" new: "Rolle erstellen"
edit: "Rolle bearbeiten" edit: "Rolle bearbeiten"
@ -943,7 +1177,7 @@ _role:
permission: "Rollenberechtigungen" permission: "Rollenberechtigungen"
descriptionOfPermission: "<b>Moderatoren</b> können grundlegende Verwaltungsaufgaben erledigen.\n<b>Administratoren</b> können alle Einstellungen der Instanz verwalten." descriptionOfPermission: "<b>Moderatoren</b> können grundlegende Verwaltungsaufgaben erledigen.\n<b>Administratoren</b> können alle Einstellungen der Instanz verwalten."
assignTarget: "Zuweisungsart" assignTarget: "Zuweisungsart"
descriptionOfAssignTarget: "<b>Manuell</b> bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\n<b>Konditionell</b> bedeutet, dass die Liste der Benutzer einer Rolle durch eine Bedingung automatisch verwaltet wird." descriptionOfAssignTarget: "<b>Manuell</b> bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\n<b>Konditional</b> bedeutet, dass die Liste der Benutzer einer Rolle durch eine Bedingung automatisch verwaltet wird."
manual: "Manuell" manual: "Manuell"
conditional: "Konditional" conditional: "Konditional"
condition: "Bedingung" condition: "Bedingung"
@ -966,7 +1200,7 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen" gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen" canPublicNote: "Kann öffentliche Notizen erstellen"
canInvite: "Einladungscodes für diese Instanz erstellen" canInvite: "Kann Einladungscodes für diese Instanz erstellen"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
driveCapacity: "Drive-Kapazität" driveCapacity: "Drive-Kapazität"
pinMax: "Maximale Anzahl an angehefteten Notizen" pinMax: "Maximale Anzahl an angehefteten Notizen"
@ -979,6 +1213,7 @@ _role:
userEachUserListsMax: "Maximale Anzahl an Benutzerlisten" userEachUserListsMax: "Maximale Anzahl an Benutzerlisten"
rateLimitFactor: "Versuchsanzahl" rateLimitFactor: "Versuchsanzahl"
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
canHideAds: "Kann Werbung ausblenden"
_condition: _condition:
isLocal: "Lokaler Benutzer" isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz" isRemote: "Benutzer fremder Instanz"
@ -1023,7 +1258,7 @@ _accountDelete:
_ad: _ad:
back: "Zurück" back: "Zurück"
reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen" reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen"
hide: "Nie anzeigen" hide: "Ausblenden"
_forgotPassword: _forgotPassword:
enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst." enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst."
ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator." ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator."
@ -1583,6 +1818,7 @@ _notification:
pollEnded: "Umfrageergebnisse sind verfügbar" pollEnded: "Umfrageergebnisse sind verfügbar"
unreadAntennaNote: "Antenne {name}" unreadAntennaNote: "Antenne {name}"
emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert" emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert"
achievementEarned: "Errungenschaft freigeschaltet"
_types: _types:
all: "Alle" all: "Alle"
follow: "Neue Follower" follow: "Neue Follower"

View file

@ -103,6 +103,7 @@ you: "Εσύ"
clickToShow: "Κάντε κλικ για εμφάνιση" clickToShow: "Κάντε κλικ για εμφάνιση"
add: "Προσθέστε" add: "Προσθέστε"
reaction: "Αντιδράσεις" reaction: "Αντιδράσεις"
reactions: "Αντιδράσεις"
reactionSetting: "Αντιδράσεις για εμφάνιση στην επιλογή αντίδρασης" reactionSetting: "Αντιδράσεις για εμφάνιση στην επιλογή αντίδρασης"
reactionSettingDescription2: "Σύρετε για να αλλάξετε τη σειρά, κάντε κλικ για να διαγράψετε, πατήστε \"+\" για να προσθέσετε." reactionSettingDescription2: "Σύρετε για να αλλάξετε τη σειρά, κάντε κλικ για να διαγράψετε, πατήστε \"+\" για να προσθέσετε."
rememberNoteVisibility: "Θυμήσου τις ρυθμίσεις ορατότητας σημειώματος" rememberNoteVisibility: "Θυμήσου τις ρυθμίσεις ορατότητας σημειώματος"

View file

@ -110,6 +110,7 @@ clickToShow: "Click to show"
sensitive: "NSFW" sensitive: "NSFW"
add: "Add" add: "Add"
reaction: "Reactions" reaction: "Reactions"
reactions: "Reactions"
reactionSetting: "Reactions to show in the reaction picker" reactionSetting: "Reactions to show in the reaction picker"
reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add." reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add."
rememberNoteVisibility: "Remember note visibility settings" rememberNoteVisibility: "Remember note visibility settings"
@ -935,8 +936,239 @@ manageCustomEmojis: "Manage Custom Emojis"
youCannotCreateAnymore: "You've hit the creation limit." youCannotCreateAnymore: "You've hit the creation limit."
cannotPerformTemporary: "Temporarily unavailable" cannotPerformTemporary: "Temporarily unavailable"
cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again." cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again."
preset: "Presets" preset: "Preset"
selectFromPresets: "Choose from presets" selectFromPresets: "Choose from presets"
achievements: "Achievements"
_achievements:
earnedAt: "Unlocked at"
_types:
_notes1:
title: "just setting up my msky"
description: "Post your first note"
flavor: "Have a good Misskey life!"
_notes10:
title: "Some notes"
description: "Post 10 notes"
_notes100:
title: "A lot of notes"
description: "Post 100 notes"
_notes500:
title: "Covered in notes"
description: "Post 500 notes"
_notes1000:
title: "A mountain of notes"
description: "Post 1,000 notes"
_notes5000:
title: "Overflowing notes"
description: "Post 5,000 notes"
_notes10000:
title: "Supernote"
description: "Post 10,000 notes"
_notes20000:
title: "Need... more... notes..."
description: "Post 20,000 notes"
_notes30000:
title: "Notes notes notes!"
description: "Post 30,000 notes"
_notes40000:
title: "Note factory"
description: "Post 40,000 notes"
_notes50000:
title: "Planet of notes"
description: "Post 50,000 notes"
_notes60000:
title: "Note quasar"
description: "Post 60,000 notes"
_notes70000:
title: "Note black hole"
description: "Post 70,000 notes"
_notes80000:
title: "Note galaxy"
description: "Post 80,000 notes"
_notes90000:
title: "Note universe"
description: "Post 90,000 notes"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "Post 100,000 notes"
flavor: "You sure have a lot to say."
_login3:
title: "Beginner I"
description: "Log in for a total of 3 days"
flavor: "Starting today, just call me Misskist"
_login7:
title: "Beginner II"
description: "Log in for a total of 7 days"
flavor: "Feel like you've gotten the hang of things yet?"
_login15:
title: "Beginner III"
description: "Log in for a total of 15 days"
_login30:
title: "Misskist I"
description: "Log in for a total of 30 days"
_login60:
title: "Misskist II"
description: "Log in for a total of 60 days"
_login100:
title: "Misskist III"
description: "Log in for a total of 100 days"
flavor: "Violent Misskist"
_login200:
title: "Regular I"
description: "Log in for a total of 200 days"
_login300:
title: "Regular II"
description: "Log in for a total of 300 days"
_login400:
title: "Regular III"
description: "Log in for a total of 400 days"
_login500:
title: "Expert I"
description: "Log in for a total of 500 days"
flavor: "My friends, it has often been said that I like notes"
_login600:
title: "Expert II"
description: "Log in for a total of 600 days"
_login700:
title: "Expert III"
description: "Log in for a total of 700 days"
_login800:
title: "Master of Notes I"
description: "Log in for a total of 800 days"
_login900:
title: "Master of Notes II"
description: "Log in for a total of 900 days"
_login1000:
title: "Master of Notes III"
description: "Log in for a total of 1,000 days"
flavor: "Thank you for using Misskey!"
_noteClipped1:
title: "Must... clip..."
description: "Clip your first note"
_noteFavorited1:
title: "Stargazer"
description: "Favorite your first note"
_myNoteFavorited1:
title: "Seeking Stars"
description: "Have somebody else favorite one of your notes"
_profileFilled:
title: "Well-prepared"
description: "Set up your profile"
_markedAsCat:
title: "I Am a Cat"
description: "Mark your account as a cat"
flavor: "I'll give you a name later."
_following1:
title: "Following your first user"
description: "Follow a user"
_following10:
title: "Keep up... keep up..."
description: "Follow 10 users"
_following50:
title: "Lots of friends"
description: "Follow 50 accounts"
_following100:
title: "100 Friends"
description: "Follow 100 accounts"
_following300:
title: "Friend overload"
description: "Follow 300 accounts"
_followers1:
title: "First follower"
description: "Gain 1 follower"
_followers10:
title: "Follow me!"
description: "Gain 10 followers"
_followers50:
title: "Coming in crowds"
description: "Gain 50 followers"
_followers100:
title: "Popular"
description: "Gain 100 followers"
_followers300:
title: "Please form a single line"
description: "Gain 300 followers"
_followers500:
title: "Radio Tower"
description: "Gain 500 followers"
_followers1000:
title: "Influencer"
description: "Gain 1,000 followers"
_collectAchievements30:
title: "Achievement Collector"
description: "Earn 30 achievements"
_viewAchievements3min:
title: "Likes Achievements"
description: "Look at your list of achievements for at least 3 minutes"
_iLoveMisskey:
title: "I Love Misskey"
description: "Post \"I ❤ #Misskey\""
flavor: "Misskey's development team greatly appreciates your support!"
_client30min:
title: "Short break"
description: "Spend 30 minutes on Misskey"
_noteDeletedWithin1min:
title: "Nevermind"
description: "Delete a note within a minute of posting it"
_postedAtLateNight:
title: "Nocturnal"
description: "Post a note late at night"
flavor: "It's about time to go to bed."
_postedAt0min0sec:
title: "Speaking Clock"
description: "Post a note at 00:00"
flavor: "Click Click Click Claaang"
_selfQuote:
title: "Self-Reference"
description: "Quote your own note"
_htl20npm:
title: "Flowing Timeline"
description: "Have the speed of your home timeline exceed 20 npm (notes per minute)"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "Output \"hello world\" in the Scratchpad"
_open3windows:
title: "Multi-Window"
description: "Have at least 3 windows open at the same time"
_driveFolderCircularReference:
title: "Circular Reference"
description: "Attempt to create a recursively nested folder in Drive"
_reactWithoutRead:
title: "Did you really read that?"
description: "React on a note that's over 100 characters long within 3 seconds of it being posted"
_clickedClickHere:
title: "Click here"
description: "You've clicked here"
_justPlainLucky:
title: "Just Plain Lucky"
description: "Has a chance to be obtained with a probability of 0.01% every 10 seconds"
_setNameToSyuilo:
title: "God Complex"
description: "Set your name to \"syuilo\""
_passedSinceAccountCreated1:
title: "One Year Anniversary"
description: "One year has passed since your account was created"
_passedSinceAccountCreated2:
title: "Two Year Anniversary"
description: "Two years have passed since your account was created"
_passedSinceAccountCreated3:
title: "Three Year Anniversary"
description: "Three years have passed since your account was created"
_loggedInOnBirthday:
title: "Happy Birthday"
description: "Log in on your birthday"
_loggedInOnNewYearsDay:
title: "Happy New Year!"
description: "Logged in on the first day of the year"
flavor: "To another great year on this instance"
_cookieClicked:
title: "A game in which you click cookies"
description: "Clicked the cookie"
flavor: "Wait, are you on the correct website?"
_brainDiver:
title: "Brain Diver"
description: "Post the link to Brain Diver"
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "New role" new: "New role"
edit: "Edit role" edit: "Edit role"
@ -954,10 +1186,10 @@ _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." 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: "Role options"
policies: "Policies" policies: "Policies"
baseRole: "Base role" baseRole: "Role template"
useBaseValue: "Use base role value" useBaseValue: "Use role template value"
chooseRoleToAssign: "Select the role to assign" chooseRoleToAssign: "Select the role to assign"
canEditMembersByModerator: "Allow moderators to edit the list members of this role" canEditMembersByModerator: "Allow moderators to edit the list of members for this role"
descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users."
priority: "Priority" priority: "Priority"
_priority: _priority:
@ -965,11 +1197,11 @@ _role:
middle: "Medium" middle: "Medium"
high: "High" high: "High"
_options: _options:
gtlAvailable: "Viewing the global timeline" gtlAvailable: "Can view the global timeline"
ltlAvailable: "Viewing the local timeline" ltlAvailable: "Can view the local timeline"
canPublicNote: "Can send public notes" canPublicNote: "Can send public notes"
canInvite: "Create instance invite codes" canInvite: "Can create instance invite codes"
canManageCustomEmojis: "Manage Custom Emojis" canManageCustomEmojis: "Can manage custom emojis"
driveCapacity: "Drive capacity" driveCapacity: "Drive capacity"
pinMax: "Maximum number of pinned notes" pinMax: "Maximum number of pinned notes"
antennaMax: "Maximum number of antennas" antennaMax: "Maximum number of antennas"
@ -981,7 +1213,7 @@ _role:
userEachUserListsMax: "Maximum number of users within a user list" userEachUserListsMax: "Maximum number of users within a user list"
rateLimitFactor: "Rate limit" rateLimitFactor: "Rate limit"
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
canHideAds: "Remove ads" canHideAds: "Can hide ads"
_condition: _condition:
isLocal: "Local user" isLocal: "Local user"
isRemote: "Remote user" isRemote: "Remote user"
@ -1026,7 +1258,7 @@ _accountDelete:
_ad: _ad:
back: "Back" back: "Back"
reduceFrequencyOfThisAd: "Show this ad less" reduceFrequencyOfThisAd: "Show this ad less"
hide: "Never show" hide: "Hide"
_forgotPassword: _forgotPassword:
enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it." enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it."
ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead." ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead."
@ -1586,6 +1818,7 @@ _notification:
pollEnded: "Poll results have become available" pollEnded: "Poll results have become available"
unreadAntennaNote: "Antenna {name}" unreadAntennaNote: "Antenna {name}"
emptyPushNotificationMessage: "Push notifications have been updated" emptyPushNotificationMessage: "Push notifications have been updated"
achievementEarned: "Achievement unlocked"
_types: _types:
all: "All" all: "All"
follow: "New followers" follow: "New followers"

View file

@ -110,6 +110,7 @@ clickToShow: "Click para ver"
sensitive: "Marcado como sensible" sensitive: "Marcado como sensible"
add: "Agregar" add: "Agregar"
reaction: "Reacción" reaction: "Reacción"
reactions: "Reacción"
reactionSetting: "Reacciones para mostrar en el menú de reacciones" reactionSetting: "Reacciones para mostrar en el menú de reacciones"
reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir." reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir."
rememberNoteVisibility: "Recordar visibilidad" rememberNoteVisibility: "Recordar visibilidad"

View file

@ -110,6 +110,7 @@ clickToShow: "Cliquer pour afficher"
sensitive: "Contenu sensible" sensitive: "Contenu sensible"
add: "Ajouter" add: "Ajouter"
reaction: "Réactions" reaction: "Réactions"
reactions: "Réactions"
reactionSetting: "Réactions à afficher dans le sélecteur de réactions" reactionSetting: "Réactions à afficher dans le sélecteur de réactions"
reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter." reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter."
rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la publication de votre note précédente." rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la publication de votre note précédente."

View file

@ -2,6 +2,7 @@
_lang_: "Bahasa Indonesia" _lang_: "Bahasa Indonesia"
headlineMisskey: "Jaringan terhubung melalui catatan" headlineMisskey: "Jaringan terhubung melalui catatan"
introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersifat sumber terbuka.\nMulailah menuliskan catatan, bagikan peristiwa terkini, serta ceritakan segala tentangmu.📡\nTunjukkan juga reaksimu pada catatan pengguna lain.👍\nMari jelajahi dunia baru🚀" introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersifat sumber terbuka.\nMulailah menuliskan catatan, bagikan peristiwa terkini, serta ceritakan segala tentangmu.📡\nTunjukkan juga reaksimu pada catatan pengguna lain.👍\nMari jelajahi dunia baru🚀"
poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka <b>Misskey</b>."
monthAndDay: "{day} {month}" monthAndDay: "{day} {month}"
search: "Penelusuran" search: "Penelusuran"
notifications: "Pemberitahuan" notifications: "Pemberitahuan"
@ -47,6 +48,7 @@ deleteAndEdit: "Hapus dan sunting"
deleteAndEditConfirm: "Apakah kamu yakin ingin menghapus note ini dan menyuntingnya? Kamu akan kehilangan semua reaksi, renote dan balasan di note ini." deleteAndEditConfirm: "Apakah kamu yakin ingin menghapus note ini dan menyuntingnya? Kamu akan kehilangan semua reaksi, renote dan balasan di note ini."
addToList: "Tambahkan ke daftar" addToList: "Tambahkan ke daftar"
sendMessage: "Kirim pesan" sendMessage: "Kirim pesan"
copyRSS: "Salin RSS"
copyUsername: "Salin nama pengguna" copyUsername: "Salin nama pengguna"
searchUser: "Cari pengguna" searchUser: "Cari pengguna"
reply: "Balas" reply: "Balas"
@ -107,6 +109,7 @@ clickToShow: "Klik untuk melihat"
sensitive: "Konten sensitif" sensitive: "Konten sensitif"
add: "Tambahkan" add: "Tambahkan"
reaction: "Reaksi" reaction: "Reaksi"
reactions: "Reaksi"
reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi" reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi"
reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus, tekan \"+\" untuk menambahkan" reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus, tekan \"+\" untuk menambahkan"
rememberNoteVisibility: "Ingat pengaturan visibilitas catatan" rememberNoteVisibility: "Ingat pengaturan visibilitas catatan"
@ -383,6 +386,7 @@ administrator: "Admin"
token: "Token" token: "Token"
twoStepAuthentication: "Otentikasi dua faktor" twoStepAuthentication: "Otentikasi dua faktor"
moderator: "Moderator" moderator: "Moderator"
moderation: "Moderasi"
nUsersMentioned: "{n} pengguna disebut" nUsersMentioned: "{n} pengguna disebut"
securityKey: "Kunci keamanan" securityKey: "Kunci keamanan"
securityKeyName: "Nama kunci" securityKeyName: "Nama kunci"
@ -449,6 +453,7 @@ language: "Bahasa"
uiLanguage: "Bahasa antarmuka pengguna" uiLanguage: "Bahasa antarmuka pengguna"
groupInvited: "Telah diundang ke grup" groupInvited: "Telah diundang ke grup"
aboutX: "Tentang {x}" aboutX: "Tentang {x}"
emojiStyle: "Gaya emoji"
disableDrawer: "Jangan gunakan menu bergaya laci" disableDrawer: "Jangan gunakan menu bergaya laci"
youHaveNoGroups: "Kamu tidak memiliki grup" youHaveNoGroups: "Kamu tidak memiliki grup"
joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri." joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri."
@ -561,6 +566,7 @@ author: "Pembuat"
leaveConfirm: "Ada perubahan yang belum disimpan. Apakah kamu ingin membuangnya?" leaveConfirm: "Ada perubahan yang belum disimpan. Apakah kamu ingin membuangnya?"
manage: "Manajemen" manage: "Manajemen"
plugins: "Plugin" plugins: "Plugin"
preferencesBackups: "Aturan pencadangan"
deck: "Dek" deck: "Dek"
undeck: "Keluar dari dek" undeck: "Keluar dari dek"
useBlurEffectForModal: "Gunakan efek buram untuk modal" useBlurEffectForModal: "Gunakan efek buram untuk modal"
@ -706,6 +712,7 @@ accentColor: "Aksen"
textColor: "Teks" textColor: "Teks"
saveAs: "Simpan sebagai…" saveAs: "Simpan sebagai…"
advanced: "Tingkat lanjut" advanced: "Tingkat lanjut"
advancedSettings: "Pengaturan Lanjut"
value: "Nilai" value: "Nilai"
createdAt: "Dibuat pada" createdAt: "Dibuat pada"
updatedAt: "Diperbarui pada" updatedAt: "Diperbarui pada"
@ -850,10 +857,21 @@ rateLimitExceeded: "Batas sudah terlampaui"
cropImage: "potong gambar" cropImage: "potong gambar"
cropImageAsk: "Ingin memotong gambar?" cropImageAsk: "Ingin memotong gambar?"
file: "Berkas" file: "Berkas"
noEmailServerWarning: "Mail Server tidak disetel."
recommended: "Disarankan"
check: "Cek"
deleteAccount: "Hapus Akun"
logoutConfirm: "Anda yakin ingin keluar?"
lastActiveDate: "Terakhir digunakan"
statusbar: "Bilah status"
pleaseSelect: "Pilih opsi..."
reverse: "Balik" reverse: "Balik"
colored: "Diwarnai" colored: "Diwarnai"
refreshInterval: "Jeda pembaharuan"
label: "Label" label: "Label"
type: "Tipe"
localOnly: "Hanya lokal" localOnly: "Hanya lokal"
shuffle: "Acak"
account: "Akun" account: "Akun"
like: "Suka" like: "Suka"
unlike: "Tidak Suka" unlike: "Tidak Suka"

View file

@ -1,7 +1,7 @@
--- ---
_lang_: "Italiano" _lang_: "Italiano"
headlineMisskey: "Rete collegata tramite note" headlineMisskey: "Rete collegata tramite note"
introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n🚀 Esplora un nuovo mondo insieme a noi!" introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n\n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n\n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n\n🚀 Esplora un nuovo mondo insieme a noi!"
poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source <b>Misskey</b>." poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source <b>Misskey</b>."
monthAndDay: "{day}/{month}" monthAndDay: "{day}/{month}"
search: "Cerca" search: "Cerca"
@ -95,7 +95,7 @@ follow: "Segui"
followRequest: "Richiesta di follow" followRequest: "Richiesta di follow"
followRequests: "Richieste di follow" followRequests: "Richieste di follow"
unfollow: "Smetti di seguire" unfollow: "Smetti di seguire"
followRequestPending: "La richiesta di follow deve essere approvata" followRequestPending: "Richiesta in approvazione"
enterEmoji: "Inserisci emoji" enterEmoji: "Inserisci emoji"
renote: "Rinota" renote: "Rinota"
unrenote: "Annulla rinota" unrenote: "Annulla rinota"
@ -110,6 +110,7 @@ clickToShow: "Clicca per visualizzare"
sensitive: "Contenuto sensibile" sensitive: "Contenuto sensibile"
add: "Aggiungi" add: "Aggiungi"
reaction: "Reazioni" reaction: "Reazioni"
reactions: "Reazioni"
reactionSetting: "Reazioni visualizzate sul pannello" reactionSetting: "Reazioni visualizzate sul pannello"
reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere." reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere."
rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note" rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note"
@ -836,7 +837,7 @@ hide: "Nascondere"
leaveGroup: "Esci dal gruppo" leaveGroup: "Esci dal gruppo"
leaveGroupConfirm: "Uscire da「{name}」?" leaveGroupConfirm: "Uscire da「{name}」?"
useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile" useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile"
welcomeBackWithName: "Eccoti di nuovo, {name}! Ciao!" welcomeBackWithName: "Ciao, {name}! Eccoti di nuovo!"
clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email." clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email."
overridedDeviceKind: "Tipo di dispositivo" overridedDeviceKind: "Tipo di dispositivo"
smartphone: "Smartphone" smartphone: "Smartphone"
@ -935,6 +936,239 @@ manageCustomEmojis: "Gestisci le emoji personalizzate"
youCannotCreateAnymore: "Non puoi creare, hai raggiunto il limite." youCannotCreateAnymore: "Non puoi creare, hai raggiunto il limite."
cannotPerformTemporary: "Indisponibilità temporanea" cannotPerformTemporary: "Indisponibilità temporanea"
cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi." cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi."
preset: "Preimpostato"
selectFromPresets: "Seleziona preimpostato"
achievements: "Obiettivi raggiunti"
_achievements:
earnedAt: "Data di conseguimento"
_types:
_notes1:
title: "Hai iniziato a usare Misskey"
description: "Hai pubblicato la prima Nota"
flavor: "Goditi la vita su Misskey!"
_notes10:
title: "Alcune Note"
description: "Hai inserito 10 Note"
_notes100:
title: "Un po' di Note"
description: "Hai inserito 100 Note"
_notes500:
title: "Un bel po' di Note"
description: "Hai inserito 500 Note"
_notes1000:
title: "Una montagna di Note"
description: "Hai inserito 1.000 Note"
_notes5000:
title: "Un sovraccarico di Note!"
description: "Hai inserito 5.000 Note"
_notes10000:
title: "SuperNote!"
description: "Hai inserito 10.000 Note"
_notes20000:
title: "Voglio più... Note!"
description: "Hai inserito 20.000 Note"
_notes30000:
title: "Note, Note, Note!"
description: "Hai inserito 30.000 Note"
_notes40000:
title: "Una fabbrica di Note"
description: "Hai inserito 40.000 Note"
_notes50000:
title: "Un pianeta di Note"
description: "Hai inserito 50.000 Note"
_notes60000:
title: "Un quasar di Note"
description: "Hai inserito 60.000 Note"
_notes70000:
title: "Un buco nero supermassiccio di Note"
description: "Hai inserito 70.000 Note"
_notes80000:
title: "Una galassia di Note"
description: "Hai inserito 80.000 Note"
_notes90000:
title: "Un universo di Note!"
description: "Hai inserito 90.000 Note"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "Hai inserito 100.000 Note"
flavor: "Hai molto da scrivere?"
_login3:
title: "Principiante I"
description: "Accedi per un totale di 3 giorni"
flavor: "Da oggi, chiamatemi Misskist"
_login7:
title: "Principiante II"
description: "Accedi per un totale di 7 giorni"
flavor: "Ti sembra di avere la situazione sotto controllo?"
_login15:
title: "Principiante III"
description: "Accedi per un totale di 15 giorni"
_login30:
title: "Misskist I"
description: "Accedi per un totale di 30 giorni"
_login60:
title: "Misskeist II"
description: "Accedi per un totale di 60 giorni"
_login100:
title: "Misskeist III"
description: "Accedi per un totale di 100 giorni"
flavor: "Violent Misskeist"
_login200:
title: "Regolare I"
description: "Accedi per un totale di 200 giorni"
_login300:
title: "Regolare II"
description: "Accedi per un totale di 300 giorni"
_login400:
title: "Regolare III"
description: "Accedi per un totale di 400 giorni"
_login500:
title: "Professionista I"
description: "Accedi per un totale di 500 giorni"
flavor: "Amici cari, mi piacciono le Note"
_login600:
title: "Professionista II"
description: "Accedi per un totale di 600 giorni"
_login700:
title: "Professionista III"
description: "Accedi per un totale di 700 giorni"
_login800:
title: "Maestro di Note I"
description: "Accedi per un totale di 800 giorni"
_login900:
title: "Maestro di Note II"
description: "Accedi per un totale di 900 giorni"
_login1000:
title: "Maestro di Note III"
description: "Accedi per un totale di 1.000 giorni"
flavor: "Grazie per aver usato Misskey!"
_noteClipped1:
title: "Devo clippare!"
description: "Ho raccolto in Clip la prima Nota"
_noteFavorited1:
title: "Guarda le stelle"
description: "Aggiungi una Nota ai preferiti per la prima volta"
_myNoteFavorited1:
title: "Fornitura stelline"
description: "Qualcuno ha preferito una delle tue Note"
_profileFilled:
title: "Perfettamente"
description: "Imposta il tuo profilo"
_markedAsCat:
title: "Io sono un gatto"
description: "Aggiungi le orecchie da gatto al tuo profilo"
flavor: "Ti chiamerò..."
_following1:
title: "Hai seguito il tuo primo profilo"
description: "Il tuo primo profilo Follower"
_following10:
title: "Segui, segui!"
description: "Hai seguito 10 profili"
_following50:
title: "Tanti amici"
description: "Hai seguito 50 profili"
_following100:
title: "Cento amici"
description: "Hai seguito 100 profili"
_following300:
title: "Sovraccarico di amici"
description: "Hai seguito 300 profili"
_followers1:
title: "Primo Follower"
description: "Hai ottenuto un Follower"
_followers10:
title: "Follow me!"
description: "Hai ottenuto 10 Follower"
_followers50:
title: "Follower a frotte"
description: "Hai ottenuto 50 Follower"
_followers100:
title: "Popolare"
description: "Hai ottenuto 100 Follower"
_followers300:
title: "Mettetevi in fila"
description: "Hai ottenuto 300 Follower"
_followers500:
title: "Trasmettitore"
description: "Hai ottenuto 500 Follower"
_followers1000:
title: "Influenzer"
description: "Hai superato i 1.000 Follower"
_collectAchievements30:
title: "Collezionista di successi"
description: "Hai raggiunto 30 obiettivi"
_viewAchievements3min:
title: "Mi piacciono i risultati"
description: "Guarda la tua collezione di obiettivi per almeno 3 minuti"
_iLoveMisskey:
title: "I LOVE Misskey"
description: "Pubblica «I ♥ #Misskey»"
flavor: "Grazie per aver utilizzato Misskey! Dal team di sviluppo"
_client30min:
title: "Piccola pausa"
description: "Hai passato più di 30 minuti di fila su Misskey"
_noteDeletedWithin1min:
title: "Ooops!"
description: "Hai eliminato una nota entro un minuto dalla sua pubblicazione"
_postedAtLateNight:
title: "Biassanot!"
description: "Hai pubblicato una nota in tarda notte"
flavor: "Andiamo a dormire presto"
_postedAt0min0sec:
title: "Mezzanotte"
description: "Hai pubblicato una Nota a mezzanotte in punto"
flavor: "tic, tac, tic, tac! Gong!"
_selfQuote:
title: "Autoreferenziale"
description: "Hai citato una delle tue Note"
_htl20npm:
title: "Timeline scorrevole"
description: "La tua Timeline personale ha superato la velocità di 20 Note orarie (Note al minuto)"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "Hai scritto «Hello world» nel blocco appunti"
_open3windows:
title: "Finestrato"
description: "Hai aperto almeno 3 finestre contemporaneamente"
_driveFolderCircularReference:
title: "Riferimento circolare"
description: "Hai provato a nidificare in modo ricorsivo le cartelle del Drive"
_reactWithoutRead:
title: "Hai letto bene?"
description: "Hai reagito ad una Nota più lunga di 100 caratteri entro 3 secondi dalla sua pubblicazione"
_clickedClickHere:
title: "Clicca qui"
description: "Hai cliccato qui"
_justPlainLucky:
title: "Proprio fortunato"
description: "Ottenuto con una probabilità dello 0,01% ogni 10 secondi"
_setNameToSyuilo:
title: "Complesso divino"
description: "Hai impostati il tuo nome in «syuilo»"
_passedSinceAccountCreated1:
title: "Primo Anniversario"
description: "È passato un anno da quando hai creato il profilo"
_passedSinceAccountCreated2:
title: "Secondo Anniversario"
description: "Sono passati due anni da quando hai creato il profilo"
_passedSinceAccountCreated3:
title: "Terzo Anniversario"
description: "Sono passati tre anni da quando hai creato il profilo"
_loggedInOnBirthday:
title: "Buon compleanno!"
description: "Hai effettuato l'accesso il giorno del tuo compleanno"
_loggedInOnNewYearsDay:
title: "Buon anno nuovo!"
description: "Hai usato effettuato l'accesso il giorno di capodanno"
flavor: "Anche quest'anno, grazie per il tuo continuo supporto a questa istanza"
_cookieClicked:
title: "Clicca il biscotto"
description: "Hai giocato a cliccare il cookie"
flavor: "Hai autorizzato i cookie?"
_brainDiver:
title: "Brain Diver"
description: "Pubblica un link a Brain Diver"
flavor: "Sulle note di Brain Diver"
_role: _role:
new: "Nuovo ruolo" new: "Nuovo ruolo"
edit: "Modifica ruolo" edit: "Modifica ruolo"
@ -979,6 +1213,7 @@ _role:
userEachUserListsMax: "Quantità massima di profili per lista" userEachUserListsMax: "Quantità massima di profili per lista"
rateLimitFactor: "Limite del rapporto" rateLimitFactor: "Limite del rapporto"
descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più."
canHideAds: "Può nascondere i banner"
_condition: _condition:
isLocal: "Profilo locale" isLocal: "Profilo locale"
isRemote: "Profilo remoto" isRemote: "Profilo remoto"
@ -1289,7 +1524,7 @@ _tutorial:
step3_1: "Hai finito di impostare il tuo profilo?" step3_1: "Hai finito di impostare il tuo profilo?"
step3_2: "Ora puoi pubblicare una «Nota». Proviamo subito! Premi il bottone con l'icona «penna» per iniziare a scrivere in una finestra di dialogo. " step3_2: "Ora puoi pubblicare una «Nota». Proviamo subito! Premi il bottone con l'icona «penna» per iniziare a scrivere in una finestra di dialogo. "
step3_3: "Scritto il testo della nota, puoi pubblicarla premendo il pulsante nella parte superiore destra della finestra di dialogo." step3_3: "Scritto il testo della nota, puoi pubblicarla premendo il pulsante nella parte superiore destra della finestra di dialogo."
step3_4: "Non ti viene niente in mente? Perché non scrivi semplicemente \"Ho appena cominciato a usare Misskey\"?" step3_4: "Non ti viene niente in mente? Perché non scrivi semplicemente \"Ho appena iniziato a usare Misskey\"?"
step4_1: "Hai pubblicato qualcosa?" step4_1: "Hai pubblicato qualcosa?"
step4_2: "Se puoi visualizzare la tua nota sulla timeline, ce l'hai fatta!" step4_2: "Se puoi visualizzare la tua nota sulla timeline, ce l'hai fatta!"
step5_1: "Adesso, cerca di seguire altre persone per vivacizzare la tua timeline. " step5_1: "Adesso, cerca di seguire altre persone per vivacizzare la tua timeline. "
@ -1583,6 +1818,7 @@ _notification:
pollEnded: "Risultati del sondaggio." pollEnded: "Risultati del sondaggio."
unreadAntennaNote: "Antenna {name}" unreadAntennaNote: "Antenna {name}"
emptyPushNotificationMessage: "Le notifiche push sono state aggiornate." emptyPushNotificationMessage: "Le notifiche push sono state aggiornate."
achievementEarned: "Obiettivo raggiunto"
_types: _types:
all: "Tutto" all: "Tutto"
follow: "Novità follower" follow: "Novità follower"

View file

@ -110,6 +110,7 @@ clickToShow: "クリックして表示"
sensitive: "閲覧注意" sensitive: "閲覧注意"
add: "追加" add: "追加"
reaction: "リアクション" reaction: "リアクション"
reactions: "リアクション"
reactionSetting: "ピッカーに表示するリアクション" reactionSetting: "ピッカーに表示するリアクション"
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。" reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
rememberNoteVisibility: "公開範囲を記憶する" rememberNoteVisibility: "公開範囲を記憶する"
@ -937,6 +938,244 @@ cannotPerformTemporary: "一時的に利用できません"
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。" cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
preset: "プリセット" preset: "プリセット"
selectFromPresets: "プリセットから選択" selectFromPresets: "プリセットから選択"
achievements: "実績"
_achievements:
earnedAt: "獲得日時"
_types:
_notes1:
title: "just setting up my msky"
description: "初めてノートを投稿した"
flavor: "良いMisskeyライフを"
_notes10:
title: "いくつかのノート"
description: "ートを10回投稿した"
_notes100:
title: "たくさんのノート"
description: "ートを100回投稿した"
_notes500:
title: "ノートまみれ"
description: "ートを500回投稿した"
_notes1000:
title: "ノートの山"
description: "ートを1,000回投稿した"
_notes5000:
title: "湧き出るノート"
description: "ートを5,000回投稿した"
_notes10000:
title: "スーパーノート"
description: "ートを10,000回投稿した"
_notes20000:
title: "ニードモアノート"
description: "ートを20,000回投稿した"
_notes30000:
title: "ノートノートノート"
description: "ートを30,000回投稿した"
_notes40000:
title: "ノート工場"
description: "ートを40,000回投稿した"
_notes50000:
title: "ノートの惑星"
description: "ートを50,000回投稿した"
_notes60000:
title: "ノートクエーサー"
description: "ートを60,000回投稿した"
_notes70000:
title: "ブラックノートホール"
description: "ートを70,000回投稿した"
_notes80000:
title: "ノートギャラクシー"
description: "ートを80,000回投稿した"
_notes90000:
title: "ノートバース"
description: "ートを90,000回投稿した"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "ートを100,000回投稿した"
flavor: "そんなに書くことある?"
_login3:
title: "ビギナーⅠ"
description: "通算ログイン日数が3日"
flavor: "今日からね僕は ミスキストってことで"
_login7:
title: "ビギナーⅡ"
description: "通算ログイン日数が7日"
flavor: "慣れてきましたか?"
_login15:
title: "ビギナーⅢ"
description: "通算ログイン日数が15日"
_login30:
title: "ミスキストⅠ"
description: "通算ログイン日数が30日"
_login60:
title: "ミスキストⅡ"
description: "通算ログイン日数が60日"
_login100:
title: "ミスキストⅢ"
description: "通算ログイン日数が100日"
flavor: "そのユーザー、ミスキストにつき"
_login200:
title: "常連Ⅰ"
description: "通算ログイン日数が200日"
_login300:
title: "常連Ⅱ"
description: "通算ログイン日数が300日"
_login400:
title: "常連Ⅲ"
description: "通算ログイン日数が400日"
_login500:
title: "ベテランⅠ"
description: "通算ログイン日数が500日"
flavor: "諸君、私はノートが好きだ"
_login600:
title: "ベテランⅡ"
description: "通算ログイン日数が600日"
_login700:
title: "ベテランⅢ"
description: "通算ログイン日数が700日"
_login800:
title: "ノートマスターⅠ"
description: "通算ログイン日数が800日"
_login900:
title: "ノートマスターⅡ"
description: "通算ログイン日数が900日"
_login1000:
title: "ノートマスターⅢ"
description: "通算ログイン日数が1,000日"
flavor: "Misskeyを使ってくれてありがとう"
_noteClipped1:
title: "クリップせずにはいられないな"
description: "初めてノートをクリップした"
_noteFavorited1:
title: "星をみるひと"
description: "初めてノートをお気に入りに登録した"
_myNoteFavorited1:
title: "星が欲しい"
description: "自分のノートが他の人からお気に入りに登録された"
_profileFilled:
title: "準備万端"
description: "プロフィール設定を行った"
_markedAsCat:
title: "吾輩は猫である"
description: "アカウントをCatとして設定した"
flavor: "名前はまだない。"
_following1:
title: "はじめてのフォロー"
description: "初めてフォローした"
_following10:
title: "ついてく、ついてく"
description: "フォローが10人を超した"
_following50:
title: "友達たくさん"
description: "フォローが50人を超した"
_following100:
title: "友達100人"
description: "フォローが100人を超した"
_following300:
title: "友達過多"
description: "フォローが300人を超した"
_followers1:
title: "はじめてのフォロワー"
description: "初めてフォローされた"
_followers10:
title: "フォローミー!"
description: "フォロワーが10人を超した"
_followers50:
title: "ぞろぞろ"
description: "フォロワーが50人を超した"
_followers100:
title: "人気者"
description: "フォロワーが100人を超した"
_followers300:
title: "一列でお並びください"
description: "フォロワーが300人を超した"
_followers500:
title: "基地局"
description: "フォロワーが500人を超した"
_followers1000:
title: "インフルエンサー"
description: "フォロワーが1,000人を超した"
_collectAchievements30:
title: "実績コレクター"
description: "実績を30個以上獲得した"
_viewAchievements3min:
title: "実績好き"
description: "実績一覧を3分以上眺め続けた"
_iLoveMisskey:
title: "I Love Misskey"
description: "\"I ❤ #Misskey\"を投稿した"
flavor: "Misskeyを使ってくださりありがとうございます by 開発チーム"
_foundTreasure:
title: "宝探し"
description: "隠されたお宝を発見した"
_client30min:
title: "ひとやすみ"
description: "クライアントを起動してから30分以上経過した"
_noteDeletedWithin1min:
title: "いまのなし"
description: "投稿してから1分以内にその投稿を削除した"
_postedAtLateNight:
title: "夜行性"
description: "深夜にノートを投稿した"
flavor: "そろそろ寝よう。"
_postedAt0min0sec:
title: "時報"
description: "0分0秒にートを投稿した"
flavor: "ポッ ポッ ポッ ピーン"
_selfQuote:
title: "自己言及"
description: "自分のノートを引用した"
_htl20npm:
title: "流れるTL"
description: "ホームタイムラインの流速が20npmを越す"
_viewInstanceChart:
title: "アナリスト"
description: "インスタンスのチャートを表示した"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "スクラッチパッドで hello world を出力した"
_open3windows:
title: "マルチウィンドウ"
description: "ウィンドウを3つ以上開いた状態にした"
_driveFolderCircularReference:
title: "循環参照"
description: "ドライブのフォルダを再帰的な入れ子にしようとした"
_reactWithoutRead:
title: "ちゃんと読んだ?"
description: "100文字以上のテキストを含むートに投稿されてから3秒以内にリアクションした"
_clickedClickHere:
title: "ここをクリック"
description: "ここをクリックした"
_justPlainLucky:
title: "単なるラッキー"
description: "10秒ごとに0.01%の確率で獲得"
_setNameToSyuilo:
title: "神様コンプレックス"
description: "名前を syuilo に設定した"
_passedSinceAccountCreated1:
title: "一周年"
description: "アカウント作成から1年経過した"
_passedSinceAccountCreated2:
title: "二周年"
description: "アカウント作成から2年経過した"
_passedSinceAccountCreated3:
title: "三周年"
description: "アカウント作成から3年経過した"
_loggedInOnBirthday:
title: "ハッピーバースデー"
description: "誕生日にログインした"
_loggedInOnNewYearsDay:
title: "あけましておめでとうございます"
description: "元日にログインした"
flavor: "今年も弊インスタンスをよろしくお願いします"
_cookieClicked:
title: "クッキーをクリックするゲーム"
description: "クッキーをクリックした"
flavor: "ソフト間違ってない?"
_brainDiver:
title: "Brain Diver"
description: "Brain Diverへのリンクを投稿した"
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "ロールの作成" new: "ロールの作成"
@ -1634,6 +1873,7 @@ _notification:
pollEnded: "アンケートの結果が出ました" pollEnded: "アンケートの結果が出ました"
unreadAntennaNote: "アンテナ {name}" unreadAntennaNote: "アンテナ {name}"
emptyPushNotificationMessage: "プッシュ通知の更新をしました" emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得"
_types: _types:
all: "すべて" all: "すべて"

View file

@ -8,9 +8,9 @@ search: "探す"
notifications: "通知" notifications: "通知"
username: "ユーザー名" username: "ユーザー名"
password: "パスワード" password: "パスワード"
forgotPassword: "パスワード忘れて" forgotPassword: "パスワード忘れてもうた"
fetchingAsApObject: "今ちと連合に照会しとるで" fetchingAsApObject: "今ちと連合に照会しとるで"
ok: "OKや" ok: "ええで"
gotIt: "ほい" gotIt: "ほい"
cancel: "やめとく" cancel: "やめとく"
noThankYou: "やめとく" noThankYou: "やめとく"
@ -110,6 +110,7 @@ clickToShow: "押したら見えるで"
sensitive: "ちょっとアカンやつやで" sensitive: "ちょっとアカンやつやで"
add: "増やす" add: "増やす"
reaction: "リアクション" reaction: "リアクション"
reactions: "リアクション"
reactionSetting: "Reaction that will be displayed in Picker. " reactionSetting: "Reaction that will be displayed in Picker. "
reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。" reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。"
rememberNoteVisibility: "公開範囲覚えといて" rememberNoteVisibility: "公開範囲覚えといて"
@ -607,7 +608,7 @@ wordMute: "ワードミュート"
regexpError: "正規表現エラー" regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:"
instanceMute: "インスタンスミュート" instanceMute: "インスタンスミュート"
userSaysSomething: "{name}が何か言ったようやで" userSaysSomething: "{name}が何か言うとるわ"
makeActive: "使うで" makeActive: "使うで"
display: "表示" display: "表示"
copy: "コピー" copy: "コピー"

View file

@ -110,6 +110,7 @@ clickToShow: "클릭하여 보기"
sensitive: "열람주의" sensitive: "열람주의"
add: "추가" add: "추가"
reaction: "리액션" reaction: "리액션"
reactions: "리액션"
reactionSetting: "선택기에 표시할 리액션" reactionSetting: "선택기에 표시할 리액션"
reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다." reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
rememberNoteVisibility: "공개 범위를 기억하기" rememberNoteVisibility: "공개 범위를 기억하기"
@ -935,12 +936,242 @@ manageCustomEmojis: "커스텀 이모지 관리"
youCannotCreateAnymore: "더 이상 생성할 수 없습니다." youCannotCreateAnymore: "더 이상 생성할 수 없습니다."
cannotPerformTemporary: "일시적으로 사용할 수 없음" cannotPerformTemporary: "일시적으로 사용할 수 없음"
cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시적으로 사용이 불가합니다. 잠시 후 다시 시도해 주세요." cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시적으로 사용이 불가합니다. 잠시 후 다시 시도해 주세요."
preset: "프리셋"
selectFromPresets: "프리셋에서 선택"
achievements: "도전과제"
_achievements:
earnedAt: "달성 일시"
_types:
_notes1:
title: "미스키 설정하고 있었는데요"
description: "첫 노트를 포스트했습니다"
flavor: "Misskey에 오신 것을 환영합니다!"
_notes10:
title: "노트 조금"
description: "10개의 노트를 작성했습니다"
_notes100:
title: "노트 많이"
description: "100개의 노트를 작성했습니다"
_notes500:
title: "노트로 뒤덮여버렸어"
description: "500개의 노트를 작성했습니다"
_notes1000:
title: "노트만 산더미"
description: "1,000개의 노트를 작성했습니다"
_notes5000:
title: "노트가 어디서 솟아?"
description: "5,000개의 노트를 작성했습니다"
_notes10000:
title: "슈퍼-노트"
description: "10,000개의 노트를 작성했습니다"
_notes20000:
title: "노트 더 없어?"
description: "20,000개의 노트를 작성했습니다"
_notes30000:
title: "노트노트노트"
description: "30,000개의 노트를 작성했습니다"
_notes40000:
title: "노트 공장"
description: "40,000개의 노트를 작성했습니다"
_notes50000:
title: "노트 행성"
description: "50,000개의 노트를 작성했습니다"
_notes60000:
title: "노트 퀘이사"
description: "60,000개의 노트를 작성했습니다"
_notes70000:
title: "노트 블랙홀"
description: "70,000개의 노트를 작성했습니다"
_notes80000:
title: "노트 은하"
description: "80,000개의 노트를 작성했습니다"
_notes90000:
title: "노트 우주"
description: "90,000개의 노트를 작성했습니다"
_notes100000:
title: "네 모든 노트는 내 거야"
description: "100,000개의 노트를 작성했습니다"
flavor: "이만큼 쓸 일도 없겠지만... 다른 할 일이 있진 않으신가요?"
_login3:
title: "비기너 I"
description: "총 3일간 로그인했습니다"
flavor: "오늘부터 여러분도 미스키스트에요!"
_login7:
title: "비기너 II"
description: "총 7일간 로그인했습니다"
flavor: "슬슬 익숙해지셨나요?"
_login15:
title: "비기너 III"
description: "총 15일간 로그인했습니다"
_login30:
title: "미스키스트 I"
description: "총 30일간 로그인했습니다"
_login60:
title: "미스키스트 II"
description: "총 60일간 로그인했습니다"
_login100:
title: "미스키스트 III"
description: "총 100일간 로그인했습니다"
flavor: "그 유저, 미스키스트를 위하여"
_login200:
title: "단골 I"
description: "총 200일간 로그인했습니다"
_login300:
title: "단골 II"
description: "총 300일간 로그인했습니다"
_login400:
title: "단골 III"
description: "총 400일간 로그인했습니다"
_login500:
title: "베테랑 I"
description: "총 500일간 로그인했습니다"
flavor: "여러분, 저 이 노트들 좋아해요"
_login600:
title: "베테랑 II"
description: "총 600일간 로그인했습니다"
_login700:
title: "베테랑 III"
description: "총 700일간 로그인했습니다"
_login800:
title: "노트 마스터 I"
description: "총 800일간 로그인했습니다"
_login900:
title: "노트 마스터 II"
description: "총 900일간 로그인했습니다"
_login1000:
title: "노트 마스터 III"
description: "총 1,000일간 로그인했습니다"
flavor: "미스키를 사용해 주셔서 감사합니다!"
_noteClipped1:
title: "클립할 수밖에 없었어"
description: "처음으로 노트를 클립했습니다"
_noteFavorited1:
title: "별을 바라보는 자"
description: "처음으로 노트를 즐겨찾기했습니다"
_profileFilled:
title: "준비 완료"
description: "프로필 설정을 완료했습니다"
_markedAsCat:
title: "나는 고양이다냥!"
description: "계정을 고양이로 설정했습니다냥"
flavor: "냐냐냐냐냐냐아아아아앙!"
_following1:
title: "첫 팔로우"
description: "사용자를 처음으로 팔로우했습니다"
_following10:
title: "팔로우, 팔로우"
description: "10명의 사용자를 팔로우했습니다"
_following50:
title: "친구 잔뜩"
description: "50명의 사용자를 팔로우했습니다"
_following100:
title: "주소록 한 권으론 부족해"
description: "100명의 사용자를 팔로우했습니다"
_following300:
title: "친구가 넘쳐나"
description: "300명의 사용자를 팔로우했습니다"
_followers1:
title: "첫 팔로워"
description: "사용자가 처음으로 팔로잉했습니다"
_followers10:
title: "날 따라와!"
description: "10명의 사용자가 팔로우했습니다"
_followers50:
title: "이곳저곳"
description: "50명의 사용자가 팔로우했습니다"
_followers100:
title: "인기왕"
description: "100명의 사용자가 팔로우했습니다"
_followers300:
title: "줄 좀 서봐요"
description: "100명의 사용자가 팔로우했습니다"
_followers500:
title: "기지국"
description: "500명의 사용자가 팔로우했습니다"
_followers1000:
title: "유명인사"
description: "1,000명의 사용자가 팔로우했습니다"
_collectAchievements30:
title: "도전과제 콜렉터"
description: "30개의 도전과제를 획득했습니다"
_viewAchievements3min:
title: "저 도전과제 좋아해요"
description: "도전과제 목록을 3분 이상 보세요"
_iLoveMisskey:
title: "I Love Misskey"
description: "\"I ❤ #Misskey\"를 포스트했습니다"
flavor: "Misskey를 이용해주셔서 감사합니다! - 개발팀 일동"
_client30min:
title: "잠깐 쉬어"
description: "클라이언트를 시작하고 30분이 경과하였습니다"
_noteDeletedWithin1min:
title: "있었는데요 없었습니다"
description: "노트를 포스트한 후 1분 이내에 삭제했습니다"
_postedAtLateNight:
title: "올빼미"
description: "한밤중에 노트를 포스트했습니다"
flavor: "잠 좀 자세요. 걱정돼요."
_postedAt0min0sec:
title: "정각"
description: "1초도 어긋나지 않은 정각에 노트를 포스트했습니다"
flavor: "째깍 째깍 째깍 땡!"
_selfQuote:
title: "혼잣말"
description: "자기 노트를 인용했습니다"
_htl20npm:
title: "타임라인 폭주 중"
description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었습니다"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "스크래치패드에서 hello world를 출력하세요"
_open3windows:
title: "멀티 윈도우"
description: "3개 이상의 창을 여세요"
_driveFolderCircularReference:
title: "순환 참조"
description: "드라이브 폴더를 자신을 가리키도록 만드려 시도했습니다"
_reactWithoutRead:
title: "읽고 답하긴 하시는 건가요?"
description: "100자가 넘는 포스트에 3초 안에 포스트했습니다"
_clickedClickHere:
title: "여길 눌러보세요"
description: "이 곳을 눌러봤습니다"
_justPlainLucky:
title: "그냥 운이 좋았어"
description: "매 10초마다 0.01%의 확률로 달성됩니다"
_setNameToSyuilo:
title: "신 콤플렉스"
description: "이름을 syuilo로 설정했습니다"
_passedSinceAccountCreated1:
title: "1년"
description: "계정을 생성하고 1년이 지났습니다"
_passedSinceAccountCreated2:
title: "2년"
description: "계정을 생성하고 2년이 지났습니다"
_passedSinceAccountCreated3:
title: "3년"
description: "계정을 생성하고 3년이 지났습니다"
_loggedInOnBirthday:
title: "생일 축하합니다!"
description: "설정한 생일에 로그인했습니다"
_loggedInOnNewYearsDay:
title: "새해 복 많이 받으세요"
description: "새해 첫 날에 로그인했습니다"
flavor: "올해에도 저희 인스턴스에 관심을 가져 주셔서 감사합니다"
_cookieClicked:
title: "쿠키 클리커 게임"
description: "쿠키를 클릭했습니다"
flavor: "뭔가 문제가 있나요?"
_brainDiver:
title: "Brain Diver"
description: "Brain Diver로의 링크를 첨부했습니다"
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "새 역할 생성" new: "새 역할 생성"
edit: "역할 수정" edit: "역할 수정"
name: "역할 이름" name: "역할 이름"
description: "역할 설명" description: "역할 설명"
permission: "역할의 권한" permission: "역할 권한"
descriptionOfPermission: "<b>모더레이터</b>는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n<b>관리자</b>는 인스턴스의 모든 설정을 변경할 수 있습니다." descriptionOfPermission: "<b>모더레이터</b>는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n<b>관리자</b>는 인스턴스의 모든 설정을 변경할 수 있습니다."
assignTarget: "할당 대상" assignTarget: "할당 대상"
descriptionOfAssignTarget: "<b>수동</b>을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n<b>조건부</b>를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다." descriptionOfAssignTarget: "<b>수동</b>을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n<b>조건부</b>를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다."
@ -948,7 +1179,7 @@ _role:
conditional: "조건부" conditional: "조건부"
condition: "조건" condition: "조건"
isConditionalRole: "조건부 역할입니다." isConditionalRole: "조건부 역할입니다."
isPublic: "공개 역할" isPublic: "역할 공개"
descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다." descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다."
options: "옵션" options: "옵션"
policies: "정책" policies: "정책"
@ -956,7 +1187,7 @@ _role:
useBaseValue: "기본값 사용" useBaseValue: "기본값 사용"
chooseRoleToAssign: "할당할 역할 선택" chooseRoleToAssign: "할당할 역할 선택"
canEditMembersByModerator: "모더레이터의 역할 수정 허용" canEditMembersByModerator: "모더레이터의 역할 수정 허용"
descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 추가하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 가능합니다." descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다."
priority: "우선순위" priority: "우선순위"
_priority: _priority:
low: "낮음" low: "낮음"
@ -971,19 +1202,20 @@ _role:
driveCapacity: "드라이브 용량" driveCapacity: "드라이브 용량"
pinMax: "고정할 수 있는 노트 수" pinMax: "고정할 수 있는 노트 수"
antennaMax: "최대 안테나 생성 허용 수" antennaMax: "최대 안테나 생성 허용 수"
wordMuteMax: "뮤트할 수 있는 단어의 수" wordMuteMax: "단어 뮤트할 수 있는 문자 수"
webhookMax: "생성할 수 있는 WebHook의 수" webhookMax: "생성할 수 있는 웹훅 수"
clipMax: "생성할 수 있는 클립 수" clipMax: "생성할 수 있는 클립 수"
noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수" noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수"
userListMax: "생성할 수 있는 리스트 수" userListMax: "생성할 수 있는 유저 리스트 수"
userEachUserListsMax: "리스트당 최대 사용자 수" userEachUserListsMax: "유저 리스트당 최대 사용자 수"
rateLimitFactor: "속도 제한" rateLimitFactor: "속도 제한"
descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다." descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다."
canHideAds: "광고 숨기기"
_condition: _condition:
isLocal: "로컬 사용자" isLocal: "로컬 사용자"
isRemote: "리모트 사용자" isRemote: "리모트 사용자"
createdLessThan: "다음 일수 이내에 가입한 유저" createdLessThan: "가압한 지 다음 일수 이내인 유저"
createdMoreThan: "다음 일수 이상 활동한 유저" createdMoreThan: "가입한 지 다음 일수 이상인 유저"
followersLessThanOrEq: "팔로워 수가 다음 이하인 유저" followersLessThanOrEq: "팔로워 수가 다음 이하인 유저"
followersMoreThanOrEq: "팔로워 수가 다음 이상인 유저" followersMoreThanOrEq: "팔로워 수가 다음 이상인 유저"
followingLessThanOrEq: "팔로잉 수가 다음 이하인 유저" followingLessThanOrEq: "팔로잉 수가 다음 이하인 유저"
@ -1583,6 +1815,7 @@ _notification:
pollEnded: "투표 결과가 발표되었습니다" pollEnded: "투표 결과가 발표되었습니다"
unreadAntennaNote: "안테나 {name}" unreadAntennaNote: "안테나 {name}"
emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다" emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
achievementEarned: "도전 과제를 달성했습니다"
_types: _types:
all: "전부" all: "전부"
follow: "팔로잉" follow: "팔로잉"

View file

@ -109,6 +109,7 @@ clickToShow: "Klik om te bekijken"
sensitive: "NSFW" sensitive: "NSFW"
add: "Toevoegen" add: "Toevoegen"
reaction: "Reacties" reaction: "Reacties"
reactions: "Reacties"
reactionSetting: "Reacties die in de reactie-selector worden getoond" reactionSetting: "Reacties die in de reactie-selector worden getoond"
reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen" reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen"
rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen" rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen"

View file

@ -110,6 +110,7 @@ clickToShow: "Kliknij, aby wyświetlić"
sensitive: "NSFW" sensitive: "NSFW"
add: "Dodaj" add: "Dodaj"
reaction: "Reakcja" reaction: "Reakcja"
reactions: "Reakcja"
reactionSetting: "Reakcje do pokazania w wyborniku reakcji" reactionSetting: "Reakcje do pokazania w wyborniku reakcji"
reactionSettingDescription2: "Przeciągnij aby zmienić kolejność, naciśnij aby usunąć, naciśnij „+” aby dodać" reactionSettingDescription2: "Przeciągnij aby zmienić kolejność, naciśnij aby usunąć, naciśnij „+” aby dodać"
rememberNoteVisibility: "Zapamiętuj ustawienia widoczności wpisu" rememberNoteVisibility: "Zapamiętuj ustawienia widoczności wpisu"

View file

@ -107,6 +107,7 @@ clickToShow: "Clique para ver"
sensitive: "Conteúdo sensível" sensitive: "Conteúdo sensível"
add: "Adicionar" add: "Adicionar"
reaction: "Reações" reaction: "Reações"
reactions: "Reações"
reactionSetting: "Quais reações a mostrar no selecionador de reações" reactionSetting: "Quais reações a mostrar no selecionador de reações"
reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar." reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar."
rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas"

View file

@ -107,6 +107,7 @@ clickToShow: "Click pentru a afișa"
sensitive: "NSFW" sensitive: "NSFW"
add: "Adaugă" add: "Adaugă"
reaction: "Reacție" reaction: "Reacție"
reactions: "Reacție"
reactionSetting: "Reacții care să apară in selectorul de reacții" reactionSetting: "Reacții care să apară in selectorul de reacții"
reactionSettingDescription2: "Trage pentru a rearanja, apasă pe \"+\" pentru a adăuga." reactionSettingDescription2: "Trage pentru a rearanja, apasă pe \"+\" pentru a adăuga."
rememberNoteVisibility: "Amintește setarea de vizibilitate a notelor" rememberNoteVisibility: "Amintește setarea de vizibilitate a notelor"

View file

@ -2,6 +2,7 @@
_lang_: "Русский" _lang_: "Русский"
headlineMisskey: "Сеть, сплетённая из заметок" headlineMisskey: "Сеть, сплетённая из заметок"
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀" introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
poweredByMisskeyDescription: "{name} один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом <b>Misskey</b>."
monthAndDay: "{day}.{month}" monthAndDay: "{day}.{month}"
search: "Поиск" search: "Поиск"
notifications: "Уведомления" notifications: "Уведомления"
@ -12,6 +13,7 @@ fetchingAsApObject: "Приём с других сайтов"
ok: "Окей" ok: "Окей"
gotIt: "Ясно!" gotIt: "Ясно!"
cancel: "Отмена" cancel: "Отмена"
noThankYou: "Нет, спасибо"
enterUsername: "Введите имя пользователя" enterUsername: "Введите имя пользователя"
renotedBy: "{user} делится" renotedBy: "{user} делится"
noNotes: "Нет ни одной заметки" noNotes: "Нет ни одной заметки"
@ -47,6 +49,7 @@ deleteAndEdit: "Удалить и отредактировать"
deleteAndEditConfirm: "Удалить эту заметку и создать отредактированную? Все реакции, ссылки и ответы на существующую будут будут потеряны." deleteAndEditConfirm: "Удалить эту заметку и создать отредактированную? Все реакции, ссылки и ответы на существующую будут будут потеряны."
addToList: "Добавить в список" addToList: "Добавить в список"
sendMessage: "Отправить сообщение" sendMessage: "Отправить сообщение"
copyRSS: "Скопировать RSS"
copyUsername: "Скопировать имя пользователя" copyUsername: "Скопировать имя пользователя"
searchUser: "Поиск людей" searchUser: "Поиск людей"
reply: "Ответить" reply: "Ответить"
@ -107,6 +110,7 @@ clickToShow: "Нажмите для просмотра"
sensitive: "Содержимое не для всех" sensitive: "Содержимое не для всех"
add: "Добавить" add: "Добавить"
reaction: "Реакции" reaction: "Реакции"
reactions: "Реакции"
reactionSetting: "Реакции, отображаемые в палитре" reactionSetting: "Реакции, отображаемые в палитре"
reactionSettingDescription2: "Расставляйте перетаскиванием, удаляйте нажатием, добавляйте кнопкой «+»." reactionSettingDescription2: "Расставляйте перетаскиванием, удаляйте нажатием, добавляйте кнопкой «+»."
rememberNoteVisibility: "Запоминать видимость заметок" rememberNoteVisibility: "Запоминать видимость заметок"
@ -451,6 +455,7 @@ language: "Язык"
uiLanguage: "Язык интерфейса" uiLanguage: "Язык интерфейса"
groupInvited: "Приглашение в группу" groupInvited: "Приглашение в группу"
aboutX: "Описание {x}" aboutX: "Описание {x}"
emojiStyle: "Стиль эмодзи"
disableDrawer: "Не использовать выдвижные меню" disableDrawer: "Не использовать выдвижные меню"
youHaveNoGroups: "У вас нет ни одной группы" youHaveNoGroups: "У вас нет ни одной группы"
joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные" joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные"
@ -708,6 +713,7 @@ accentColor: "Акцент"
textColor: "Текст" textColor: "Текст"
saveAs: "Сохранить под названием…" saveAs: "Сохранить под названием…"
advanced: "Для продвинутых" advanced: "Для продвинутых"
advancedSettings: "Расширенные настройки "
value: "Значения" value: "Значения"
createdAt: "Создано" createdAt: "Создано"
updatedAt: "Обновлено" updatedAt: "Обновлено"
@ -839,40 +845,351 @@ numberOfColumn: "Количество столбцов"
searchByGoogle: "Поиск" searchByGoogle: "Поиск"
instanceDefaultLightTheme: "Светлая тема по умолчанию" instanceDefaultLightTheme: "Светлая тема по умолчанию"
instanceDefaultDarkTheme: "Темная тема по умолчанию" instanceDefaultDarkTheme: "Темная тема по умолчанию"
instanceDefaultThemeDescription: "Описание темы по умолчанию для инстанса"
mutePeriod: "Продолжительность скрытия" mutePeriod: "Продолжительность скрытия"
indefinitely: "вечно" indefinitely: "вечно"
tenMinutes: "10 минут" tenMinutes: "10 минут"
oneHour: "1 час" oneHour: "1 час"
oneDay: "1 день" oneDay: "1 день"
oneWeek: "1 неделя" oneWeek: "1 неделя"
reflectMayTakeTime: "Изменения могут занять время для отображения"
failedToFetchAccountInformation: "Не удалось получить информацию об аккаунте"
cropImage: "Кадрирование" cropImage: "Кадрирование"
cropImageAsk: "Нужно ли кадрировать изображение?" cropImageAsk: "Нужно ли кадрировать изображение?"
file: "Файлы" file: "Файлы"
recentNHours: "Последние {n} ч" recentNHours: "Последние {n} ч"
recentNDays: "Последние {n} сут" recentNDays: "Последние {n} сут"
noEmailServerWarning: "Почтовый сервер не установлен "
thereIsUnresolvedAbuseReportWarning: "Остались нерешённые жалобы"
recommended: "Рекомендуем" recommended: "Рекомендуем"
check: "Проверить" check: "Проверить"
driveCapOverrideLabel: "Изменение лимита дискового пространства для этого пользователя" driveCapOverrideLabel: "Изменение лимита дискового пространства для этого пользователя"
driveCapOverrideCaption: "Укажите меньше или равное нулю для отмены"
requireAdminForView: "Для просмотра необходимо иметь аккаунт администратора"
isSystemAccount: "Данная учётная запись создана автоматически и управляется системой"
typeToConfirm: "Введите {x} для продолжения"
deleteAccount: "Удаление учётной записи" deleteAccount: "Удаление учётной записи"
document: "Документ"
numberOfPageCache: "Количество сохранённых страниц в кэше"
numberOfPageCacheDescription: "Описание количества страниц в кэше"
logoutConfirm: "Вы хотите выйти из аккаунта?"
lastActiveDate: "Последняя дата использования"
statusbar: "Статусбар"
pleaseSelect: "Пожалуйста, выберите"
reverse: "Переворот" reverse: "Переворот"
colored: "Выделена цветом" colored: "Выделена цветом"
refreshInterval: "Интервал перезагрузки"
label: "Метка" label: "Метка"
type: "Тип"
speed: "Скорость"
sensitiveMediaDetection: "Определение содержимого деликатного характера"
localOnly: "Локально" localOnly: "Локально"
remoteOnly: "Только удалённо"
failedToUpload: "Сбой выгрузки"
cannotUploadBecauseInappropriate: "Файл не может быть загружен, так как было установлено, что он может содержать неприемлемое содержимое."
cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске"
beta: "Бета" beta: "Бета"
enableAutoSensitive: "Автоматическое определение NSFW" enableAutoSensitive: "Автоматическое определение NSFW"
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта." enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта."
navbar: "Панель навигации"
shuffle: "Перемешать"
account: "Учётные записи" account: "Учётные записи"
move: "Переместить"
pushNotification: "Push-уведомления"
subscribePushNotification: "Включить push-уведомления"
unsubscribePushNotification: "Выключить push-уведомления"
pushNotificationAlreadySubscribed: "Push-уведомления уже включены"
pushNotificationNotSupported: "Push-уведмления не поддерживаются инстансом или браузером"
sendPushNotificationReadMessage: "Удалять push-уведомления когда сообщение или прочитано"
sendPushNotificationReadMessageCaption: "На мгновение появится уведомление \"{emptyPushNotificationMessage}\". Расход заряда батареи может увеличиться "
windowMaximize: "Развернуть" windowMaximize: "Развернуть"
windowRestore: "Восстановить" windowRestore: "Восстановить"
caption: "Подпись (Automatic Translation)"
loggedInAsBot: "Вы под аккаунтом бота!"
tools: "Инструменты"
cannotLoad: "Не удалось загрузить"
numberOfProfileView: "Количество профилей для просмотра"
like: "Нравится!" like: "Нравится!"
unlike: "Отменить «нравится»"
numberOfLikes: "Количество лайков"
show: "Отображение" show: "Отображение"
neverShow: "Больше не показывать"
remindMeLater: "Напомнить позже"
didYouLikeMisskey: "Вам нравится Misskey?"
pleaseDonate: "Сайт {host} работает на Misskey. Это бесплатное программное обеспечение, и ваши пожертвования очень бы помогли продолжать его разработку!"
roles: "Роли"
role: "Роль"
normalUser: "Обычный пользователь"
undefined: "неопределён"
assign: "Назначить"
unassign: "Отменить назначение"
color: "Цвет" color: "Цвет"
manageCustomEmojis: "Управлять пользовательскими эмодзи"
youCannotCreateAnymore: "Вы достигли лимита создания."
cannotPerformTemporary: "Временно недоступен"
cannotPerformTemporaryDescription: "Это действие временно невозможно выполнить из-за превышения лимита выполнения."
preset: "Шаблоны"
selectFromPresets: "Выбрать из шаблонов"
achievements: "Достижения"
_achievements:
earnedAt: "Разблокировано в"
_types:
_notes1:
title: "Первые шаги в Misskey"
description: "Опубликована первая заметка"
flavor: "Приятных дней с Misskey!"
_notes10:
title: "Несколько заметок"
description: "Опубликовано 10 заметок"
_notes100:
title: "Много заметок"
description: "Опубликовано 100 заметок"
_notes500:
title: "Всё в заметках"
description: "Опубликовано 500 заметок"
_notes1000:
title: "Гора заметок"
description: "Опубликовано 1000 заметок"
_notes5000:
title: "Заметки льются рекой"
description: "Опубликовано 5000 заметок"
_notes10000:
title: "Превосходство в заметках"
description: "Опубликовано 10000 заметок"
_notes20000:
title: "Нужно больше заметок!"
description: "Опубликовано 20000 заметок"
_notes30000:
title: "Заметки, заметки, заметки"
description: "Опубликовано 30000 заметок"
_notes40000:
title: "Фабрика заметок"
description: "Опубликовано 40000 заметок"
_notes50000:
title: "Планета заметок"
description: "Опубликовано 50000 заметок"
_notes60000:
title: "Замет-квазар"
description: "Опубликовано 60000 заметок"
_notes70000:
title: "Чёрная дыра из заметок"
description: "Опубликовано 70000 заметок"
_notes80000:
title: "Галактика заметок"
description: "Опубликовано 80000 заметок"
_notes90000:
title: "Вселенная заметок"
description: "Опубликовано 90000 заметок"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "Опубликовано 100000 заметок"
flavor: "Вам правда нужно столько писать?"
_login3:
title: "Новичок "
description: "3 дня на сайте"
flavor: "С сегодняшнего дня зовите меня просто мискиец"
_login7:
title: "Новичок Ⅱ"
description: "Неделя на сайте"
flavor: "Кажется, вы начали свыкаться с этим, нет?"
_login15:
title: "Новичок Ⅲ"
description: "15 дней на сайте"
_login30:
title: "Мискиец "
description: "30 дней на сайте"
_login60:
title: "Мискиец Ⅱ"
description: "60 дней на сайте"
_login100:
title: "Мискиец Ⅲ"
description: "100 дней на сайте"
flavor: "Жестокий Misskist "
_login200:
title: "Завсегдатай "
description: "200 дней на сайте"
_login300:
title: "Завсегдатай Ⅱ"
description: "300 дней на сайте"
_login400:
title: "Завсегдатай Ⅲ"
description: "400 дней на сайте"
_login500:
title: "Ветеран "
description: "500 дней на сайте"
flavor: "Господа, я люблю заметки"
_login600:
title: "Ветеран Ⅱ"
description: "600 дней на сайте"
_login700:
title: "Ветеран Ⅲ"
description: "700 дней на сайте"
_login800:
title: "Повелитель заметок "
description: "800 дней на сайте"
_login900:
title: "Повелитель заметок Ⅱ"
description: "900 дней на сайте"
_login1000:
title: "Повелитель заметок Ⅲ"
description: "1000 дней на сайте"
flavor: "Спасибо, что пользуетесь Misskey!"
_noteClipped1:
title: "Нельзя не сохранить"
description: "Первая заметка в подборке"
_noteFavorited1:
title: "Смотрящий на звёзды"
description: "Первое добавление в избранное"
_myNoteFavorited1:
title: "В поиске звёзд"
description: "Кому-то понравилась ваша заметка"
_profileFilled:
title: "Приготовления закончены"
description: "Заполнен профиль"
_markedAsCat:
title: "Ваш покорный слуга кот"
description: "Включена опция «Аккаунт кота»"
flavor: "Позвольте представиться: я — кот, просто кот, у меня еще нет имени."
_following1:
title: "Я не один"
description: "Сделана первая подписка"
_following10:
title: "Не останавливайся… Не останавливайся…"
description: "Количество подписок достигло 10"
_following50:
title: "Много друзей"
description: "Количество подписок достигло 50"
_following100:
title: "Сотня друзей"
description: "Количество подписок достигло 100"
_following300:
title: "Друзья в избытке"
description: "Количество подписок достигло 300"
_followers1:
title: "Первый подписчик"
description: "Появился 1 подписчик"
_followers10:
title: "Следуй за мной!"
description: "Количество подписчиков достигло 10"
_followers50:
title: "Один за другим"
description: "Количество подписчиков достигло 50"
_followers100:
title: "Всеобщий любимец"
description: "Количество подписчиков достигло 100"
_followers300:
title: "В очередь!"
description: "Количество подписчиков достигло 300"
_followers500:
title: "Радиостанция"
description: "Количество подписчиков достигло 500"
_followers1000:
title: "Авторитет"
description: "Количество подписчиков достигло 1000"
_collectAchievements30:
title: "Достигатор"
description: "Получено 30 достижений"
_viewAchievements3min:
title: "Любовь к успехам"
description: "Более 3 минут любования достижениями"
_iLoveMisskey:
title: "Я люблю Misskey"
description: "Написана заметка «I ❤ #Misskey»"
flavor: "Спасибо за поддержку Misskey! Ваша команда разработчиков"
_client30min:
title: "Перерыв на обед"
description: "Прошло 30 минут с момента запуска клиента"
_noteDeletedWithin1min:
title: "Ой, нет!"
description: "Заметка удалена через минуту после публикации"
_postedAtLateNight:
title: "Житель ночи"
description: "Заметка опубликована в глухую ночь"
flavor: "Вроде бы пора спать"
_postedAt0min0sec:
title: "Говорящие часы"
description: "Заметка опубликована ровно в 0 минут 0 секунд"
flavor: "Дин-дон дин-дон"
_selfQuote:
title: "Самовоспроизведение"
description: "Процитирована собственная заметка"
_htl20npm:
title: "В потоке"
description: "Достигнута скорость домашней ленты в 20 з/мин (заметок минуту)"
_outputHelloWorldOnScratchpad:
title: "Привет, мир!"
description: "Выведен текст «hello world» в Когтеточке"
_open3windows:
title: "Многооконный"
description: "Открыто одновременно 3 окна"
_driveFolderCircularReference:
title: "Циклическая ссылка"
description: "Попытка создать на «диске» рекурсивно вложенную папку"
_reactWithoutRead:
title: "Не читай @ отвечай!"
description: "На заметку более чем 100 знаков написан ответ в первые же 3 секунды с её появления."
_clickedClickHere:
title: "Нажмите здесь"
description: "Нажато здесь"
_justPlainLucky:
title: "Чистая удача"
description: "Может достаться с вероятностью 0,01% каждые 10 секунд."
_setNameToSyuilo:
title: "Комплекс бога"
description: "Установлено «syuilo» в качестве имени"
_passedSinceAccountCreated1:
title: "Первая годовщина"
description: "Прошёл 1 год с момента регистрации"
_passedSinceAccountCreated2:
title: "Вторая годовщина"
description: "Прошло 2 года с момента регистрации"
_passedSinceAccountCreated3:
title: "Третья годовщина"
description: "Прошло 3 года с момента регистрации"
_loggedInOnBirthday:
title: "С днём рождения!"
description: "Вход на сайт в свой день рождения"
_loggedInOnNewYearsDay:
title: "С Новым годом!"
description: "Вход на сайт в первый день года"
flavor: "Желаем отличного года на нашем сайте!"
_cookieClicked:
title: "Игра, в которой вы щёлкаете по печенькам"
description: "Нажато печенье"
flavor: "Стоп, вы вообще на том сайте-то?"
_brainDiver:
title: "Brain Diver"
description: "Опубликована ссылка на песню «Brain Diver»"
flavor: "Мисски-Мисски Ла-Ту-Ма"
_role: _role:
new: "Новая роль"
edit: "Изменить роль"
name: "Название роли"
description: "Описание роли"
permission: "Ролевые полномочия"
descriptionOfPermission: "<b>Модераторы</b> могут изменять базовые операции для модераторов.\n<b>Администраторы</b> могут изменять полностью настройки инстанса."
assignTarget: "Метод присвоения"
descriptionOfAssignTarget: "<b>Вручную</b> чтобы указать кому выдавать роль, а кому нет.\n<b>По условию<b> чтобы автоматически выдавать и удалять роль при условиях."
manual: "Вручную"
conditional: "По условию"
condition: "Условия"
isConditionalRole: "Эта роль выдаётся по условию."
isPublic: "Общедоступная роль"
descriptionOfIsPublic: "Список тех, кому назначена эта роль будет доступен всем. Кроме того эта роль будет отмечена у каждого в профиле."
options: "Настройки ролей"
policies: "Политики"
baseRole: "Шаблон роли"
useBaseValue: "Использовать значение из шаблона"
chooseRoleToAssign: "Выберите роль, которую хотите выдать"
canEditMembersByModerator: "Могут назначать модераторы"
descriptionOfCanEditMembersByModerator: "Если включено, на эту роль могут назначать пользователей как администраторы, так и модераторы. Если выключено, назначать могут только администраторы."
priority: "Приоритет" priority: "Приоритет"
_priority: _priority:
low: "Низкий" low: "Низкий"
middle: "Средне" middle: "Средне"
high: "Высокий" high: "Высокий"
_options:
canManageCustomEmojis: "Управлять пользовательскими эмодзи"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно." description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
setSensitiveFlagAutomatically: "Установить флаг NSFW" setSensitiveFlagAutomatically: "Установить флаг NSFW"
@ -919,6 +1236,11 @@ _plugin:
install: "Установка расширений" install: "Установка расширений"
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете." installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
manage: "Управление расширениями" manage: "Управление расширениями"
_preferencesBackups:
saveConfirm: "Сохранить бэкап как {name}?"
deleteConfirm: "Удалить резервную копию {name}?"
renameConfirm: "Переименовать резервную копию с \"{old}\" на \"{new}\"?"
noBackups: "Резервной копии не существует. Вы можете создать резервную копию в настройках на этом инстансе с помощью \"Создать новую резервную копию\"."
_registry: _registry:
scope: "Область" scope: "Область"
key: "Ключ" key: "Ключ"
@ -1415,6 +1737,7 @@ _notification:
youReceivedFollowRequest: "У вас новый запрос на подписку." youReceivedFollowRequest: "У вас новый запрос на подписку."
yourFollowRequestAccepted: "Ваш запрос на подписку одобрен." yourFollowRequestAccepted: "Ваш запрос на подписку одобрен."
youWereInvitedToGroup: "Вы приглашены в группу." youWereInvitedToGroup: "Вы приглашены в группу."
achievementEarned: "Получено достижение"
_types: _types:
all: "Все" all: "Все"
follow: "Подписки" follow: "Подписки"

View file

@ -110,6 +110,7 @@ clickToShow: "Kliknutím zobrazíte"
sensitive: "NSFW" sensitive: "NSFW"
add: "Pridať" add: "Pridať"
reaction: "Reakcie" reaction: "Reakcie"
reactions: "Reakcie"
reactionSetting: "Reakcie zobrazené vo výbere reakcií" reactionSetting: "Reakcie zobrazené vo výbere reakcií"
reactionSettingDescription2: "Ťahaním preusporiadate, kliknutím odstránite, Stlačením \"+\" pridáte" reactionSettingDescription2: "Ťahaním preusporiadate, kliknutím odstránite, Stlačením \"+\" pridáte"
rememberNoteVisibility: "Zapamätať nastavenia viditeľnosti poznámky" rememberNoteVisibility: "Zapamätať nastavenia viditeľnosti poznámky"

View file

@ -110,6 +110,7 @@ clickToShow: "Klicka för att visa"
sensitive: "Känsligt innehåll" sensitive: "Känsligt innehåll"
add: "Lägg till" add: "Lägg till"
reaction: "Reaktioner" reaction: "Reaktioner"
reactions: "Reaktioner"
reactionSetting: "Reaktioner som ska visas i reaktionsväljaren" reactionSetting: "Reaktioner som ska visas i reaktionsväljaren"
reactionSettingDescription2: "Dra för att omordna, klicka för att radera, tryck \"+\" för att lägga till." reactionSettingDescription2: "Dra för att omordna, klicka för att radera, tryck \"+\" för att lägga till."
rememberNoteVisibility: "Komihåg notvisningsinställningar" rememberNoteVisibility: "Komihåg notvisningsinställningar"

View file

@ -110,6 +110,7 @@ clickToShow: "คลิกเพื่อแสดง"
sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW" sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW"
add: "เพิ่ม" add: "เพิ่ม"
reaction: "รีแอคชั่น" reaction: "รีแอคชั่น"
reactions: "รีแอคชั่น"
reactionSetting: "รีแอคชั่นไปยังแสดงผลในตัวเลือกการรีแอคชั่น" reactionSetting: "รีแอคชั่นไปยังแสดงผลในตัวเลือกการรีแอคชั่น"
reactionSettingDescription2: "กดลากเพื่อจัดลำดับใหม่ กดคลิกเพื่อลบ กด \"+\" เพื่อเพิ่ม" reactionSettingDescription2: "กดลากเพื่อจัดลำดับใหม่ กดคลิกเพื่อลบ กด \"+\" เพื่อเพิ่ม"
rememberNoteVisibility: "จดจำการตั้งค่าการมองเห็นตัวโน้ต" rememberNoteVisibility: "จดจำการตั้งค่าการมองเห็นตัวโน้ต"
@ -932,6 +933,23 @@ assign: "กำหนด"
unassign: "ยังไม่มอบหมาย" unassign: "ยังไม่มอบหมาย"
color: "สี" color: "สี"
manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ"
cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว"
cannotPerformTemporaryDescription: "การดําเนินการนี้ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้งนะค่ะ"
preset: "พรีเซ็ต"
selectFromPresets: "เลือกจากการพรีเซ็ต"
achievements: "ความสำเร็จ"
_achievements:
earnedAt: "ได้รับเมื่อ"
_types:
_followers100:
title: "บุคคลที่เป็นที่นิยม"
_followers500:
title: "เสาสัญญาณ"
_iLoveMisskey:
title: "ฉันรัก Misskey"
_driveFolderCircularReference:
title: "อ้างอิงวงจร"
_role: _role:
new: "บทบาทใหม่" new: "บทบาทใหม่"
edit: "แก้ไขบทบาท" edit: "แก้ไขบทบาท"
@ -948,6 +966,7 @@ _role:
isPublic: "บทบาทสาธารณะ" isPublic: "บทบาทสาธารณะ"
descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย" descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย"
options: "ตัวเลือกบทบาท" options: "ตัวเลือกบทบาท"
policies: "นโยบาย"
baseRole: "บทบาทพื้นฐาน" baseRole: "บทบาทพื้นฐาน"
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
@ -965,7 +984,17 @@ _role:
canInvite: "สร้างรหัสเชิญอินสแตนซ์" canInvite: "สร้างรหัสเชิญอินสแตนซ์"
canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
driveCapacity: "ความจุของไดรฟ์" driveCapacity: "ความจุของไดรฟ์"
pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้"
antennaMax: "จำนวนสูงสุดของเสาอากาศ" antennaMax: "จำนวนสูงสุดของเสาอากาศ"
wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ"
webhookMax: "จำนวนเว็บฮุคสูงสุด"
clipMax: "จำนวนคลิปสูงสุด"
noteEachClipsMax: "จำนวนโน้ตสูงสุดภายในคลิป"
userListMax: "จำนวนรายชื่อผู้ใช้สูงสุด"
userEachUserListsMax: "จำนวนผู้ใช้สูงสุดภายในรายการผู้ใช้"
rateLimitFactor: "ขีดจำกัดอัตรา"
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
canHideAds: "ซ่อนโฆษณา"
_condition: _condition:
isLocal: "ผู้ใช้ภายใน" isLocal: "ผู้ใช้ภายใน"
isRemote: "ผู้ใช้ระยะไกล" isRemote: "ผู้ใช้ระยะไกล"
@ -1570,6 +1599,7 @@ _notification:
pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน" pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน"
unreadAntennaNote: "เสาอากาศ {name}" unreadAntennaNote: "เสาอากาศ {name}"
emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว" emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว"
achievementEarned: "รับความสำเร็จ"
_types: _types:
all: "ทั้งหมด" all: "ทั้งหมด"
follow: "กำลังติดตาม" follow: "กำลังติดตาม"

View file

@ -109,6 +109,7 @@ clickToShow: "Натисніть для перегляду"
sensitive: "NSFW" sensitive: "NSFW"
add: "Додати" add: "Додати"
reaction: "Реакції" reaction: "Реакції"
reactions: "Реакції"
reactionSetting: "Налаштування реакцій" reactionSetting: "Налаштування реакцій"
reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати." reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати."
rememberNoteVisibility: "Пам’ятати параметри видимісті" rememberNoteVisibility: "Пам’ятати параметри видимісті"
@ -586,7 +587,7 @@ pluginTokenRequestedDescription: "Цей плагін зможе викорис
notificationType: "Тип сповіщення" notificationType: "Тип сповіщення"
edit: "Редагувати" edit: "Редагувати"
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий" useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
emailServer: "Сервер електронної пошти" emailServer: "Email сервер"
enableEmail: "Увімкнути функцію доставки пошти" enableEmail: "Увімкнути функцію доставки пошти"
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю." emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
email: "E-mail" email: "E-mail"
@ -892,9 +893,21 @@ unsubscribePushNotification: "Вимкнути push-сповіщення"
windowMaximize: "Розгорнути" windowMaximize: "Розгорнути"
windowRestore: "Відновити" windowRestore: "Відновити"
caption: "Підпис" caption: "Підпис"
tools: "Інструменти"
like: "Вподобати" like: "Вподобати"
unlike: "Не вподобати"
numberOfLikes: "Вподобання"
show: "Відображення" show: "Відображення"
color: "Колір" color: "Колір"
achievements: "Досягнення"
_achievements:
_types:
_notes1:
title: "налаштовую свій msky"
description: "Перша нотатка"
flavor: "Приємного часу з Misskey!"
_notes10:
title: "Декілька нотаток"
_role: _role:
priority: "Пріоритет" priority: "Пріоритет"
_priority: _priority:

View file

@ -107,6 +107,7 @@ clickToShow: "Nhấn để xem"
sensitive: "Nhạy cảm" sensitive: "Nhạy cảm"
add: "Thêm" add: "Thêm"
reaction: "Biểu cảm" reaction: "Biểu cảm"
reactions: "Biểu cảm"
reactionSetting: "Chọn những biểu cảm hiển thị" reactionSetting: "Chọn những biểu cảm hiển thị"
reactionSettingDescription2: "Kéo để sắp xếp, nhấn để xóa, nhấn \"+\" để thêm." reactionSettingDescription2: "Kéo để sắp xếp, nhấn để xóa, nhấn \"+\" để thêm."
rememberNoteVisibility: "Lưu kiểu tút mặc định" rememberNoteVisibility: "Lưu kiểu tút mặc định"

View file

@ -110,6 +110,7 @@ clickToShow: "点击以显示"
sensitive: "敏感内容" sensitive: "敏感内容"
add: "添加" add: "添加"
reaction: "回应" reaction: "回应"
reactions: "回应"
reactionSetting: "在选择器中显示的回应" reactionSetting: "在选择器中显示的回应"
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。" reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
rememberNoteVisibility: "保存上次设置的可见性" rememberNoteVisibility: "保存上次设置的可见性"
@ -607,7 +608,7 @@ wordMute: "文字屏蔽"
regexpError: "正则表达式错误" regexpError: "正则表达式错误"
regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:" regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:"
instanceMute: "实例的屏蔽" instanceMute: "实例的屏蔽"
userSaysSomething: "{name}说了什么,但是被屏蔽了" userSaysSomething: "{name}说了什么,但是被屏蔽词过滤了"
makeActive: "启用" makeActive: "启用"
display: "显示" display: "显示"
copy: "复制" copy: "复制"
@ -826,7 +827,7 @@ makeReactionsPublicDescription: "将您发表过的回应设置成公开可见
classic: "经典" classic: "经典"
muteThread: "屏蔽帖子列表" muteThread: "屏蔽帖子列表"
unmuteThread: "取消屏蔽帖子列表" unmuteThread: "取消屏蔽帖子列表"
ffVisibility: "连接的可见范围" ffVisibility: "关注关系的可见范围"
ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围" ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围"
continueThread: "查看更多帖子" continueThread: "查看更多帖子"
deleteAccountConfirm: "将要删除账户。是否确认?" deleteAccountConfirm: "将要删除账户。是否确认?"
@ -937,6 +938,113 @@ cannotPerformTemporary: "暂时不可用"
cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,请稍后再试。" cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,请稍后再试。"
preset: "預設值" preset: "預設值"
selectFromPresets: "從預設值中選擇" selectFromPresets: "從預設值中選擇"
achievements: "成就"
_achievements:
earnedAt: "达成时间"
_types:
_notes1:
title: "初来乍到"
description: "第一次发帖"
flavor: "祝您在Misskey玩的愉快"
_notes10:
title: "一些帖子"
description: "发布了10篇帖子"
_notes100:
title: "很多帖子"
description: "发布了100篇帖子"
_notes500:
title: "满是帖子"
description: "发布了500篇帖子"
_notes1000:
title: "积帖成山"
description: "发布了1,000篇帖子"
_notes5000:
title: "帖如泉涌"
description: "发布了5,000篇帖子"
_notes10000:
title: "超级帖"
description: "发布了10,000篇帖子"
_notes20000:
title: "还想要更多帖子"
description: "发布了20,000篇帖子"
_notes30000:
title: "帖子帖子帖子"
description: "发布了30,000篇帖子"
_notes40000:
title: "帖子工厂"
description: "发布了40,000篇帖子"
_notes50000:
title: "帖子星球"
description: "发布了50,000篇帖子"
_notes60000:
title: "帖子类星体"
description: "发布了60,000篇帖子"
_notes70000:
title: "帖子黑洞"
description: "发布了70,000篇帖子"
_notes80000:
title: "帖子星系"
description: "发布了80,000篇帖子"
_notes90000:
title: "帖子起源"
description: "发布了90,000篇帖子"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "发布了100,000篇帖子"
flavor: "真的有那么多可以写的东西吗?"
_login3:
title: "初学者 I"
description: "连续登录3天"
_login7:
description: "连续登录7天"
_login15:
description: "连续登录15天"
_login30:
description: "连续登录30天"
_login60:
description: "连续登录60天"
_login1000:
flavor: "感谢您使用Misskey"
_noteFavorited1:
title: "观星者"
_markedAsCat:
title: "我是猫"
description: "将账户设定为一只猫"
_following10:
title: "关注,跟随"
_following50:
title: "我的朋友很多"
_following300:
description: "关注数超过300"
_followers100:
title: "胜友如云"
_collectAchievements30:
description: "获得超过30个成就"
_viewAchievements3min:
description: "盯着成就看三分钟"
_iLoveMisskey:
title: "I Love Misskey"
description: "发布\"I ❤ #Misskey\"帖子"
flavor: "感谢您使用 Misskey by 开发团队"
_noteDeletedWithin1min:
description: "发帖后一分钟内就将其删除"
_postedAtLateNight:
title: "夜行者"
description: "深夜发布帖子"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
_passedSinceAccountCreated1:
description: "账户创建时间超过1年"
_passedSinceAccountCreated2:
description: "账户创建时间超过2年"
_passedSinceAccountCreated3:
description: "账户创建时间超过3年"
_loggedInOnBirthday:
title: "生日快乐"
description: "在生日当天登录"
_loggedInOnNewYearsDay:
title: "恭贺新禧"
description: "在元旦登入"
_role: _role:
new: "创建角色" new: "创建角色"
edit: "编辑角色" edit: "编辑角色"
@ -981,6 +1089,7 @@ _role:
userEachUserListsMax: "单个用户列表内用户数量限制" userEachUserListsMax: "单个用户列表内用户数量限制"
rateLimitFactor: "速率限制" rateLimitFactor: "速率限制"
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
canHideAds: "可以隐藏广告"
_condition: _condition:
isLocal: "是本地用户" isLocal: "是本地用户"
isRemote: "是远程用户" isRemote: "是远程用户"
@ -1008,7 +1117,7 @@ _emailUnavailable:
mx: "邮件服务器不正确" mx: "邮件服务器不正确"
smtp: "邮件服务器没有响应" smtp: "邮件服务器没有响应"
_ffVisibility: _ffVisibility:
public: "发布" public: "公开"
followers: "只有关注你的用户能看到" followers: "只有关注你的用户能看到"
private: "私密" private: "私密"
_signup: _signup:
@ -1585,6 +1694,7 @@ _notification:
pollEnded: "问卷调查结果已生成。" pollEnded: "问卷调查结果已生成。"
unreadAntennaNote: "天线 {name}" unreadAntennaNote: "天线 {name}"
emptyPushNotificationMessage: "推送通知已更新" emptyPushNotificationMessage: "推送通知已更新"
achievementEarned: "获得成就"
_types: _types:
all: "全部" all: "全部"
follow: "关注中" follow: "关注中"

View file

@ -110,6 +110,7 @@ clickToShow: "按一下以顯示"
sensitive: "敏感內容" sensitive: "敏感內容"
add: "新增" add: "新增"
reaction: "情感" reaction: "情感"
reactions: "情感"
reactionSetting: "在選擇器中顯示反應" reactionSetting: "在選擇器中顯示反應"
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。" reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。"
rememberNoteVisibility: "記住貼文可見性" rememberNoteVisibility: "記住貼文可見性"
@ -389,7 +390,7 @@ administrator: "管理員"
token: "權杖" token: "權杖"
twoStepAuthentication: "兩階段驗證" twoStepAuthentication: "兩階段驗證"
moderator: "監察員" moderator: "監察員"
moderation: "言論調節" moderation: "監察"
nUsersMentioned: "提到了{n}" nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰" securityKey: "安全金鑰"
securityKeyName: "金鑰名稱" securityKeyName: "金鑰名稱"
@ -932,8 +933,242 @@ assign: "指派"
unassign: "取消指派" unassign: "取消指派"
color: "顏色" color: "顏色"
manageCustomEmojis: "管理自訂表情符號" manageCustomEmojis: "管理自訂表情符號"
youCannotCreateAnymore: "您無法再建立更多了。"
cannotPerformTemporary: "暫時無法進行" cannotPerformTemporary: "暫時無法進行"
cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無法進行。請過一段時間之後再嘗試。" cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無法進行。請過一段時間之後再嘗試。"
preset: "預設值"
selectFromPresets: "從預設值中選擇"
achievements: "成就"
_achievements:
earnedAt: "獲得日期"
_types:
_notes1:
title: "just setting up my msky"
description: "發出了第一則貼文"
flavor: "祝您的Misskey生活愉快"
_notes10:
title: "若干貼文"
description: "發表了10則貼文"
_notes100:
title: "許多的貼文"
description: "發表了100則貼文"
_notes500:
title: "滿滿的貼文"
description: "發表了500則貼文"
_notes1000:
title: "一堆貼文"
description: "發表了1000則貼文"
_notes5000:
title: "滔滔不絕的貼文"
description: "發表了5000則貼文"
_notes10000:
title: "超級貼文"
description: "發表了10000則貼文"
_notes20000:
title: "需要更多的貼文"
description: "發表了20000則貼文"
_notes30000:
title: "貼文貼文貼文"
description: "發表了30000則貼文"
_notes40000:
title: "貼文工廠"
description: "發表了40000則貼文"
_notes50000:
title: "貼文星球"
description: "發表了50000則貼文"
_notes60000:
title: "貼文類星體"
description: "發表了60000則貼文"
_notes70000:
title: "貼文黑洞"
description: "發表了70000則貼文"
_notes80000:
title: "貼文銀河"
description: "發表了80000則貼文"
_notes90000:
title: "貼文宇宙"
description: "發表了90000則貼文"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "發表了100,000則貼文"
flavor: "有這麼多東西要寫嗎?"
_login3:
title: "初學者Ⅰ"
description: "總登入天數為3天"
flavor: "從今天開始我就是Misskeyist"
_login7:
title: "初學者ⅠⅠ"
description: "總登入天數為7天"
flavor: "您開始習慣了嗎?"
_login15:
title: "初學者III"
description: "總登入天數為15天"
_login30:
title: "Misskeyist "
description: "總登入天數為30天"
_login60:
title: "Misskeyist "
description: "總登入天數為60天"
_login100:
title: "Misskeyist "
description: "總登入天數為100天"
flavor: "辣個 Misskeyist 用戶"
_login200:
title: "普通Ⅰ"
description: "總登入天數為200天"
_login300:
title: "普通I"
description: "總登入天數為300天"
_login400:
title: "普通II"
description: "總登入天數為400天"
_login500:
title: "老兵Ⅰ"
description: "總登入天數為500天"
flavor: "諸君,我喜歡貼文"
_login600:
title: "老兵ⅠⅠ"
description: "總登入天數為600天"
_login700:
title: "老兵ⅠⅠⅠ"
description: "總登入天數為700天"
_login800:
title: "貼文大師Ⅰ"
description: "總登入天數為800天"
_login900:
title: "貼文大師ⅠⅠ"
description: "總登入天數為900天"
_login1000:
title: "貼文大師ⅠⅠⅠ"
description: "總登入天數為1,000天"
flavor: "感謝您使用Misskey"
_noteClipped1:
title: "忍不住要收進摘錄裡"
description: "第一次將貼文收進摘錄"
_noteFavorited1:
title: "觀星者"
description: "第一次將貼文收藏至我的最愛"
_myNoteFavorited1:
title: "想要星星"
description: "自己的貼文被他人收藏至「我的最愛」了"
_profileFilled:
title: "有備而來"
description: "設定了個人檔案"
_markedAsCat:
title: "我是貓"
description: "已將帳戶設定為貓"
flavor: "還沒有名字。"
_following1:
title: "首次追隨"
description: "首次追隨了"
_following10:
title: "跟著跟著"
description: "跟隨超過10人了"
_following50:
title: "朋友很多"
description: "跟隨超過50人了"
_following100:
title: "100位朋友"
description: "跟隨超過100人了"
_following300:
title: "朋友過多"
description: "跟隨超過300人了"
_followers1:
title: "第一個追隨者"
description: "第一次被追隨"
_followers10:
title: "Follow me!"
description: "跟隨者超過10人了"
_followers50:
title: "成群結隊"
description: "跟隨者超過50人了"
_followers100:
title: "紅人"
description: "跟隨者超過100人了"
_followers300:
title: "請排成一排"
description: "跟隨者超過300人了"
_followers500:
title: "基站"
description: "超過500名追隨者了"
_followers1000:
title: "影響者"
description: "超過1000名追隨者了"
_collectAchievements30:
title: "成就收藏家"
description: "獲得30個以上的成就"
_viewAchievements3min:
title: "喜愛成就"
description: "看成就列表要花3分鐘以上"
_iLoveMisskey:
title: "I Love Misskey"
description: "發布「I ❤ #Misskey」"
flavor: "感謝您使用Misskey by 開發團隊"
_client30min:
title: "休息一下"
description: "用戶端啟動已超過30分鐘"
_noteDeletedWithin1min:
title: "現在沒有"
description: "發文後1分鐘內刪文"
_postedAtLateNight:
title: "夜行性"
description: "在深夜發佈貼文"
flavor: "該去睡覺了。"
_postedAt0min0sec:
title: "報時"
description: "在0分0秒發佈貼文"
flavor: "啵.啵.啵.嗶ー"
_selfQuote:
title: "自我引用"
description: "引用了自己的貼文"
_htl20npm:
title: "流動的TL"
description: "在首頁時間軸的流速超過20npm"
_outputHelloWorldOnScratchpad:
title: "Hello world!"
description: "在暫存記憶體輸出了 hello world"
_open3windows:
title: "多重視窗"
description: "開啟了3個以上的視窗"
_driveFolderCircularReference:
title: "循環引用"
description: "試圖遞迴套入雲端硬碟資料夾"
_reactWithoutRead:
title: "有好好讀過嗎?"
description: "對包含100字以上內容的貼文做出情感反應"
_clickedClickHere:
title: "點擊這裡"
description: "已點擊這裡了"
_justPlainLucky:
title: "只是運氣好"
description: "每10秒有0.01%的機率獲得"
_setNameToSyuilo:
title: "神的情結"
description: "將名稱設定為 syuilo"
_passedSinceAccountCreated1:
title: "一周年"
description: "自建立帳戶開始過了1年"
_passedSinceAccountCreated2:
title: "二周年"
description: "自建立帳戶開始過了2年"
_passedSinceAccountCreated3:
title: "三周年"
description: "自建立帳戶開始過了3年"
_loggedInOnBirthday:
title: "生日快樂"
description: "在生日當天登入了"
_loggedInOnNewYearsDay:
title: "新年快樂"
description: "在元旦當天登入了"
flavor: "今年也請對敝實例多多指教"
_cookieClicked:
title: "點擊餅乾的遊戲"
description: "點擊了餅乾"
flavor: "是不是軟體有問題?"
_brainDiver:
title: "Brain Driver"
description: "發佈了Brain Driver的連結"
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "建立角色" new: "建立角色"
edit: "編輯角色" edit: "編輯角色"
@ -970,8 +1205,15 @@ _role:
driveCapacity: "雲端硬碟容量" driveCapacity: "雲端硬碟容量"
pinMax: "置頂貼文的最大數量" pinMax: "置頂貼文的最大數量"
antennaMax: "可建立的天線數量" antennaMax: "可建立的天線數量"
wordMuteMax: "靜音文字的最大字數"
webhookMax: "可建立的Webhook數量" webhookMax: "可建立的Webhook數量"
clipMax: "可建立的摘錄數量" clipMax: "可建立的摘錄數量"
noteEachClipsMax: "摘錄內貼文的最大數量"
userListMax: "可建立的使用者清單數量"
userEachUserListsMax: "使用者清單內使用者的最大數量"
rateLimitFactor: "速率限制"
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
canHideAds: "不顯示廣告"
_condition: _condition:
isLocal: "本地使用者" isLocal: "本地使用者"
isRemote: "遠端使用者" isRemote: "遠端使用者"
@ -1576,6 +1818,7 @@ _notification:
pollEnded: "問卷調查已產生結果" pollEnded: "問卷調查已產生結果"
unreadAntennaNote: "天線 {name}" unreadAntennaNote: "天線 {name}"
emptyPushNotificationMessage: "推送通知已更新" emptyPushNotificationMessage: "推送通知已更新"
achievementEarned: "獲得成就"
_types: _types:
all: "全部 " all: "全部 "
follow: "追隨中" follow: "追隨中"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.0.0", "version": "13.1.7",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@ -38,7 +38,7 @@
"cleanall": "pnpm clean-all" "cleanall": "pnpm clean-all"
}, },
"resolutions": { "resolutions": {
"chokidar": "^3.3.1", "chokidar": "^3.5.3",
"lodash": "^4.17.21" "lodash": "^4.17.21"
}, },
"dependencies": { "dependencies": {
@ -54,11 +54,11 @@
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.48.1", "@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.1", "@typescript-eslint/parser": "5.48.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.3.0", "cypress": "12.3.0",
"eslint": "^8.31.0", "eslint": "^8.32.0",
"start-server-and-test": "1.15.2" "start-server-and-test": "1.15.2"
}, },
"optionalDependencies": { "optionalDependencies": {

View file

@ -9,7 +9,17 @@
"transform": { "transform": {
"legacyDecorator": true, "legacyDecorator": true,
"decoratorMetadata": true "decoratorMetadata": true
} },
"experimental": {
"keepImportAssertions": true
},
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
"target": "es2021"
}, },
"minify": false "minify": false
} }

View file

@ -0,0 +1,11 @@
export class flashScriptLength1674086433654 {
name = 'flashScriptLength1674086433654'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "script" TYPE character varying(32768)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "script" TYPE character varying(16384)`);
}
}

View file

@ -0,0 +1,33 @@
export class achievement1674118260469 {
name = 'achievement1674118260469'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "notification" ADD "achievement" character varying(128)`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "achievements" jsonb NOT NULL DEFAULT '[]'`);
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "achievements"`);
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "achievement"`);
}
}

View file

@ -0,0 +1,11 @@
export class loggedInDates1674255666603 {
name = 'loggedInDates1674255666603'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "loggedInDates" character varying(32) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "loggedInDates"`);
}
}

View file

@ -7,6 +7,8 @@
"start": "node ./built/index.js", "start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js", "start:test": "NODE_ENV=test node ./built/index.js",
"migrate": "pnpm typeorm migration:run -d ormconfig.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js",
"build:swc": "swc src -d built -D",
"watch:swc": "swc src -d built -D -w",
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs", "watch": "node watch.mjs",
"lint": "tsc --noEmit && eslint --quiet \"src/**/*.ts\"", "lint": "tsc --noEmit && eslint --quiet \"src/**/*.ts\"",
@ -56,9 +58,9 @@
"date-fns": "2.29.3", "date-fns": "2.29.3",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"fastify": "4.11.0", "fastify": "4.12.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.1.0", "file-type": "18.2.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"got": "12.5.3", "got": "12.5.3",
@ -87,7 +89,7 @@
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.2",
"punycode": "2.2.0", "punycode": "2.3.0",
"pureimage": "0.3.15", "pureimage": "0.3.15",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"random-seed": "0.3.0", "random-seed": "0.3.0",
@ -118,7 +120,7 @@
"typeorm": "0.3.11", "typeorm": "0.3.11",
"typescript": "4.9.4", "typescript": "4.9.4",
"ulid": "2.3.0", "ulid": "2.3.0",
"undici": "^5.15.0", "undici": "^5.15.1",
"unzipper": "0.10.11", "unzipper": "0.10.11",
"uuid": "9.0.0", "uuid": "9.0.0",
"vary": "1.1.2", "vary": "1.1.2",
@ -129,7 +131,8 @@
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.120", "@redocly/openapi-core": "1.0.0-beta.120",
"@swc/core": "1.3.26", "@swc/cli": "^0.1.59",
"@swc/core": "1.3.27",
"@swc/jest": "0.2.24", "@swc/jest": "0.2.24",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.1", "@types/archiver": "5.3.1",
@ -141,7 +144,7 @@
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.20", "@types/fluent-ffmpeg": "2.1.20",
"@types/ioredis": "4.28.10", "@types/ioredis": "4.28.10",
"@types/jest": "29.2.5", "@types/jest": "29.2.6",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "20.0.1", "@types/jsdom": "20.0.1",
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.8",
@ -173,11 +176,11 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.48.1", "@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.1", "@typescript-eslint/parser": "5.48.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.31.0", "eslint": "8.32.0",
"eslint-plugin-import": "2.27.4", "eslint-plugin-import": "2.27.5",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.3.1", "jest": "29.3.1",
"jest-mock": "^29.3.1", "jest-mock": "^29.3.1",

View file

@ -1,13 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ServerModule } from '@/server/ServerModule.js'; import { ServerModule } from '@/server/ServerModule.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; import { DaemonModule } from '@/daemons/DaemonModule.js';
@Module({ @Module({
imports: [ imports: [
GlobalModule, GlobalModule,
ServerModule, ServerModule,
QueueProcessorModule, DaemonModule,
], ],
}) })
export class RootModule {} export class MainModule {}

View file

@ -17,6 +17,9 @@ import { JanitorService } from '@/daemons/JanitorService.js';
import { QueueStatsService } from '@/daemons/QueueStatsService.js'; import { QueueStatsService } from '@/daemons/QueueStatsService.js';
import { ServerStatsService } from '@/daemons/ServerStatsService.js'; import { ServerStatsService } from '@/daemons/ServerStatsService.js';
import { NestLogger } from '@/NestLogger.js'; import { NestLogger } from '@/NestLogger.js';
import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
import { ServerService } from '@/server/ServerService.js';
import { MainModule } from '@/MainModule.js';
import { envOption } from '../env.js'; import { envOption } from '../env.js';
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
@ -70,6 +73,15 @@ export async function masterMain() {
process.exit(1); process.exit(1);
} }
const app = await NestFactory.createApplicationContext(MainModule, {
logger: new NestLogger(),
});
app.enableShutdownHooks();
// start server
const serverService = app.get(ServerService);
serverService.launch();
bootLogger.succ('Misskey initialized'); bootLogger.succ('Misskey initialized');
if (!envOption.disableClustering) { if (!envOption.disableClustering) {
@ -78,15 +90,10 @@ export async function masterMain() {
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
if (!envOption.noDaemons) { app.get(ChartManagementService).start();
const daemons = await NestFactory.createApplicationContext(DaemonModule, { app.get(JanitorService).start();
logger: new NestLogger(), app.get(QueueStatsService).start();
}); app.get(ServerStatsService).start();
daemons.enableShutdownHooks();
daemons.get(JanitorService).start();
daemons.get(QueueStatsService).start();
daemons.get(ServerStatsService).start();
}
} }
function showEnvironment(): void { function showEnvironment(): void {

View file

@ -1,32 +1,23 @@
import cluster from 'node:cluster'; import cluster from 'node:cluster';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { envOption } from '@/env.js';
import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
import { ServerService } from '@/server/ServerService.js';
import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; import { QueueProcessorService } from '@/queue/QueueProcessorService.js';
import { NestLogger } from '@/NestLogger.js'; import { NestLogger } from '@/NestLogger.js';
import { RootModule } from '../RootModule.js'; import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js';
/** /**
* Init worker process * Init worker process
*/ */
export async function workerMain() { export async function workerMain() {
const app = await NestFactory.createApplicationContext(RootModule, { const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, {
logger: new NestLogger(), logger: new NestLogger(),
}); });
app.enableShutdownHooks(); jobQueue.enableShutdownHooks();
// start server
const serverService = app.get(ServerService);
serverService.launch();
// start job queue // start job queue
if (!envOption.onlyServer) { jobQueue.get(QueueProcessorService).start();
const queueProcessorService = app.get(QueueProcessorService);
queueProcessorService.start();
}
app.get(ChartManagementService).run(); jobQueue.get(ChartManagementService).start();
if (cluster.isWorker) { if (cluster.isWorker) {
// Send a 'ready' message to parent process // Send a 'ready' message to parent process

View file

@ -0,0 +1,121 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
const ACHIEVEMENT_TYPES = [
'notes1',
'notes10',
'notes100',
'notes500',
'notes1000',
'notes5000',
'notes10000',
'notes20000',
'notes30000',
'notes40000',
'notes50000',
'notes60000',
'notes70000',
'notes80000',
'notes90000',
'notes100000',
'login3',
'login7',
'login15',
'login30',
'login60',
'login100',
'login200',
'login300',
'login400',
'login500',
'login600',
'login700',
'login800',
'login900',
'login1000',
'passedSinceAccountCreated1',
'passedSinceAccountCreated2',
'passedSinceAccountCreated3',
'loggedInOnBirthday',
'loggedInOnNewYearsDay',
'noteClipped1',
'noteFavorited1',
'myNoteFavorited1',
'profileFilled',
'markedAsCat',
'following1',
'following10',
'following50',
'following100',
'following300',
'followers1',
'followers10',
'followers50',
'followers100',
'followers300',
'followers500',
'followers1000',
'collectAchievements30',
'viewAchievements3min',
'iLoveMisskey',
'foundTreasure',
'client30min',
'noteDeletedWithin1min',
'postedAtLateNight',
'postedAt0min0sec',
'selfQuote',
'htl20npm',
'viewInstanceChart',
'outputHelloWorldOnScratchpad',
'open3windows',
'driveFolderCircularReference',
'reactWithoutRead',
'clickedClickHere',
'justPlainLucky',
'setNameToSyuilo',
'cookieClicked',
'brainDiver',
] as const;
@Injectable()
export class AchievementService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private createNotificationService: CreateNotificationService,
) {
}
@bindThis
public async create(
userId: User['id'],
type: typeof ACHIEVEMENT_TYPES[number],
): Promise<void> {
if (!ACHIEVEMENT_TYPES.includes(type)) return;
const date = Date.now();
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: userId });
if (profile.achievements.some(a => a.name === type)) return;
await this.userProfilesRepository.update(userId, {
achievements: [...profile.achievements, {
name: type,
unlockedAt: date,
}],
});
this.createNotificationService.createNotification(userId, 'achievementEarned', {
achievement: type,
});
}
}

View file

@ -4,6 +4,7 @@ import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js'; import { AiService } from './AiService.js';
import { AntennaService } from './AntennaService.js'; import { AntennaService } from './AntennaService.js';
import { AppLockService } from './AppLockService.js'; import { AppLockService } from './AppLockService.js';
import { AchievementService } from './AchievementService.js';
import { CaptchaService } from './CaptchaService.js'; import { CaptchaService } from './CaptchaService.js';
import { CreateNotificationService } from './CreateNotificationService.js'; import { CreateNotificationService } from './CreateNotificationService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js';
@ -128,6 +129,7 @@ const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useEx
const $AiService: Provider = { provide: 'AiService', useExisting: AiService }; const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService }; const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService }; const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService };
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
@ -255,6 +257,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AiService, AiService,
AntennaService, AntennaService,
AppLockService, AppLockService,
AchievementService,
CaptchaService, CaptchaService,
CreateNotificationService, CreateNotificationService,
CreateSystemUserService, CreateSystemUserService,
@ -376,6 +379,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AiService, $AiService,
$AntennaService, $AntennaService,
$AppLockService, $AppLockService,
$AchievementService,
$CaptchaService, $CaptchaService,
$CreateNotificationService, $CreateNotificationService,
$CreateSystemUserService, $CreateSystemUserService,
@ -498,6 +502,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AiService, AiService,
AntennaService, AntennaService,
AppLockService, AppLockService,
AchievementService,
CaptchaService, CaptchaService,
CreateNotificationService, CreateNotificationService,
CreateSystemUserService, CreateSystemUserService,
@ -618,6 +623,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AiService, $AiService,
$AntennaService, $AntennaService,
$AppLockService, $AppLockService,
$AchievementService,
$CaptchaService, $CaptchaService,
$CreateNotificationService, $CreateNotificationService,
$CreateSystemUserService, $CreateSystemUserService,

View file

@ -125,7 +125,7 @@ export class UndiciFetcher {
...(options.headers ?? {}), ...(options.headers ?? {}),
}, },
}).catch((err) => { }).catch((err) => {
this.logger?.error('fetch error', err); this.logger?.error(`fetch error to ${typeof url === 'string' ? url : url.href}`, err);
throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable'); throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable');
}); });
if (!res.ok && !privateOptions.noOkError) { if (!res.ok && !privateOptions.noOkError) {

View file

@ -57,7 +57,7 @@ export class ApRequestService {
method: 'POST', method: 'POST',
headers: this.objectAssignWithLcKey({ headers: this.objectAssignWithLcKey({
'Date': new Date().toUTCString(), 'Date': new Date().toUTCString(),
'Host': u.hostname, 'Host': u.host,
'Content-Type': 'application/activity+json', 'Content-Type': 'application/activity+json',
'Digest': digestHeader, 'Digest': digestHeader,
}, args.additionalHeaders), }, args.additionalHeaders),
@ -83,7 +83,7 @@ export class ApRequestService {
headers: this.objectAssignWithLcKey({ headers: this.objectAssignWithLcKey({
'Accept': 'application/activity+json, application/ld+json', 'Accept': 'application/activity+json, application/ld+json',
'Date': new Date().toUTCString(), 'Date': new Date().toUTCString(),
'Host': new URL(args.url).hostname, 'Host': new URL(args.url).host,
}, args.additionalHeaders), }, args.additionalHeaders),
}; };
@ -106,6 +106,8 @@ export class ApRequestService {
request.headers = this.objectAssignWithLcKey(request.headers, { request.headers = this.objectAssignWithLcKey(request.headers, {
Signature: signatureHeader, Signature: signatureHeader,
}); });
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
delete request.headers['host'];
return { return {
request, request,

View file

@ -54,7 +54,7 @@ export class ChartManagementService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public async run() { public async start() {
// 20分おきにメモリ情報をDBに書き込み // 20分おきにメモリ情報をDBに書き込み
this.saveIntervalId = setInterval(() => { this.saveIntervalId = setInterval(() => {
for (const chart of this.charts) { for (const chart of this.charts) {

View file

@ -22,7 +22,7 @@ export class EmojiEntityService {
@bindThis @bindThis
public async pack( public async pack(
src: Emoji['id'] | Emoji, src: Emoji['id'] | Emoji,
opts: { omitHost?: boolean; omitId?: boolean; } = {}, opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {},
): Promise<Packed<'Emoji'>> { ): Promise<Packed<'Emoji'>> {
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
@ -32,13 +32,15 @@ export class EmojiEntityService {
name: emoji.name, name: emoji.name,
category: emoji.category, category: emoji.category,
host: opts.omitHost ? undefined : emoji.host, host: opts.omitHost ? undefined : emoji.host,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: opts.withUrl ? (emoji.publicUrl || emoji.originalUrl) : undefined,
}; };
} }
@bindThis @bindThis
public packMany( public packMany(
emojis: any[], emojis: any[],
opts: { omitHost?: boolean; omitId?: boolean; } = {}, opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {},
) { ) {
return Promise.all(emojis.map(x => this.pack(x, opts))); return Promise.all(emojis.map(x => this.pack(x, opts)));
} }

View file

@ -114,6 +114,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'groupInvited' ? { ...(notification.type === 'groupInvited' ? {
invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!), invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!),
} : {}), } : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),
...(notification.type === 'app' ? { ...(notification.type === 'app' ? {
body: notification.customBody, body: notification.customBody,
header: notification.customHeader ?? token?.name, header: notification.customHeader ?? token?.name,

View file

@ -12,7 +12,7 @@ import { Cache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository } from '@/models/index.js'; import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -343,6 +343,7 @@ export class UserEntityService implements OnModuleInit {
options?: { options?: {
detail?: D, detail?: D,
includeSecrets?: boolean, includeSecrets?: boolean,
userProfile?: UserProfile,
}, },
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> { ): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
const opts = Object.assign({ const opts = Object.assign({
@ -375,7 +376,7 @@ export class UserEntityService implements OnModuleInit {
.innerJoinAndSelect('pin.note', 'note') .innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC') .orderBy('pin.id', 'DESC')
.getMany() : []; .getMany() : [];
const profile = opts.detail ? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null; const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
const followingCount = profile == null ? null : const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followingCount : (profile.ffVisibility === 'public') || isMe ? user.followingCount :
@ -493,6 +494,8 @@ export class UserEntityService implements OnModuleInit {
mutingNotificationTypes: profile!.mutingNotificationTypes, mutingNotificationTypes: profile!.mutingNotificationTypes,
emailNotificationTypes: profile!.emailNotificationTypes, emailNotificationTypes: profile!.emailNotificationTypes,
showTimelineReplies: user.showTimelineReplies ?? falsy, showTimelineReplies: user.showTimelineReplies ?? falsy,
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
} : {}), } : {}),
...(opts.includeSecrets ? { ...(opts.includeSecrets ? {

View file

@ -44,7 +44,7 @@ export class Flash {
public user: User | null; public user: User | null;
@Column('varchar', { @Column('varchar', {
length: 16384, length: 32768,
}) })
public script: string; public script: string;

View file

@ -64,6 +64,7 @@ export class Notification {
* receiveFollowRequest - * receiveFollowRequest -
* followRequestAccepted - * followRequestAccepted -
* groupInvited - * groupInvited -
* achievementEarned -
* app - * app -
*/ */
@Index() @Index()
@ -129,6 +130,11 @@ export class Notification {
}) })
public choice: number | null; public choice: number | null;
@Column('varchar', {
length: 128, nullable: true,
})
public achievement: string | null;
/** /**
* body * body
*/ */

View file

@ -213,6 +213,19 @@ export class UserProfile {
}) })
public mutingNotificationTypes: typeof notificationTypes[number][]; public mutingNotificationTypes: typeof notificationTypes[number][];
@Column('varchar', {
length: 32, array: true, default: '{}',
})
public loggedInDates: string[];
@Column('jsonb', {
default: [],
})
public achievements: {
name: string;
unlockedAt: number;
}[];
//#region Denormalized fields //#region Denormalized fields
@Index() @Index()
@Column('varchar', { @Column('varchar', {

View file

@ -29,5 +29,9 @@ export const packedEmojiSchema = {
optional: true, nullable: true, optional: true, nullable: true,
description: 'The local host is represented with `null`.', description: 'The local host is represented with `null`.',
}, },
url: {
type: 'string',
optional: true, nullable: false,
},
}, },
} as const; } as const;

View file

@ -1,5 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js';
import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js'; import { QueueProcessorService } from './QueueProcessorService.js';
import { DbQueueProcessorsService } from './DbQueueProcessorsService.js'; import { DbQueueProcessorsService } from './DbQueueProcessorsService.js';
@ -34,6 +35,7 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro
@Module({ @Module({
imports: [ imports: [
GlobalModule,
CoreModule, CoreModule,
], ],
providers: [ providers: [

View file

@ -14,6 +14,7 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { ActivityPubServerService } from './ActivityPubServerService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js'; import { ApiServerService } from './api/ApiServerService.js';
@ -22,7 +23,6 @@ import { WellKnownServerService } from './WellKnownServerService.js';
import { MediaProxyServerService } from './MediaProxyServerService.js'; import { MediaProxyServerService } from './MediaProxyServerService.js';
import { FileServerService } from './FileServerService.js'; import { FileServerService } from './FileServerService.js';
import { ClientServerService } from './web/ClientServerService.js'; import { ClientServerService } from './web/ClientServerService.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ServerService { export class ServerService {
@ -82,13 +82,13 @@ export class ServerService {
fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
const path = request.params.path; const path = request.params.path;
reply.header('Cache-Control', 'public, max-age=86400');
if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) { if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) {
reply.code(404); reply.code(404);
return; return;
} }
reply.header('Cache-Control', 'public, max-age=86400');
const name = path.split('@')[0].replace('.webp', ''); const name = path.split('@')[0].replace('.webp', '');
const host = path.split('@')[1]?.replace('.webp', ''); const host = path.split('@')[1]?.replace('.webp', '');
@ -101,7 +101,12 @@ export class ServerService {
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
if (emoji == null) { if (emoji == null) {
if ('fallback' in request.query) {
return await reply.redirect('/static-assets/emoji-unknown.png'); return await reply.redirect('/static-assets/emoji-unknown.png');
} else {
reply.code(404);
return;
}
} }
const url = new URL('/proxy/emoji.webp', this.config.url); const url = new URL('/proxy/emoji.webp', this.config.url);
@ -127,6 +132,8 @@ export class ServerService {
relations: ['avatar'], relations: ['avatar'],
}); });
reply.header('Cache-Control', 'public, max-age=86400');
if (user) { if (user) {
reply.redirect(this.userEntityService.getAvatarUrlSync(user)); reply.redirect(this.userEntityService.getAvatarUrlSync(user));
} else { } else {
@ -138,6 +145,7 @@ export class ServerService {
const [temp, cleanup] = await createTemp(); const [temp, cleanup] = await createTemp();
await genIdenticon(request.params.x, fs.createWriteStream(temp)); await genIdenticon(request.params.x, fs.createWriteStream(temp));
reply.header('Content-Type', 'image/png'); reply.header('Content-Type', 'image/png');
reply.header('Cache-Control', 'public, max-age=86400');
return fs.createReadStream(temp).on('close', () => cleanup()); return fs.createReadStream(temp).on('close', () => cleanup());
}); });

View file

@ -175,6 +175,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js'; import * as ep___i_apps from './endpoints/i/apps.js';
import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
import * as ep___i_changePassword from './endpoints/i/change-password.js'; import * as ep___i_changePassword from './endpoints/i/change-password.js';
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
@ -329,6 +330,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___retention from './endpoints/retention.js'; import * as ep___retention from './endpoints/retention.js';
import { GetterService } from './GetterService.js'; import { GetterService } from './GetterService.js';
@ -509,6 +511,7 @@ const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: e
const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default }; const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default };
const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default }; const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default };
const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default }; const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default };
const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default };
const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default }; const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default };
const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default }; const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default };
const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default }; const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default };
@ -663,6 +666,7 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-
const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default }; const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
@ -847,6 +851,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_unregister, $i_2fa_unregister,
$i_apps, $i_apps,
$i_authorizedApps, $i_authorizedApps,
$i_claimAchievement,
$i_changePassword, $i_changePassword,
$i_deleteAccount, $i_deleteAccount,
$i_exportBlocking, $i_exportBlocking,
@ -1001,6 +1006,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search, $users_search,
$users_show, $users_show,
$users_stats, $users_stats,
$users_achievements,
$fetchRss, $fetchRss,
$retention, $retention,
], ],
@ -1179,6 +1185,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_unregister, $i_2fa_unregister,
$i_apps, $i_apps,
$i_authorizedApps, $i_authorizedApps,
$i_claimAchievement,
$i_changePassword, $i_changePassword,
$i_deleteAccount, $i_deleteAccount,
$i_exportBlocking, $i_exportBlocking,
@ -1331,6 +1338,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search, $users_search,
$users_show, $users_show,
$users_stats, $users_stats,
$users_achievements,
$fetchRss, $fetchRss,
$retention, $retention,
], ],

View file

@ -174,6 +174,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js'; import * as ep___i_apps from './endpoints/i/apps.js';
import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
import * as ep___i_changePassword from './endpoints/i/change-password.js'; import * as ep___i_changePassword from './endpoints/i/change-password.js';
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
@ -328,6 +329,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___retention from './endpoints/retention.js'; import * as ep___retention from './endpoints/retention.js';
@ -506,6 +508,7 @@ const eps = [
['i/2fa/unregister', ep___i_2fa_unregister], ['i/2fa/unregister', ep___i_2fa_unregister],
['i/apps', ep___i_apps], ['i/apps', ep___i_apps],
['i/authorized-apps', ep___i_authorizedApps], ['i/authorized-apps', ep___i_authorizedApps],
['i/claim-achievement', ep___i_claimAchievement],
['i/change-password', ep___i_changePassword], ['i/change-password', ep___i_changePassword],
['i/delete-account', ep___i_deleteAccount], ['i/delete-account', ep___i_deleteAccount],
['i/export-blocking', ep___i_exportBlocking], ['i/export-blocking', ep___i_exportBlocking],
@ -660,6 +663,7 @@ const eps = [
['users/search', ep___users_search], ['users/search', ep___users_search],
['users/show', ep___users_show], ['users/show', ep___users_show],
['users/stats', ep___users_stats], ['users/stats', ep___users_stats],
['users/achievements', ep___users_achievements],
['fetch-rss', ep___fetchRss], ['fetch-rss', ep___fetchRss],
['retention', ep___retention], ['retention', ep___retention],
]; ];

View file

@ -28,8 +28,8 @@ export const meta = {
recursiveNesting: { recursiveNesting: {
message: 'It can not be structured like nesting folders recursively.', message: 'It can not be structured like nesting folders recursively.',
code: 'NO_SUCH_PARENT_FOLDER', code: 'RECURSIVE_NESTING',
id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1', id: 'dbeb024837894013aed44279f9199740',
}, },
}, },

View file

@ -85,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
emojis: await this.emojiEntityService.packMany(emojis, { emojis: await this.emojiEntityService.packMany(emojis, {
omitId: true, omitId: true,
omitHost: true, omitHost: true,
withUrl: true,
}), }),
}; };
}); });

View file

@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository } from '@/models/index.js'; import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -29,15 +29,36 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
) { ) {
super(meta, paramDef, async (ps, user, token) => { super(meta, paramDef, async (ps, user, token) => {
const isSecure = token == null; const isSecure = token == null;
// ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す const now = new Date();
return await this.userEntityService.pack<true, true>(user.id, user, { const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
// 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得
const userProfile = await this.userProfilesRepository.findOneOrFail({
where: {
userId: user.id,
},
relations: ['user'],
});
if (!userProfile.loggedInDates.includes(today)) {
this.userProfilesRepository.update({ userId: user.id }, {
loggedInDates: [...userProfile.loggedInDates, today],
});
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
}
return await this.userEntityService.pack<true, true>(userProfile.user!, userProfile.user!, {
detail: true, detail: true,
includeSecrets: isSecure, includeSecrets: isSecure,
userProfile,
}); });
}); });
} }

View file

@ -0,0 +1,28 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { AchievementService } from '@/core/AchievementService.js';
export const meta = {
requireCredential: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
name: { type: 'string' },
},
required: ['name'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
private achievementService: AchievementService,
) {
super(meta, paramDef, async (ps, me) => {
await this.achievementService.create(me.id, ps.name);
});
}
}

View file

@ -6,6 +6,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
import { AchievementService } from '@/core/AchievementService.js';
export const meta = { export const meta = {
tags: ['notes', 'favorites'], tags: ['notes', 'favorites'],
@ -51,6 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private idService: IdService, private idService: IdService,
private getterService: GetterService, private getterService: GetterService,
private achievementService: AchievementService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
// Get favoritee // Get favoritee
@ -76,6 +78,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
noteId: note.id, noteId: note.id,
userId: me.id, userId: me.id,
}); });
if (note.userHost == null) {
this.achievementService.create(note.userId, 'myNoteFavorited1');
}
}); });
} }
} }

View file

@ -0,0 +1,31 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
return profile.achievements;
});
}
}

View file

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import autwh from 'autwh'; import * as autwh from 'autwh';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';

View file

@ -22,13 +22,13 @@
renderError('SOMETHING_HAPPENED_IN_PROMISE', e); renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
}; };
const v = localStorage.getItem('v') || VERSION; let forceError = localStorage.getItem('forceError');
if (forceError != null) {
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
}
//#region Detect language & fetch translations //#region Detect language & fetch translations
const localeVersion = localStorage.getItem('localeVersion'); if (!localStorage.hasOwnProperty('locale')) {
const localeOutdated = (localeVersion == null || localeVersion !== v);
if (!localStorage.hasOwnProperty('locale') || localeOutdated) {
const supportedLangs = LANGS; const supportedLangs = LANGS;
let lang = localStorage.getItem('lang'); let lang = localStorage.getItem('lang');
if (lang == null || !supportedLangs.includes(lang)) { if (lang == null || !supportedLangs.includes(lang)) {
@ -42,13 +42,31 @@
} }
} }
const res = await window.fetch(`/assets/locales/${lang}.${v}.json`); const metaRes = await window.fetch('/api/meta', {
if (res.status === 200) { method: 'POST',
body: JSON.stringify({}),
credentials: 'omit',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
});
if (metaRes.status !== 200) {
renderError('META_FETCH');
return;
}
const meta = await metaRes.json();
const v = meta.version;
if (v == null) {
renderError('META_FETCH_V');
return;
}
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
if (localRes.status === 200) {
localStorage.setItem('lang', lang); localStorage.setItem('lang', lang);
localStorage.setItem('locale', await res.text()); localStorage.setItem('locale', await localRes.text());
localStorage.setItem('localeVersion', v); localStorage.setItem('localeVersion', v);
} else { } else {
await checkUpdate();
renderError('LOCALE_FETCH'); renderError('LOCALE_FETCH');
return; return;
} }
@ -59,7 +77,6 @@
function importAppScript() { function importAppScript() {
import(`/vite/${CLIENT_ENTRY}`) import(`/vite/${CLIENT_ENTRY}`)
.catch(async e => { .catch(async e => {
await checkUpdate();
console.error(e); console.error(e);
renderError('APP_IMPORT', e); renderError('APP_IMPORT', e);
}); });
@ -286,48 +303,4 @@
} }
`) `)
} }
// eslint-disable-next-line no-inner-declarations
async function checkUpdate() {
try {
const res = await window.fetch('/api/meta', {
method: 'POST',
cache: 'no-cache',
body: '{}',
headers: {
'Content-Type': 'application/json',
},
});
const meta = await res.json();
if (meta.version == null) {
throw new Error('failed to fetch instance metadata');
}
if (meta.version != v) {
localStorage.setItem('v', meta.version);
refresh();
}
} catch (e) {
console.error(e);
renderError('UPDATE_CHECK', e);
throw e;
}
}
// eslint-disable-next-line no-inner-declarations
function refresh() {
// Clear cache (service worker)
try {
navigator.serviceWorker.controller.postMessage('clear');
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(registration => registration.unregister());
});
} catch (e) {
console.error(e);
}
location.reload();
}
})(); })();

View file

@ -1,4 +1,4 @@
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View file

@ -8,7 +8,7 @@
}, },
"dependencies": { "dependencies": {
"@discordapp/twemoji": "14.0.2", "@discordapp/twemoji": "14.0.2",
"@rollup/plugin-alias": "4.0.2", "@rollup/plugin-alias": "4.0.3",
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "5.0.2", "@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.12.2", "@syuilo/aiscript": "0.12.2",
@ -18,10 +18,10 @@
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "5.0.2", "autosize": "5.0.2",
"blurhash": "2.0.4", "blurhash": "2.0.4",
"broadcast-channel": "4.20.1", "broadcast-channel": "4.20.2",
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"canvas-confetti": "^1.6.0", "canvas-confetti": "^1.6.0",
"chart.js": "4.1.2", "chart.js": "4.2.0",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "^1.3.0", "chartjs-chart-matrix": "^1.3.0",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
@ -41,10 +41,10 @@
"misskey-js": "0.0.14", "misskey-js": "0.0.14",
"photoswipe": "5.3.4", "photoswipe": "5.3.4",
"prismjs": "1.29.0", "prismjs": "1.29.0",
"punycode": "2.2.0", "punycode": "2.3.0",
"querystring": "0.2.1", "querystring": "0.2.1",
"rndstr": "1.0.0", "rndstr": "1.0.0",
"rollup": "3.10.0", "rollup": "3.10.1",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "^2.8.1", "sanitize-html": "^2.8.1",
"sass": "1.57.1", "sass": "1.57.1",
@ -69,7 +69,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/glob": "8.0.0", "@types/glob": "8.0.1",
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@types/matter-js": "0.18.2", "@types/matter-js": "0.18.2",
@ -82,13 +82,13 @@
"@types/uuid": "9.0.0", "@types/uuid": "9.0.0",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.48.1", "@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.1", "@typescript-eslint/parser": "5.48.2",
"@vue/runtime-core": "3.2.45", "@vue/runtime-core": "3.2.45",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.3.0", "cypress": "12.3.0",
"eslint": "8.31.0", "eslint": "8.32.0",
"eslint-plugin-import": "2.27.4", "eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.9.0", "eslint-plugin-vue": "9.9.0",
"start-server-and-test": "1.15.2", "start-server-and-test": "1.15.2",
"vue-eslint-parser": "^9.1.0", "vue-eslint-parser": "^9.1.0",

View file

@ -2,11 +2,11 @@ import { defineAsyncComponent, reactive } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import { showSuspendedDialog } from './scripts/show-suspended-dialog'; import { showSuspendedDialog } from './scripts/show-suspended-dialog';
import { i18n } from './i18n'; import { i18n } from './i18n';
import { miLocalStorage } from './local-storage';
import { del, get, set } from '@/scripts/idb-proxy'; import { del, get, set } from '@/scripts/idb-proxy';
import { apiUrl } from '@/config'; import { apiUrl } from '@/config';
import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { waiting, api, popup, popupMenu, success, alert } from '@/os';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
import { miLocalStorage } from './local-storage';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -20,6 +20,11 @@ export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : n
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
export const iAmAdmin = $i != null && $i.isAdmin; export const iAmAdmin = $i != null && $i.isAdmin;
export let notesCount = $i == null ? 0 : $i.notesCount;
export function incNotesCount() {
notesCount++;
}
export async function signout() { export async function signout() {
waiting(); waiting();
miLocalStorage.removeItem('account'); miLocalStorage.removeItem('account');

View file

@ -0,0 +1,224 @@
<template>
<div>
<div v-if="achievements" :class="$style.root">
<div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel">
<div :class="$style.icon">
<div :class="[$style.iconFrame, $style['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]">
<div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
<img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div>
</div>
</div>
<div :class="$style.body">
<div :class="$style.header">
<span :class="$style.title">{{ i18n.ts._achievements._types['_' + achievement.name].title }}</span>
<span :class="$style.time">
<time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time>
</span>
</div>
<div :class="$style.description">{{ i18n.ts._achievements._types['_' + achievement.name].description }}</div>
<div v-if="i18n.ts._achievements._types['_' + achievement.name].flavor" :class="$style.flavor">{{ i18n.ts._achievements._types['_' + achievement.name].flavor }}</div>
</div>
</div>
<template v-if="withLocked">
<div v-for="achievement in lockedAchievements" :key="achievement" :class="[$style.achievement, $style.locked]" class="_panel" @click="achievement === 'clickedClickHere' ? clickHere() : () => {}">
<div :class="$style.icon">
</div>
<div :class="$style.body">
<div :class="$style.header">
<span :class="$style.title">???</span>
</div>
<div :class="$style.description">???</div>
</div>
</div>
</template>
</div>
<div v-else>
<MkLoading/>
</div>
</div>
</template>
<script lang="ts" setup>
import * as misskey from 'misskey-js';
import { onMounted } from 'vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements';
const props = withDefaults(defineProps<{
user: misskey.entities.User;
withLocked: boolean;
}>(), {
withLocked: true,
});
let achievements = $ref();
const lockedAchievements = $computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements ?? []).some(a => a.name === x)));
function fetch() {
os.api('users/achievements', { userId: props.user.id }).then(res => {
achievements = [];
for (const t of ACHIEVEMENT_TYPES) {
const a = res.find(x => x.name === t);
if (a) achievements.push(a);
}
//achievements = res.sort((a, b) => b.unlockedAt - a.unlockedAt);
});
}
function clickHere() {
claimAchievement('clickedClickHere');
fetch();
}
onMounted(() => {
fetch();
});
</script>
<style lang="scss" module>
.root {
display: grid;
grid-template-columns: repeat(auto-fill, min(380px, 100%));
grid-gap: 12px;
place-content: center;
}
.achievement {
display: flex;
padding: 16px;
&.locked {
opacity: 0.5;
}
}
.icon {
flex-shrink: 0;
margin-right: 12px;
}
@keyframes shine {
0% { translate: -30px; }
100% { translate: -130px; }
}
.iconFrame {
width: 58px;
height: 58px;
padding: 6px;
border-radius: 100%;
box-sizing: border-box;
pointer-events: none;
user-select: none;
filter: drop-shadow(0px 2px 2px #00000044);
box-shadow: 0 1px 0px #ffffff88 inset;
overflow: clip;
}
.iconFrame_bronze {
background: linear-gradient(0deg, #703827, #d37566);
> .iconInner {
background: linear-gradient(0deg, #d37566, #703827);
}
}
.iconFrame_silver {
background: linear-gradient(0deg, #7c7c7c, #e1e1e1);
> .iconInner {
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
}
}
.iconFrame_gold {
background: linear-gradient(0deg, rgba(255,182,85,1) 0%, rgba(233,133,0,1) 49%, rgba(255,243,93,1) 51%, rgba(255,187,25,1) 100%);
> .iconInner {
background: linear-gradient(0deg, #ffee20, #eb7018);
}
&:before {
content: "";
display: block;
position: absolute;
top: 30px;
width: 200px;
height: 8px;
rotate: -45deg;
translate: -30px;
background: #ffffff88;
animation: shine 2s infinite;
}
}
.iconFrame_platinum {
background: linear-gradient(0deg, rgba(154,154,154,1) 0%, rgba(226,226,226,1) 49%, rgba(255,255,255,1) 51%, rgba(195,195,195,1) 100%);
> .iconInner {
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
}
&:before {
content: "";
display: block;
position: absolute;
top: 30px;
width: 200px;
height: 8px;
rotate: -45deg;
translate: -30px;
background: #ffffffee;
animation: shine 2s infinite;
}
}
.iconInner {
position: relative;
width: 100%;
height: 100%;
border-radius: 100%;
box-shadow: 0 1px 0px #ffffff88 inset;
}
.iconImg {
width: calc(100% - 12px);
height: calc(100% - 12px);
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
filter: drop-shadow(0px 1px 2px #000000aa);
}
.body {
flex: 1;
min-width: 0;
}
.header {
margin-bottom: 8px;
display: flex;
}
.title {
font-weight: bold;
}
.time {
margin-left: auto;
font-size: 85%;
opacity: 0.7;
}
.description {
font-size: 85%;
}
.flavor {
opacity: 0.7;
transform: skewX(-15deg);
font-size: 85%;
margin-top: 8px;
}
</style>

View file

@ -7,9 +7,9 @@
</div> </div>
<span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }">{{ c.text }}</span> <span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }">{{ c.text }}</span>
<Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }" :text="c.text"/> <Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }" :text="c.text"/>
<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="c.onClick">{{ c.text }}</MkButton> <MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :disabled="c.disabled" :small="size === 'small'" inline @click="c.onClick">{{ c.text }}</MkButton>
<div v-else-if="c.type === 'buttons'" class="_buttons"> <div v-else-if="c.type === 'buttons'" class="_buttons" :style="{ justifyContent: align }">
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton> <MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :disabled="button.disabled" inline :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
</div> </div>
<MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate"> <MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate">
<template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.label" #label>{{ c.label }}</template>
@ -41,7 +41,7 @@
</MkFolder> </MkFolder>
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }"> <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
<template v-for="child in c.children" :key="child"> <template v-for="child in c.children" :key="child">
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
</template> </template>
</div> </div>
</div> </div>
@ -62,8 +62,10 @@ const props = withDefaults(defineProps<{
component: AsUiComponent; component: AsUiComponent;
components: Ref<AsUiComponent>[]; components: Ref<AsUiComponent>[];
size: 'small' | 'medium' | 'large'; size: 'small' | 'medium' | 'large';
align: 'left' | 'center' | 'right';
}>(), { }>(), {
size: 'medium', size: 'medium',
align: 'left',
}); });
const c = props.component; const c = props.component;

View file

@ -20,6 +20,7 @@ import * as os from '@/os';
import { useInterval } from '@/scripts/use-interval'; import { useInterval } from '@/scripts/use-interval';
import * as game from '@/scripts/clicker-game'; import * as game from '@/scripts/clicker-game';
import number from '@/filters/number'; import number from '@/filters/number';
import { claimAchievement } from '@/scripts/achievements';
defineProps<{ defineProps<{
}>(); }>();
@ -30,14 +31,18 @@ let cps = $ref(0);
let prevCookies = $ref(0); let prevCookies = $ref(0);
function onClick(ev: MouseEvent) { function onClick(ev: MouseEvent) {
const x = ev.clientX;
const y = ev.clientY;
os.popup(MkPlusOneEffect, { x, y }, {}, 'end');
saveData.value!.cookies++; saveData.value!.cookies++;
saveData.value!.totalCookies++; saveData.value!.totalCookies++;
saveData.value!.totalHandmadeCookies++; saveData.value!.totalHandmadeCookies++;
saveData.value!.clicked++; saveData.value!.clicked++;
const x = ev.clientX; if (cookies.value === 1) {
const y = ev.clientY; claimAchievement('cookieClicked');
os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); }
} }
useInterval(() => { useInterval(() => {

View file

@ -33,6 +33,7 @@ import * as Misskey from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { claimAchievement } from '@/scripts/achievements';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
folder: Misskey.entities.DriveFolder; folder: Misskey.entities.DriveFolder;
@ -159,9 +160,11 @@ function onDrop(ev: DragEvent) {
}).then(() => { }).then(() => {
// noop // noop
}).catch(err => { }).catch(err => {
switch (err) { switch (err.code) {
case 'detected-circular-definition': case 'RECURSIVE_NESTING':
claimAchievement('driveFolderCircularReference');
os.alert({ os.alert({
type: 'error',
title: i18n.ts.unableToProcess, title: i18n.ts.unableToProcess,
text: i18n.ts.circularReferenceFolder, text: i18n.ts.circularReferenceFolder,
}); });

View file

@ -99,6 +99,7 @@ import { stream } from '@/stream';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { uploadFile, uploads } from '@/scripts/upload'; import { uploadFile, uploads } from '@/scripts/upload';
import { claimAchievement } from '@/scripts/achievements';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder; initialFolder?: Misskey.entities.DriveFolder;
@ -268,9 +269,11 @@ function onDrop(ev: DragEvent): any {
}).then(() => { }).then(() => {
// noop // noop
}).catch(err => { }).catch(err => {
switch (err) { switch (err.code) {
case 'detected-circular-definition': case 'RECURSIVE_NESTING':
claimAchievement('driveFolderCircularReference');
os.alert({ os.alert({
type: 'error',
title: i18n.ts.unableToProcess, title: i18n.ts.unableToProcess,
text: i18n.ts.circularReferenceFolder, text: i18n.ts.circularReferenceFolder,
}); });

View file

@ -35,6 +35,8 @@ import * as Misskey from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { claimAchievement } from '@/scripts/achievements';
import { $i } from '@/account';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed, user: Misskey.entities.UserDetailed,
@ -90,6 +92,21 @@ async function onClick() {
userId: props.user.id, userId: props.user.id,
}); });
hasPendingFollowRequestFromYou = true; hasPendingFollowRequestFromYou = true;
claimAchievement('following1');
if ($i.followingCount >= 10) {
claimAchievement('following10');
}
if ($i.followingCount >= 50) {
claimAchievement('following50');
}
if ($i.followingCount >= 100) {
claimAchievement('following100');
}
if ($i.followingCount >= 300) {
claimAchievement('following300');
}
} }
} }
} catch (err) { } catch (err) {

View file

@ -13,7 +13,7 @@
:href="image.url" :href="image.url"
:title="image.name" :title="image.name"
> >
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :title="image.comment" :cover="false"/> <ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :cover="false"/>
<div v-if="image.type === 'image/gif'" class="gif">GIF</div> <div v-if="image.type === 'image/gif'" class="gif">GIF</div>
</a> </a>
<button v-tooltip="$ts.hide" class="_button hide" @click="hide = true"><i class="ti ti-eye-off"></i></button> <button v-tooltip="$ts.hide" class="_button hide" @click="hide = true"><i class="ti ti-eye-off"></i></button>

View file

@ -45,7 +45,8 @@ onMounted(() => {
src: media.url, src: media.url,
w: media.properties.width, w: media.properties.width,
h: media.properties.height, h: media.properties.height,
alt: media.name, alt: media.comment || media.name,
comment: media.comment || media.name,
}; };
if (media.properties.orientation != null && media.properties.orientation >= 5) { if (media.properties.orientation != null && media.properties.orientation >= 5) {
[item.w, item.h] = [item.h, item.w]; [item.w, item.h] = [item.h, item.w];
@ -69,6 +70,7 @@ onMounted(() => {
}, },
imageClickAction: 'close', imageClickAction: 'close',
tapAction: 'toggle-controls', tapAction: 'toggle-controls',
bgOpacity: 1,
pswpModule: PhotoSwipe, pswpModule: PhotoSwipe,
}); });
@ -88,9 +90,28 @@ onMounted(() => {
[itemData.w, itemData.h] = [itemData.h, itemData.w]; [itemData.w, itemData.h] = [itemData.h, itemData.w];
} }
itemData.msrc = file.thumbnailUrl; itemData.msrc = file.thumbnailUrl;
itemData.alt = file.comment || file.name;
itemData.comment = file.comment || file.name;
itemData.thumbCropped = true; itemData.thumbCropped = true;
}); });
lightbox.on('uiRegister', () => {
lightbox.pswp.ui.registerElement({
name: 'altText',
className: 'pwsp__alt-text-container',
appendTo: 'wrapper',
onInit: (el, pwsp) => {
let textBox = document.createElement('p');
textBox.className = 'pwsp__alt-text _acrylic';
el.appendChild(textBox);
pwsp.on('change', (a) => {
textBox.textContent = pwsp.currSlide.data.comment;
});
},
});
});
lightbox.init(); lightbox.init();
}); });
@ -185,5 +206,36 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
// //
//z-index: v-bind(pswpZIndex); //z-index: v-bind(pswpZIndex);
z-index: 2000000; z-index: 2000000;
--pswp-bg: var(--modalBg);
}
.pswp__bg {
background: var(--modalBg);
backdrop-filter: var(--modalBgFilter);
}
.pwsp__alt-text-container {
display: flex;
flex-direction: row;
align-items: center;
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
width: 75%;
max-width: 800px;
}
.pwsp__alt-text {
color: var(--fg);
margin: 0 auto;
text-align: center;
padding: var(--margin);
border-radius: var(--radius);
max-height: 8em;
overflow-y: auto;
text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px;
} }
</style> </style>

View file

@ -143,6 +143,7 @@ import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture'; import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
import { claimAchievement } from '@/scripts/achievements';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@ -268,6 +269,9 @@ function react(viaKeyboard = false): void {
noteId: appearNote.id, noteId: appearNote.id,
reaction: reaction, reaction: reaction,
}); });
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => { }, () => {
focus(); focus();
}); });

View file

@ -159,6 +159,7 @@ import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture'; import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
import { claimAchievement } from '@/scripts/achievements';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@ -279,6 +280,9 @@ function react(viaKeyboard = false): void {
noteId: appearNote.id, noteId: appearNote.id,
reaction: reaction, reaction: reaction,
}); });
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => { }, () => {
focus(); focus();
}); });

View file

@ -2,6 +2,7 @@
<div ref="elRef" :class="$style.root"> <div ref="elRef" :class="$style.root">
<div v-once :class="$style.head"> <div v-once :class="$style.head">
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/> <MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/> <img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
<div :class="[$style.subIcon, $style['t_' + notification.type]]"> <div :class="[$style.subIcon, $style['t_' + notification.type]]">
@ -14,6 +15,7 @@
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i> <i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-military-award"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<MkReactionIcon <MkReactionIcon
v-else-if="notification.type === 'reaction'" v-else-if="notification.type === 'reaction'"
@ -28,6 +30,7 @@
<div :class="$style.tail"> <div :class="$style.tail">
<header :class="$style.header"> <header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span> <span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else>{{ notification.header }}</span> <span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
@ -57,6 +60,9 @@
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i> <i class="ti ti-quote" :class="$style.quote"></i>
</MkA> </MkA>
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA>
<span v-else-if="notification.type === 'follow'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> <span v-else-if="notification.type === 'follow'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> <span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<span v-else-if="notification.type === 'receiveFollowRequest'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span> <span v-else-if="notification.type === 'receiveFollowRequest'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
@ -82,6 +88,7 @@ import { i18n } from '@/i18n';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
import { $i } from '@/account';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
notification: misskey.entities.Notification; notification: misskey.entities.Notification;
@ -240,6 +247,12 @@ useTooltip(reactionRef, (showing) => {
pointer-events: none; pointer-events: none;
} }
.t_achievementEarned {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
.tail { .tail {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@ -267,9 +280,9 @@ useTooltip(reactionRef, (showing) => {
} }
.text { .text {
white-space: nowrap; display: flex;
overflow: hidden; width: 100%;
text-overflow: ellipsis; overflow: clip;
} }
.quote { .quote {

View file

@ -10,7 +10,7 @@
<template #default="{ items: notifications }"> <template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true"> <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true">
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/> <XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="false" class="_panel notification"/>
</MkDateSeparatedList> </MkDateSeparatedList>
</template> </template>
</MkPagination> </MkPagination>

View file

@ -24,7 +24,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ComputedRef, inject, provide } from 'vue'; import { ComputedRef, inject, onMounted, onUnmounted, provide } from 'vue';
import RouterView from '@/components/global/RouterView.vue'; import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout'; import { popout as _popout } from '@/scripts/popout';
@ -35,6 +35,8 @@ import { mainRouter, routes } from '@/router';
import { Router } from '@/nirax'; import { Router } from '@/nirax';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
import { openingWindowsCount } from '@/os';
import { claimAchievement } from '@/scripts/achievements';
const props = defineProps<{ const props = defineProps<{
initialPath: string; initialPath: string;
@ -128,6 +130,17 @@ function popout() {
windowEl.close(); windowEl.close();
} }
onMounted(() => {
openingWindowsCount.value++;
if (openingWindowsCount.value >= 3) {
claimAchievement('open3windows');
}
});
onUnmounted(() => {
openingWindowsCount.value--;
});
defineExpose({ defineExpose({
close, close,
}); });

View file

@ -93,11 +93,12 @@ import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'; import { $i, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
import { uploadFile } from '@/scripts/upload'; import { uploadFile } from '@/scripts/upload';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage'; import { miLocalStorage } from '@/local-storage';
import { claimAchievement } from '@/scripts/achievements';
const modal = inject('modal'); const modal = inject('modal');
@ -627,6 +628,34 @@ async function post(ev?: MouseEvent) {
} }
posting = false; posting = false;
postAccount = null; postAccount = null;
incNotesCount();
if (notesCount === 1) {
claimAchievement('notes1');
}
const text = postData.text?.toLowerCase() ?? '';
if ((text.includes('love') || text.includes('❤')) && text.includes('misskey')) {
claimAchievement('iLoveMisskey');
}
if (text.includes('Efrlqw8ytg4'.toLowerCase()) || text.includes('XVCwzwxdHuA'.toLowerCase())) {
claimAchievement('brainDiver');
}
if (props.renote && (props.renote.userId === $i.id) && text.length > 0) {
claimAchievement('selfQuote');
}
const date = new Date();
const h = date.getHours();
const m = date.getMinutes();
const s = date.getSeconds();
if (h >= 0 && h <= 3) {
claimAchievement('postedAtLateNight');
}
if (m === 0 && s === 0) {
claimAchievement('postedAt0min0sec');
}
}); });
}).catch(err => { }).catch(err => {
posting = false; posting = false;

View file

@ -0,0 +1,92 @@
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
@close="dialog.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.reactions }}</template>
<MkSpacer :margin-min="20" :margin-max="28">
<div v-if="note" class="_gaps">
<div :class="$style.tabs">
<button v-for="reaction in reactions" :key="reaction" :class="[$style.tab, { [$style.tabActive]: tab === reaction }]" class="_button" @click="tab = reaction">
<MkReactionIcon :reaction="reaction"/>
<span style="margin-left: 4px;">{{ note.reactions[reaction] }}</span>
</button>
</div>
<MkA v-for="user in users" :key="user.id" :to="userPage(user)">
<MkUserCardMini :user="user" :with-chart="false"/>
</MkA>
</div>
<div v-else>
<MkLoading/>
</div>
</MkSpacer>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { onMounted, watch } from 'vue';
import * as misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { userPage } from '@/filters/user';
import { i18n } from '@/i18n';
import * as os from '@/os';
const emit = defineEmits<{
(ev: 'closed'): void,
}>();
const props = defineProps<{
noteId: misskey.entities.Note['id'];
}>();
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
let note = $ref<misskey.entities.Note>();
let tab = $ref<string>();
let reactions = $ref<string[]>();
let users = $ref();
watch($$(tab), async () => {
const res = await os.api('notes/reactions', {
noteId: props.noteId,
type: tab,
limit: 30,
});
users = res.map(x => x.user);
});
onMounted(() => {
os.api('notes/show', {
noteId: props.noteId,
}).then((res) => {
reactions = Object.keys(res.reactions);
tab = reactions[0];
note = res;
});
});
</script>
<style lang="scss" module>
.tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tab {
padding: 4px 6px;
border: solid 1px var(--divider);
border-radius: 6px;
}
.tabActive {
border-color: var(--accent);
}
</style>

View file

@ -20,6 +20,7 @@ import * as os from '@/os';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
import { $i } from '@/account'; import { $i } from '@/account';
import MkReactionEffect from '@/components/MkReactionEffect.vue'; import MkReactionEffect from '@/components/MkReactionEffect.vue';
import { claimAchievement } from '@/scripts/achievements';
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: string;
@ -52,6 +53,9 @@ const toggleReaction = () => {
noteId: props.note.id, noteId: props.note.id,
reaction: props.reaction, reaction: props.reaction,
}); });
if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
} }
}; };

View file

@ -11,20 +11,28 @@
<script lang="ts" setup> <script lang="ts" setup>
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import { onMounted } from 'vue';
import MkMiniChart from '@/components/MkMiniChart.vue'; import MkMiniChart from '@/components/MkMiniChart.vue';
import * as os from '@/os'; import * as os from '@/os';
import { acct } from '@/filters/user'; import { acct } from '@/filters/user';
const props = defineProps<{ const props = withDefaults(defineProps<{
user: misskey.entities.User; user: misskey.entities.User;
}>(); withChart: boolean;
}>(), {
withChart: true,
});
let chartValues = $ref<number[] | null>(null); let chartValues = $ref<number[] | null>(null);
os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16 + 1, span: 'day' }).then(res => { onMounted(() => {
if (props.withChart) {
os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16 + 1, span: 'day' }).then(res => {
// //
res.inc.splice(0, 1); res.inc.splice(0, 1);
chartValues = res.inc; chartValues = res.inc;
});
}
}); });
</script> </script>

View file

@ -1,5 +1,6 @@
<template> <template>
<img v-if="isCustom" :class="[$style.root, $style.custom, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async"/> <span v-if="isCustom && errored">:{{ customEmojiName }}:</span>
<img v-else-if="isCustom" :class="[$style.root, $style.custom, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true"/>
<img v-else-if="char && !useOsNativeEmojis" :class="$style.root" :src="url" :alt="alt" decoding="async" @pointerenter="computeTitle"/> <img v-else-if="char && !useOsNativeEmojis" :class="$style.root" :src="url" :alt="alt" decoding="async" @pointerenter="computeTitle"/>
<span v-else-if="char && useOsNativeEmojis" :alt="alt" @pointerenter="computeTitle">{{ char }}</span> <span v-else-if="char && useOsNativeEmojis" :alt="alt" @pointerenter="computeTitle">{{ char }}</span>
<span v-else>{{ emoji }}</span> <span v-else>{{ emoji }}</span>
@ -11,6 +12,7 @@ import { getStaticImageUrl } from '@/scripts/media-proxy';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { getEmojiName } from '@/scripts/emojilist'; import { getEmojiName } from '@/scripts/emojilist';
import { customEmojis } from '@/custom-emojis';
const props = defineProps<{ const props = defineProps<{
emoji: string; emoji: string;
@ -23,12 +25,15 @@ const props = defineProps<{
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
const isCustom = computed(() => props.emoji.startsWith(':')); const isCustom = computed(() => props.emoji.startsWith(':'));
const customEmojiName = props.emoji.substr(1, props.emoji.length - 2); const customEmojiName = props.emoji.substr(1, props.emoji.length - 2).replace('@.', '');
const char = computed(() => isCustom.value ? undefined : props.emoji); const char = computed(() => isCustom.value ? undefined : props.emoji);
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native' && !props.isReaction); const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native' && !props.isReaction);
const url = computed(() => { const url = computed(() => {
if (char.value) { if (char.value) {
return char2path(char.value); return char2path(char.value);
} else if (props.host == null && !customEmojiName.includes('@')) {
const found = customEmojis.find(x => x.name === customEmojiName);
return found ? found.url : null;
} else { } else {
const rawUrl = props.host ? `/emoji/${customEmojiName}@${props.host}.webp` : `/emoji/${customEmojiName}.webp`; const rawUrl = props.host ? `/emoji/${customEmojiName}@${props.host}.webp` : `/emoji/${customEmojiName}.webp`;
return defaultStore.state.disableShowingAnimatedImages return defaultStore.state.disableShowingAnimatedImages
@ -37,6 +42,7 @@ const url = computed(() => {
} }
}); });
const alt = computed(() => isCustom.value ? `:${customEmojiName}:` : char.value); const alt = computed(() => isCustom.value ? `:${customEmojiName}:` : char.value);
let errored = $ref(isCustom.value && url.value == null);
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter // Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
function computeTitle(event: PointerEvent): void { function computeTitle(event: PointerEvent): void {

View file

@ -200,6 +200,12 @@ export default defineComponent({
style = `transform: translateX(${x}em) translateY(${y}em);`; style = `transform: translateX(${x}em) translateY(${y}em);`;
break; break;
} }
case 'scale': {
const x = Math.min(parseInt(token.props.args.x ?? '1'), 5);
const y = Math.min(parseInt(token.props.args.y ?? '1'), 5);
style = `transform: scale(${x}, ${y});`;
break;
}
case 'fg': { case 'fg': {
let color = token.props.args.color; let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00'; if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';

View file

@ -10,8 +10,12 @@ export const apiUrl = url + '/api';
export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
export const lang = miLocalStorage.getItem('lang'); export const lang = miLocalStorage.getItem('lang');
export const langs = _LANGS_; export const langs = _LANGS_;
export const locale = JSON.parse(miLocalStorage.getItem('locale')); export let locale = JSON.parse(miLocalStorage.getItem('locale'));
export const version = _VERSION_; export const version = _VERSION_;
export const instanceName = siteName === 'Misskey' ? host : siteName; export const instanceName = siteName === 'Misskey' ? host : siteName;
export const ui = miLocalStorage.getItem('ui'); export const ui = miLocalStorage.getItem('ui');
export const debug = miLocalStorage.getItem('debug') === 'true'; export const debug = miLocalStorage.getItem('debug') === 'true';
export function updateLocale(newLocale) {
locale = newLocale;
}

View file

@ -3,3 +3,7 @@ import { locale } from '@/config';
import { I18n } from '@/scripts/i18n'; import { I18n } from '@/scripts/i18n';
export const i18n = markRaw(new I18n(locale)); export const i18n = markRaw(new I18n(locale));
export function updateI18n(newLocale) {
i18n.ts = newLocale;
}

View file

@ -25,10 +25,10 @@ import JSON5 from 'json5';
import widgets from '@/widgets'; import widgets from '@/widgets';
import directives from '@/directives'; import directives from '@/directives';
import components from '@/components'; import components from '@/components';
import { version, ui, lang, host } from '@/config'; import { version, ui, lang, host, updateLocale } from '@/config';
import { applyTheme } from '@/scripts/theme'; import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n } from '@/i18n'; import { i18n, updateI18n } from '@/i18n';
import { confirm, alert, post, popup, toast } from '@/os'; import { confirm, alert, post, popup, toast } from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
@ -44,6 +44,8 @@ import { reactionPicker } from '@/scripts/reaction-picker';
import { getUrlWithoutLoginId } from '@/scripts/login-id'; import { getUrlWithoutLoginId } from '@/scripts/login-id';
import { getAccountFromId } from '@/scripts/get-account-from-id'; import { getAccountFromId } from '@/scripts/get-account-from-id';
import { miLocalStorage } from './local-storage'; import { miLocalStorage } from './local-storage';
import { claimAchievement, claimedAchievements } from './scripts/achievements';
import { fetchCustomEmojis } from './custom-emojis';
(async () => { (async () => {
console.info(`Misskey v${version}`); console.info(`Misskey v${version}`);
@ -79,6 +81,22 @@ import { miLocalStorage } from './local-storage';
}); });
} }
//#region Detect language & fetch translations
const localeVersion = miLocalStorage.getItem('localeVersion');
const localeOutdated = (localeVersion == null || localeVersion !== version);
if (localeOutdated) {
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
if (res.status === 200) {
const newLocale = await res.text();
const parsedNewLocale = JSON.parse(newLocale);
miLocalStorage.setItem('locale', newLocale);
miLocalStorage.setItem('localeVersion', version);
updateLocale(parsedNewLocale);
updateI18n(parsedNewLocale);
}
}
//#endregion
// タッチデバイスでCSSの:hoverを機能させる // タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true }); document.addEventListener('touchend', () => {}, { passive: true });
@ -164,6 +182,10 @@ import { miLocalStorage } from './local-storage';
initializeSw(); initializeSw();
}); });
try {
await fetchCustomEmojis();
} catch (err) {}
const app = createApp( const app = createApp(
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
@ -345,6 +367,87 @@ import { miLocalStorage } from './local-storage';
}); });
} }
const now = new Date();
const m = now.getMonth() + 1;
const d = now.getDate();
if ($i.birthday) {
const bm = parseInt($i.birthday.split('-')[1]);
const bd = parseInt($i.birthday.split('-')[2]);
if (m === bm && d === bd) {
claimAchievement('loggedInOnBirthday');
}
}
if (m === 1 && d === 1) {
claimAchievement('loggedInOnNewYearsDay');
}
if ($i.loggedInDays >= 3) claimAchievement('login3');
if ($i.loggedInDays >= 7) claimAchievement('login7');
if ($i.loggedInDays >= 15) claimAchievement('login15');
if ($i.loggedInDays >= 30) claimAchievement('login30');
if ($i.loggedInDays >= 60) claimAchievement('login60');
if ($i.loggedInDays >= 100) claimAchievement('login100');
if ($i.loggedInDays >= 200) claimAchievement('login200');
if ($i.loggedInDays >= 300) claimAchievement('login300');
if ($i.loggedInDays >= 400) claimAchievement('login400');
if ($i.loggedInDays >= 500) claimAchievement('login500');
if ($i.loggedInDays >= 600) claimAchievement('login600');
if ($i.loggedInDays >= 700) claimAchievement('login700');
if ($i.loggedInDays >= 800) claimAchievement('login800');
if ($i.loggedInDays >= 900) claimAchievement('login900');
if ($i.loggedInDays >= 1000) claimAchievement('login1000');
if ($i.notesCount > 0) claimAchievement('notes1');
if ($i.notesCount >= 10) claimAchievement('notes10');
if ($i.notesCount >= 100) claimAchievement('notes100');
if ($i.notesCount >= 500) claimAchievement('notes500');
if ($i.notesCount >= 1000) claimAchievement('notes1000');
if ($i.notesCount >= 5000) claimAchievement('notes5000');
if ($i.notesCount >= 10000) claimAchievement('notes10000');
if ($i.notesCount >= 20000) claimAchievement('notes20000');
if ($i.notesCount >= 30000) claimAchievement('notes30000');
if ($i.notesCount >= 40000) claimAchievement('notes40000');
if ($i.notesCount >= 50000) claimAchievement('notes50000');
if ($i.notesCount >= 60000) claimAchievement('notes60000');
if ($i.notesCount >= 70000) claimAchievement('notes70000');
if ($i.notesCount >= 80000) claimAchievement('notes80000');
if ($i.notesCount >= 90000) claimAchievement('notes90000');
if ($i.notesCount >= 100000) claimAchievement('notes100000');
if ($i.followersCount > 0) claimAchievement('followers1');
if ($i.followersCount >= 10) claimAchievement('followers10');
if ($i.followersCount >= 50) claimAchievement('followers50');
if ($i.followersCount >= 100) claimAchievement('followers100');
if ($i.followersCount >= 300) claimAchievement('followers300');
if ($i.followersCount >= 500) claimAchievement('followers500');
if ($i.followersCount >= 1000) claimAchievement('followers1000');
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
claimAchievement('passedSinceAccountCreated1');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
claimAchievement('passedSinceAccountCreated2');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
claimAchievement('passedSinceAccountCreated3');
}
if (claimedAchievements.length >= 30) {
claimAchievement('collectAchievements30');
}
window.setInterval(() => {
if (Math.floor(Math.random() * 10000) === 0) {
claimAchievement('justPlainLucky');
}
}, 1000 * 10);
window.setTimeout(() => {
claimAchievement('client30min');
}, 1000 * 60 * 30);
const lastUsed = miLocalStorage.getItem('lastUsed'); const lastUsed = miLocalStorage.getItem('lastUsed');
if (lastUsed) { if (lastUsed) {
const lastUsedDate = parseInt(lastUsed, 10); const lastUsedDate = parseInt(lastUsed, 10);

View file

@ -19,6 +19,7 @@ type Keys =
'fontSize' | 'fontSize' |
'ui' | 'ui' |
'locale' | 'locale' |
'localeVersion' |
'theme' | 'theme' |
'customCss' | 'customCss' |
'message_drafts' | 'message_drafts' |

View file

@ -1,11 +1,11 @@
import { computed, ref, reactive } from 'vue'; import { computed, ref, reactive } from 'vue';
import { $i } from './account'; import { $i } from './account';
import { miLocalStorage } from './local-storage';
import { search } from '@/scripts/search'; import { search } from '@/scripts/search';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { ui } from '@/config'; import { ui } from '@/config';
import { unisonReload } from '@/scripts/unison-reload'; import { unisonReload } from '@/scripts/unison-reload';
import { miLocalStorage } from './local-storage';
export const navbarItemDef = reactive({ export const navbarItemDef = reactive({
notifications: { notifications: {
@ -103,6 +103,12 @@ export const navbarItemDef = reactive({
icon: 'ti ti-device-tv', icon: 'ti ti-device-tv',
to: '/channels', to: '/channels',
}, },
achievements: {
title: i18n.ts.achievements,
icon: 'ti ti-military-award',
show: computed(() => $i != null),
to: '/my/achievements',
},
ui: { ui: {
title: i18n.ts.switchUi, title: i18n.ts.switchUi,
icon: 'ti ti-devices', icon: 'ti ti-devices',

View file

@ -1,5 +1,7 @@
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
import { pendingApiRequestsCount, api, apiGet } from '@/scripts/api';
export { pendingApiRequestsCount, api, apiGet };
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue'; import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
@ -7,9 +9,16 @@ import * as Misskey from 'misskey-js';
import { i18n } from './i18n'; import { i18n } from './i18n';
import MkPostFormDialog from '@/components/MkPostFormDialog.vue'; import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
import MkPageWindow from '@/components/MkPageWindow.vue';
import MkToast from '@/components/MkToast.vue';
import MkDialog from '@/components/MkDialog.vue';
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
import MkEmojiPickerWindow from '@/components/MkEmojiPickerWindow.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue';
import { MenuItem } from '@/types/menu'; import { MenuItem } from '@/types/menu';
import { pendingApiRequestsCount, api, apiGet } from '@/scripts/api';
export { pendingApiRequestsCount, api, apiGet }; export const openingWindowsCount = ref(0);
export const apiWithDialog = (( export const apiWithDialog = ((
endpoint: string, endpoint: string,
@ -124,7 +133,7 @@ export async function popup(component: Component, props: Record<string, any>, ev
} }
export function pageWindow(path: string) { export function pageWindow(path: string) {
popup(defineAsyncComponent(() => import('@/components/MkPageWindow.vue')), { popup(MkPageWindow, {
initialPath: path, initialPath: path,
}, {}, 'closed'); }, {}, 'closed');
} }
@ -136,7 +145,7 @@ export function modalPageWindow(path: string) {
} }
export function toast(message: string) { export function toast(message: string) {
popup(defineAsyncComponent(() => import('@/components/MkToast.vue')), { popup(MkToast, {
message, message,
}, {}, 'closed'); }, {}, 'closed');
} }
@ -147,7 +156,7 @@ export function alert(props: {
text?: string | null; text?: string | null;
}): Promise<void> { }): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), props, { popup(MkDialog, props, {
done: result => { done: result => {
resolve(); resolve();
}, },
@ -161,7 +170,7 @@ export function confirm(props: {
text?: string | null; text?: string | null;
}): Promise<{ canceled: boolean }> { }): Promise<{ canceled: boolean }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { popup(MkDialog, {
...props, ...props,
showCancelButton: true, showCancelButton: true,
}, { }, {
@ -182,7 +191,7 @@ export function inputText(props: {
canceled: false; result: string; canceled: false; result: string;
}> { }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { popup(MkDialog, {
title: props.title, title: props.title,
text: props.text, text: props.text,
input: { input: {
@ -207,7 +216,7 @@ export function inputNumber(props: {
canceled: false; result: number; canceled: false; result: number;
}> { }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { popup(MkDialog, {
title: props.title, title: props.title,
text: props.text, text: props.text,
input: { input: {
@ -232,7 +241,7 @@ export function inputDate(props: {
canceled: false; result: Date; canceled: false; result: Date;
}> { }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { popup(MkDialog, {
title: props.title, title: props.title,
text: props.text, text: props.text,
input: { input: {
@ -269,7 +278,7 @@ export function select<C = any>(props: {
canceled: false; result: C; canceled: false; result: C;
}> { }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { popup(MkDialog, {
title: props.title, title: props.title,
text: props.text, text: props.text,
select: { select: {
@ -291,7 +300,7 @@ export function success() {
window.setTimeout(() => { window.setTimeout(() => {
showing.value = false; showing.value = false;
}, 1000); }, 1000);
popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { popup(MkWaitingDialog, {
success: true, success: true,
showing: showing, showing: showing,
}, { }, {
@ -303,7 +312,7 @@ export function success() {
export function waiting() { export function waiting() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const showing = ref(true); const showing = ref(true);
popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { popup(MkWaitingDialog, {
success: false, success: false,
showing: showing, showing: showing,
}, { }, {
@ -366,7 +375,7 @@ export async function selectDriveFolder(multiple: boolean) {
export async function pickEmoji(src: HTMLElement | null, opts) { export async function pickEmoji(src: HTMLElement | null, opts) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { popup(MkEmojiPickerDialog, {
src, src,
...opts, ...opts,
}, { }, {
@ -431,7 +440,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
characterData: false, characterData: false,
}); });
openingEmojiPicker = await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerWindow.vue')), { openingEmojiPicker = await popup(MkEmojiPickerWindow, {
src, src,
...opts, ...opts,
}, { }, {
@ -454,7 +463,7 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
}) { }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let dispose; let dispose;
popup(defineAsyncComponent(() => import('@/components/MkPopupMenu.vue')), { popup(MkPopupMenu, {
items, items,
src, src,
width: options?.width, width: options?.width,
@ -478,7 +487,7 @@ export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent)
ev.preventDefault(); ev.preventDefault();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let dispose; let dispose;
popup(defineAsyncComponent(() => import('@/components/MkContextMenu.vue')), { popup(MkContextMenu, {
items, items,
ev, ev,
}, { }, {

View file

@ -4,12 +4,15 @@
<div style="overflow: clip;"> <div style="overflow: clip;">
<MkSpacer :content-max="600" :margin-min="20"> <MkSpacer :content-max="600" :margin-min="20">
<div class="_gaps_m znqjceqz"> <div class="_gaps_m znqjceqz">
<div ref="containerEl" v-panel class="about" :class="{ playing: easterEggEngine != null }"> <div v-panel class="about">
<div ref="containerEl" class="container" :class="{ playing: easterEggEngine != null }">
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/> <img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
<div class="misskey">Misskey</div> <div class="misskey">Misskey</div>
<div class="version">v{{ version }}</div> <div class="version">v{{ version }}</div>
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :is-reaction="false" :normal="true" :no-style="true"/></span> <span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :is-reaction="false" :normal="true" :no-style="true"/></span>
</div> </div>
<button v-if="thereIsTreasure" class="_button treasure" @click="getTreasure"><img src="/fluent-emoji/1f3c6.png" class="treasureImg"></button>
</div>
<div style="text-align: center;"> <div style="text-align: center;">
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a> {{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
</div> </div>
@ -70,6 +73,8 @@ import { i18n } from '@/i18n';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import * as os from '@/os'; import * as os from '@/os';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
import { $i } from '@/account';
const patrons = [ const patrons = [
'まっちゃとーにゅ', 'まっちゃとーにゅ',
@ -152,6 +157,8 @@ const patrons = [
'pixeldesu', 'pixeldesu',
]; ];
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
let easterEggReady = false; let easterEggReady = false;
let easterEggEmojis = $ref([]); let easterEggEmojis = $ref([]);
let easterEggEngine = $ref(null); let easterEggEngine = $ref(null);
@ -187,6 +194,11 @@ function iLoveMisskey() {
}); });
} }
function getTreasure() {
thereIsTreasure = false;
claimAchievement('foundTreasure');
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (easterEggEngine) { if (easterEggEngine) {
easterEggEngine.stop(); easterEggEngine.stop();
@ -206,10 +218,27 @@ definePageMetadata({
<style lang="scss" scoped> <style lang="scss" scoped>
.znqjceqz { .znqjceqz {
> .about { > .about {
position: relative;
border-radius: var(--radius);
> .treasure {
position: absolute;
top: 60px;
left: 0;
right: 0;
margin: 0 auto;
width: min-content;
> .treasureImg {
width: 25px;
vertical-align: bottom;
}
}
> .container {
position: relative; position: relative;
text-align: center; text-align: center;
padding: 16px; padding: 16px;
border-radius: var(--radius);
&.playing { &.playing {
&, * { &, * {
@ -230,21 +259,28 @@ definePageMetadata({
width: 80px; width: 80px;
margin: 0 auto; margin: 0 auto;
border-radius: 16px; border-radius: 16px;
position: relative;
z-index: 1;
} }
> .misskey { > .misskey {
margin: 0.75em auto 0 auto; margin: 0.75em auto 0 auto;
width: max-content; width: max-content;
position: relative;
z-index: 1;
} }
> .version { > .version {
margin: 0 auto; margin: 0 auto;
width: max-content; width: max-content;
opacity: 0.5; opacity: 0.5;
position: relative;
z-index: 1;
} }
> .emoji { > .emoji {
position: absolute; position: absolute;
z-index: 1;
top: 0; top: 0;
left: 0; left: 0;
visibility: hidden; visibility: hidden;
@ -256,5 +292,6 @@ definePageMetadata({
} }
} }
} }
}
} }
</style> </style>

View file

@ -86,7 +86,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import XEmojis from './about.emojis.vue'; import XEmojis from './about.emojis.vue';
import XFederation from './about.federation.vue'; import XFederation from './about.federation.vue';
import { version, instanceName, host } from '@/config'; import { version, instanceName, host } from '@/config';
@ -100,6 +100,7 @@ import * as os from '@/os';
import number from '@/filters/number'; import number from '@/filters/number';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { claimAchievement } from '@/scripts/achievements';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
initialTab?: string; initialTab?: string;
@ -110,6 +111,12 @@ const props = withDefaults(defineProps<{
let stats = $ref(null); let stats = $ref(null);
let tab = $ref(props.initialTab); let tab = $ref(props.initialTab);
watch($$(tab), () => {
if (tab === 'charts') {
claimAchievement('viewInstanceChart');
}
});
const initStats = () => os.api('stats', { const initStats = () => os.api('stats', {
}).then((res) => { }).then((res) => {
stats = res; stats = res;

View file

@ -0,0 +1,54 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<MkSpacer :content-max="1200">
<MkAchievements :user="$i"/>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
import MkAchievements from '@/components/MkAchievements.vue';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { $i } from '@/account';
import { claimAchievement } from '@/scripts/achievements';
let timer: number | null;
function viewAchievements3min() {
claimAchievement('viewAchievements3min');
}
onMounted(() => {
if (timer == null) timer = window.setTimeout(viewAchievements3min, 1000 * 60 * 3);
});
onUnmounted(() => {
if (timer != null) {
window.clearTimeout(timer);
timer = null;
}
});
onActivated(() => {
if (timer == null) timer = window.setTimeout(viewAchievements3min, 1000 * 60 * 3);
});
onDeactivated(() => {
if (timer != null) {
window.clearTimeout(timer);
timer = null;
}
});
definePageMetadata({
title: i18n.ts.achievements,
icon: 'ti ti-military-award',
});
</script>
<style lang="scss" module>
</style>

View file

@ -7,7 +7,7 @@
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton> <MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role.baseRole }}</template> <template #label>{{ i18n.ts._role.baseRole }}</template>
<div class="_gaps"> <div class="_gaps_s">
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template> <template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
<template #suffix>{{ Math.floor(policies.rateLimitFactor * 100) }}%</template> <template #suffix>{{ Math.floor(policies.rateLimitFactor * 100) }}%</template>

View file

@ -16,6 +16,7 @@
<div class="_buttons"> <div class="_buttons">
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> <MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton> <MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
<MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
@ -94,6 +95,85 @@ Ui:render([
]) ])
`; `;
const PRESET_SHUFFLE = `/// @ 0.12.2
//
let string = "ペペロンチーノ"
let length = string.len
//
var results = []
//
var cursor = 0
@do() {
if (cursor != 0) {
results = results.slice(0 (cursor + 1))
cursor = 0
}
let chars = []
for (let i, length) {
let r = Math:rnd(0 (length - 1))
chars.push(string.pick(r))
}
let result = chars.join("")
results.push(result)
// UI
render(result)
}
@back() {
cursor = cursor + 1
let result = results[results.len - (cursor + 1)]
render(result)
}
@forward() {
cursor = cursor - 1
let result = results[results.len - (cursor + 1)]
render(result)
}
@render(result) {
Ui:render([
Ui:C:container({
align: 'center'
children: [
Ui:C:mfm({ text: result })
Ui:C:buttons({
buttons: [{
text: "←"
disabled: !(results.len > 1 && (results.len - cursor) > 1)
onClick: back
} {
text: "→"
disabled: !(results.len > 1 && cursor > 0)
onClick: forward
} {
text: "引き直す"
onClick: do
}]
})
Ui:C:postFormButton({
text: "投稿する"
rounded: true
primary: true
form: {
text: \`{result}{Str:lf}{THIS_URL}\`
}
})
]
})
])
}
do()
`;
const PRESET_TIMELINE = `/// @ 0.12.2 const PRESET_TIMELINE = `/// @ 0.12.2
// API // API
@ -174,6 +254,11 @@ function selectPreset(ev: MouseEvent) {
action: () => { action: () => {
script = PRESET_OMIKUJI; script = PRESET_OMIKUJI;
}, },
}, {
text: 'Shuffle',
action: () => {
script = PRESET_SHUFFLE;
},
}, { }, {
text: 'Timeline viewer', text: 'Timeline viewer',
action: () => { action: () => {
@ -212,6 +297,19 @@ function show() {
} }
} }
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('deleteAreYouSure', { x: flash.title }),
});
if (canceled) return;
await os.apiWithDialog('flash/delete', {
flashId: props.id,
});
router.push('/play');
}
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);

View file

@ -11,13 +11,15 @@
</div> </div>
<div v-else-if="tab === 'my'" class="my"> <div v-else-if="tab === 'my'" class="my">
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> <div class="_gaps">
<MkButton class="new" gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="myFlashsPagination"> <MkPagination v-slot="{items}" :pagination="myFlashsPagination">
<div class="_gaps_s"> <div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/> <MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/>
</div> </div>
</MkPagination> </MkPagination>
</div> </div>
</div>
<div v-else-if="tab === 'liked'" class=""> <div v-else-if="tab === 'liked'" class="">
<MkPagination v-slot="{items}" :pagination="likedFlashsPagination"> <MkPagination v-slot="{items}" :pagination="likedFlashsPagination">

View file

@ -1,11 +1,15 @@
<template> <template>
<MkStickyContainer>
<template #header>
<MkPageHeader />
</template>
<div <div
ref="rootEl" ref="rootEl"
class="root" :class="$style['root']"
@dragover.prevent.stop="onDragover" @dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop" @drop.prevent.stop="onDrop"
> >
<div class="body"> <div :class="$style['body']">
<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination"> <MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
@ -17,7 +21,7 @@
<MkDateSeparatedList <MkDateSeparatedList
v-if="messages.length > 0" v-if="messages.length > 0"
v-slot="{ item: message }" v-slot="{ item: message }"
:class="{ messages: true, 'deny-move-transition': pFetching }" :class="{ [$style['messages']]: true, 'deny-move-transition': pFetching }"
:items="messages" :items="messages"
direction="up" direction="up"
reversed reversed
@ -27,23 +31,26 @@
</template> </template>
</MkPagination> </MkPagination>
</div> </div>
<footer> <footer :class="$style['footer']">
<div v-if="typers.length > 0" class="typers"> <div v-if="typers.length > 0" :class="$style['typers']">
<I18n :src="i18n.ts.typingUsers" text-tag="span" class="users"> <I18n :src="i18n.ts.typingUsers" text-tag="span">
<template #users> <template #users>
<b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b> <b v-for="typer in typers" :key="typer.id" :class="$style['user']">{{ typer.username }}</b>
</template> </template>
</I18n> </I18n>
<MkEllipsis/> <MkEllipsis/>
</div> </div>
<Transition :name="animation ? 'fade' : ''"> <Transition :name="animation ? 'fade' : ''">
<div v-show="showIndicator" class="new-message"> <div v-show="showIndicator" :class="$style['new-message']">
<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button> <button class="_buttonPrimary" @click="onIndicatorClick" :class="$style['new-message-button']">
<i class="fas ti-fw fa-arrow-circle-down" :class="$style['new-message-icon']"></i>{{ i18n.ts.newMessageExists }}
</button>
</div> </div>
</Transition> </Transition>
<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/> <XForm v-if="!fetching" ref="formEl" :user="user" :group="group" :class="$style['form']"/>
</footer> </footer>
</div> </div>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -303,14 +310,16 @@ definePageMetadata(computed(() => !fetching ? user ? {
} : null)); } : null));
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.root { .root {
display: content; display: content;
}
> .body { .body {
min-height: 80%; min-height: 80%;
}
.more { .more {
display: block; display: block;
margin: 16px auto; margin: 16px auto;
padding: 0 12px; padding: 0 12px;
@ -318,88 +327,81 @@ definePageMetadata(computed(() => !fetching ? user ? {
color: #fff; color: #fff;
background: rgba(#000, 0.3); background: rgba(#000, 0.3);
border-radius: 12px; border-radius: 12px;
&:hover { &:hover {
background: rgba(#000, 0.4); background: rgba(#000, 0.4);
} }
&:active { &:active {
background: rgba(#000, 0.5); background: rgba(#000, 0.5);
} }
&.fetching {
cursor: wait;
}
> i { > i {
margin-right: 4px; margin-right: 4px;
} }
} }
.messages { .fetching {
padding: 8px 0; cursor: wait;
}
> ::v-deep(*) { .messages {
padding: 16px 0 0;
> * {
margin-bottom: 16px; margin-bottom: 16px;
} }
} }
}
> footer { .footer {
width: 100%; width: 100%;
position: sticky; position: sticky;
z-index: 2; z-index: 2;
padding-top: 8px; padding-top: 8px;
bottom: 0; bottom: var(--minBottomSpacing);
bottom: env(safe-area-inset-bottom, 0px); }
> .new-message { .new-message {
width: 100%; width: 100%;
padding-bottom: 8px; padding-bottom: 8px;
text-align: center; text-align: center;
}
> button { .new-message-button {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
padding: 0 12px; padding: 0 12px;
line-height: 32px; line-height: 32px;
font-size: 12px; font-size: 12px;
border-radius: 16px; border-radius: 16px;
}
> i { .new-message-icon {
display: inline-block; display: inline-block;
margin-right: 8px; margin-right: 8px;
} }
}
}
> .typers { .typers {
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
padding: 0 8px 0 8px; padding: 0 8px 0 8px;
font-size: 0.9em; font-size: 0.9em;
color: var(--fgTransparentWeak); color: var(--fgTransparentWeak);
}
> .users {
> .user + .user:before { .user + .user:before {
content: ", "; content: ", ";
font-weight: normal; font-weight: normal;
} }
> .user:last-of-type:after { .user:last-of-type:after {
content: " "; content: " ";
} }
}
}
> .form { .form {
max-height: 12em; max-height: 12em;
overflow-y: scroll; overflow-y: scroll;
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
}
}
} }
.fade-enter-active, .fade-leave-active { .fade-enter-active, .fade-leave-active {

View file

@ -47,6 +47,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui'; import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
import MkAsUi from '@/components/MkAsUi.vue'; import MkAsUi from '@/components/MkAsUi.vue';
import { miLocalStorage } from '@/local-storage'; import { miLocalStorage } from '@/local-storage';
import { claimAchievement } from '@/scripts/achievements';
const parser = new Parser(); const parser = new Parser();
let aiscript: Interpreter; let aiscript: Interpreter;
@ -90,6 +91,9 @@ async function run() {
}); });
}, },
out: (value) => { out: (value) => {
if (value.type === 'str' && value.value.toLowerCase().replace(',', '').includes('hello world')) {
claimAchievement('outputHelloWorldOnScratchpad');
}
logs.value.push({ logs.value.push({
id: Math.random(), id: Math.random(),
text: value.type === 'str' ? value.value : utils.valToString(value), text: value.type === 'str' ? value.value : utils.valToString(value),

Some files were not shown because too many files have changed in this diff Show more