Compare commits

..

18 Commits

Author SHA1 Message Date
syuilo
d56fc41865 Merge branch 'develop' 2023-01-16 15:21:43 +09:00
rinsuki
fcabc99303 masterブランチをmaster_securityとマージ (#9260)
* Fix: forkbomb 2

* 12.119.2

Co-authored-by: mei23 <m@m544.net>
2022-12-04 05:34:51 +09:00
syuilo
fccd9c32e8 12.119.1 2022-12-03 18:42:19 +09:00
syuilo
58a3a0b7d4 forkbomb DOS mitigation 2022-12-03 18:42:06 +09:00
syuilo
a2a1636c10 Merge branch 'develop' 2022-09-10 19:26:43 +09:00
syuilo
46ec0303b7 Merge branch 'develop' 2022-08-08 11:39:04 +09:00
syuilo
3b1669fb6b Merge branch 'develop' 2022-08-07 00:39:21 +09:00
syuilo
09591fa4ae Merge branch 'develop' 2022-07-19 17:22:32 +09:00
syuilo
85ce00adc0 Merge branch 'develop' 2022-07-18 05:07:12 +09:00
syuilo
f25518af91 Merge branch 'develop' 2022-07-17 05:35:36 +09:00
syuilo
b796aacf7f Merge branch 'develop' 2022-07-16 23:53:24 +09:00
syuilo
ff24811676 Merge branch 'develop' 2022-07-16 18:21:44 +09:00
syuilo
4c8a1867f0 Merge branch 'develop' 2022-07-15 22:45:13 +09:00
syuilo
bce48dfee9 Merge branch 'develop' 2022-07-13 21:59:47 +09:00
syuilo
c20311b8a7 Merge branch 'develop' 2022-07-09 18:32:55 +09:00
syuilo
fb14ac50b8 Merge branch 'develop' 2022-07-08 17:34:53 +09:00
syuilo
84d984bd31 Merge branch 'develop' 2022-07-07 21:23:03 +09:00
syuilo
1bc856c451 Merge pull request #8821 from misskey-dev/develop
Release: 12.111.1
2022-06-13 00:41:09 +09:00
77 changed files with 258 additions and 2679 deletions

View File

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

View File

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

View File

@@ -9,21 +9,6 @@
You should also include the user name that made the change.
-->
## 13.x.x (unreleased)
### Improvements
- 実績機能
- Playのプリセットを追加
- Playのscriptの文字数制限を緩和
- AiScript GUIの強化
- リアクション一覧詳細ダイアログを表示できるように
- 存在しないカスタム絵文字をテキストで表示するように
- Alt text in image viewer
- ジョブキューのプロセスとWebサーバーのプロセスを分離
### Bugfixes
- playを削除する手段がなかったのを修正
## 13.0.0 (2023/01/16)
### TL;DR
@@ -47,16 +32,13 @@ You should also include the user name that made the change.
- Elasticsearchのサポートが削除されました
- 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます
- Yarnからpnpmに移行されました
corepackの有効化を推奨します: `sudo corepack enable`
- インスタンスブロックはサブドメインにも適用されるようになります
- ロールの導入に伴い、いくつかの機能がロールと統合されました
- モデレーターはロールに統合されました。今までのモデレーター情報は失われるため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。
- サイレンスはロールに統合されました。今までのユーザーは恩赦されるため、予めサイレンス一覧を記録しておくのをおすすめします。
- ユーザーごとのドライブ容量設定はロールに統合されました。
- インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールもしくはコンディショナルロールでドライブ容量を編集してください。
- インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールドライブ容量を編集してください。
- LTL/GTLの解放状態はロールに統合されました。
- Dockerの実行をrootで行わないようにしました。Dockerかつオブジェクトストレージを使用していない場合は`chown -hR 991.991 ./files`を実行してください。
https://github.com/misskey-dev/misskey/pull/9560
#### For users
- ノートのウォッチ機能が削除されました

View File

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

View File

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

View File

@@ -931,12 +931,10 @@ undefined: "Undefiniert"
assign: "Zuweisen"
unassign: "Entfernen"
color: "Farbe"
manageCustomEmojis: "Kann benutzerdefinierte Emojis verwalten"
manageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht."
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."
preset: "Vorlage"
selectFromPresets: "Aus Vorlagen wählen"
_role:
new: "Rolle erstellen"
edit: "Rolle bearbeiten"
@@ -945,7 +943,7 @@ _role:
permission: "Rollenberechtigungen"
descriptionOfPermission: "<b>Moderatoren</b> können grundlegende Verwaltungsaufgaben erledigen.\n<b>Administratoren</b> können alle Einstellungen der Instanz verwalten."
assignTarget: "Zuweisungsart"
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."
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."
manual: "Manuell"
conditional: "Konditional"
condition: "Bedingung"
@@ -968,7 +966,7 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen"
canInvite: "Kann Einladungscodes für diese Instanz erstellen"
canInvite: "Einladungscodes für diese Instanz erstellen"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
driveCapacity: "Drive-Kapazität"
pinMax: "Maximale Anzahl an angehefteten Notizen"
@@ -981,7 +979,6 @@ _role:
userEachUserListsMax: "Maximale Anzahl an Benutzerlisten"
rateLimitFactor: "Versuchsanzahl"
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
canHideAds: "Kann Werbung ausblenden"
_condition:
isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz"
@@ -1026,7 +1023,7 @@ _accountDelete:
_ad:
back: "Zurück"
reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen"
hide: "Ausblenden"
hide: "Nie anzeigen"
_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."
ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator."

View File

@@ -935,7 +935,7 @@ manageCustomEmojis: "Manage Custom Emojis"
youCannotCreateAnymore: "You've hit the creation limit."
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."
preset: "Preset"
preset: "Presets"
selectFromPresets: "Choose from presets"
_role:
new: "New role"
@@ -954,10 +954,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."
options: "Role options"
policies: "Policies"
baseRole: "Role template"
useBaseValue: "Use role template value"
baseRole: "Base role"
useBaseValue: "Use base role value"
chooseRoleToAssign: "Select the role to assign"
canEditMembersByModerator: "Allow moderators to edit the list of members for this role"
canEditMembersByModerator: "Allow moderators to edit the list members of 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."
priority: "Priority"
_priority:
@@ -965,11 +965,11 @@ _role:
middle: "Medium"
high: "High"
_options:
gtlAvailable: "Can view the global timeline"
ltlAvailable: "Can view the local timeline"
gtlAvailable: "Viewing the global timeline"
ltlAvailable: "Viewing the local timeline"
canPublicNote: "Can send public notes"
canInvite: "Can create instance invite codes"
canManageCustomEmojis: "Can manage custom emojis"
canInvite: "Create instance invite codes"
canManageCustomEmojis: "Manage Custom Emojis"
driveCapacity: "Drive capacity"
pinMax: "Maximum number of pinned notes"
antennaMax: "Maximum number of antennas"
@@ -981,7 +981,7 @@ _role:
userEachUserListsMax: "Maximum number of users within a user list"
rateLimitFactor: "Rate limit"
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
canHideAds: "Can hide ads"
canHideAds: "Remove ads"
_condition:
isLocal: "Local user"
isRemote: "Remote user"
@@ -1026,7 +1026,7 @@ _accountDelete:
_ad:
back: "Back"
reduceFrequencyOfThisAd: "Show this ad less"
hide: "Hide"
hide: "Never show"
_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."
ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead."

View File

@@ -2,7 +2,6 @@
_lang_: "Bahasa Indonesia"
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🚀"
poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka <b>Misskey</b>."
monthAndDay: "{day} {month}"
search: "Penelusuran"
notifications: "Pemberitahuan"
@@ -48,7 +47,6 @@ 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."
addToList: "Tambahkan ke daftar"
sendMessage: "Kirim pesan"
copyRSS: "Salin RSS"
copyUsername: "Salin nama pengguna"
searchUser: "Cari pengguna"
reply: "Balas"
@@ -385,7 +383,6 @@ administrator: "Admin"
token: "Token"
twoStepAuthentication: "Otentikasi dua faktor"
moderator: "Moderator"
moderation: "Moderasi"
nUsersMentioned: "{n} pengguna disebut"
securityKey: "Kunci keamanan"
securityKeyName: "Nama kunci"
@@ -452,7 +449,6 @@ language: "Bahasa"
uiLanguage: "Bahasa antarmuka pengguna"
groupInvited: "Telah diundang ke grup"
aboutX: "Tentang {x}"
emojiStyle: "Gaya emoji"
disableDrawer: "Jangan gunakan menu bergaya laci"
youHaveNoGroups: "Kamu tidak memiliki grup"
joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri."
@@ -565,7 +561,6 @@ author: "Pembuat"
leaveConfirm: "Ada perubahan yang belum disimpan. Apakah kamu ingin membuangnya?"
manage: "Manajemen"
plugins: "Plugin"
preferencesBackups: "Aturan pencadangan"
deck: "Dek"
undeck: "Keluar dari dek"
useBlurEffectForModal: "Gunakan efek buram untuk modal"
@@ -711,7 +706,6 @@ accentColor: "Aksen"
textColor: "Teks"
saveAs: "Simpan sebagai…"
advanced: "Tingkat lanjut"
advancedSettings: "Pengaturan Lanjut"
value: "Nilai"
createdAt: "Dibuat pada"
updatedAt: "Diperbarui pada"
@@ -856,21 +850,10 @@ rateLimitExceeded: "Batas sudah terlampaui"
cropImage: "potong gambar"
cropImageAsk: "Ingin memotong gambar?"
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"
colored: "Diwarnai"
refreshInterval: "Jeda pembaharuan"
label: "Label"
type: "Tipe"
localOnly: "Hanya lokal"
shuffle: "Acak"
account: "Akun"
like: "Suka"
unlike: "Tidak Suka"

View File

@@ -836,7 +836,7 @@ hide: "Nascondere"
leaveGroup: "Esci dal gruppo"
leaveGroupConfirm: "Uscire da「{name}」?"
useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile"
welcomeBackWithName: "Ciao, {name}! Eccoti di nuovo!"
welcomeBackWithName: "Eccoti di nuovo, {name}! Ciao!"
clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email."
overridedDeviceKind: "Tipo di dispositivo"
smartphone: "Smartphone"
@@ -935,8 +935,6 @@ manageCustomEmojis: "Gestisci le emoji personalizzate"
youCannotCreateAnymore: "Non puoi creare, hai raggiunto il limite."
cannotPerformTemporary: "Indisponibilità temporanea"
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"
_role:
new: "Nuovo ruolo"
edit: "Modifica ruolo"
@@ -981,7 +979,6 @@ _role:
userEachUserListsMax: "Quantità massima di profili per lista"
rateLimitFactor: "Limite del rapporto"
descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più."
canHideAds: "Può nascondere i banner"
_condition:
isLocal: "Profilo locale"
isRemote: "Profilo remoto"

View File

@@ -110,7 +110,6 @@ clickToShow: "クリックして表示"
sensitive: "閲覧注意"
add: "追加"
reaction: "リアクション"
reactions: "リアクション"
reactionSetting: "ピッカーに表示するリアクション"
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
rememberNoteVisibility: "公開範囲を記憶する"
@@ -938,222 +937,6 @@ cannotPerformTemporary: "一時的に利用できません"
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: "ートを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: "初めてノートをお気に入りに登録した"
_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個以上獲得した"
_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を越す"
_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: "誕生日にログインした"
_cookieClicked:
title: "クッキーをクリックするゲーム"
description: "クッキーをクリックした"
flavor: "ソフト間違ってない?"
_brainDiver:
title: "Brain Diver"
description: "Brain Diverへのリンクを投稿した"
flavor: "Misskey-Misskey La-Tu-Ma"
_role:
new: "ロールの作成"
@@ -1851,7 +1634,6 @@ _notification:
pollEnded: "アンケートの結果が出ました"
unreadAntennaNote: "アンテナ {name}"
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得"
_types:
all: "すべて"

View File

@@ -935,14 +935,12 @@ manageCustomEmojis: "커스텀 이모지 관리"
youCannotCreateAnymore: "더 이상 생성할 수 없습니다."
cannotPerformTemporary: "일시적으로 사용할 수 없음"
cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시적으로 사용이 불가합니다. 잠시 후 다시 시도해 주세요."
preset: "프리셋"
selectFromPresets: "프리셋에서 선택"
_role:
new: "새 역할 생성"
edit: "역할 수정"
name: "역할 이름"
description: "역할 설명"
permission: "역할 권한"
permission: "역할 권한"
descriptionOfPermission: "<b>모더레이터</b>는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n<b>관리자</b>는 인스턴스의 모든 설정을 변경할 수 있습니다."
assignTarget: "할당 대상"
descriptionOfAssignTarget: "<b>수동</b>을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n<b>조건부</b>를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다."
@@ -950,7 +948,7 @@ _role:
conditional: "조건부"
condition: "조건"
isConditionalRole: "조건부 역할입니다."
isPublic: "역할 공개"
isPublic: "공개 역할"
descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다."
options: "옵션"
policies: "정책"
@@ -958,7 +956,7 @@ _role:
useBaseValue: "기본값 사용"
chooseRoleToAssign: "할당할 역할 선택"
canEditMembersByModerator: "모더레이터의 역할 수정 허용"
descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다."
descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 추가하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 가능합니다."
priority: "우선순위"
_priority:
low: "낮음"
@@ -973,20 +971,19 @@ _role:
driveCapacity: "드라이브 용량"
pinMax: "고정할 수 있는 노트 수"
antennaMax: "최대 안테나 생성 허용 수"
wordMuteMax: "단어 뮤트할 수 있는 문자 수"
webhookMax: "생성할 수 있는 웹훅 수"
wordMuteMax: "뮤트할 수 있는 단어의 수"
webhookMax: "생성할 수 있는 WebHook의 수"
clipMax: "생성할 수 있는 클립 수"
noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수"
userListMax: "생성할 수 있는 유저 리스트 수"
userEachUserListsMax: "유저 리스트당 최대 사용자 수"
userListMax: "생성할 수 있는 리스트 수"
userEachUserListsMax: "리스트당 최대 사용자 수"
rateLimitFactor: "속도 제한"
descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다."
canHideAds: "광고 숨기기"
_condition:
isLocal: "로컬 사용자"
isRemote: "리모트 사용자"
createdLessThan: "가압한 지 다음 일수 이내 유저"
createdMoreThan: "가입한 지 다음 일수 이상 유저"
createdLessThan: "다음 일수 이내에 가입한 유저"
createdMoreThan: "다음 일수 이상 활동한 유저"
followersLessThanOrEq: "팔로워 수가 다음 이하인 유저"
followersMoreThanOrEq: "팔로워 수가 다음 이상인 유저"
followingLessThanOrEq: "팔로잉 수가 다음 이하인 유저"

View File

@@ -586,7 +586,7 @@ pluginTokenRequestedDescription: "Цей плагін зможе викорис
notificationType: "Тип сповіщення"
edit: "Редагувати"
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
emailServer: "Email сервер"
emailServer: "Сервер електронної пошти"
enableEmail: "Увімкнути функцію доставки пошти"
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
email: "E-mail"
@@ -892,10 +892,7 @@ unsubscribePushNotification: "Вимкнути push-сповіщення"
windowMaximize: "Розгорнути"
windowRestore: "Відновити"
caption: "Підпис"
tools: "Інструменти"
like: "Вподобати"
unlike: "Не вподобати"
numberOfLikes: "Вподобання"
show: "Відображення"
color: "Колір"
_role:

View File

@@ -607,7 +607,7 @@ wordMute: "文字屏蔽"
regexpError: "正则表达式错误"
regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:"
instanceMute: "实例的屏蔽"
userSaysSomething: "{name}说了什么,但是被屏蔽词过滤了"
userSaysSomething: "{name}说了什么,但是被屏蔽了"
makeActive: "启用"
display: "显示"
copy: "复制"
@@ -826,7 +826,7 @@ makeReactionsPublicDescription: "将您发表过的回应设置成公开可见
classic: "经典"
muteThread: "屏蔽帖子列表"
unmuteThread: "取消屏蔽帖子列表"
ffVisibility: "关注关系的可见范围"
ffVisibility: "连接的可见范围"
ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围"
continueThread: "查看更多帖子"
deleteAccountConfirm: "将要删除账户。是否确认?"
@@ -981,7 +981,6 @@ _role:
userEachUserListsMax: "单个用户列表内用户数量限制"
rateLimitFactor: "速率限制"
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
canHideAds: "可以隐藏广告"
_condition:
isLocal: "是本地用户"
isRemote: "是远程用户"
@@ -1009,7 +1008,7 @@ _emailUnavailable:
mx: "邮件服务器不正确"
smtp: "邮件服务器没有响应"
_ffVisibility:
public: "公开"
public: "发布"
followers: "只有关注你的用户能看到"
private: "私密"
_signup:

View File

@@ -389,7 +389,7 @@ administrator: "管理員"
token: "權杖"
twoStepAuthentication: "兩階段驗證"
moderator: "監察員"
moderation: "監察"
moderation: "言論調節"
nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰"
securityKeyName: "金鑰名稱"
@@ -932,11 +932,8 @@ assign: "指派"
unassign: "取消指派"
color: "顏色"
manageCustomEmojis: "管理自訂表情符號"
youCannotCreateAnymore: "您無法再建立更多了。"
cannotPerformTemporary: "暫時無法進行"
cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無法進行。請過一段時間之後再嘗試。"
preset: "預設值"
selectFromPresets: "從預設值中選擇"
_role:
new: "建立角色"
edit: "編輯角色"
@@ -973,15 +970,8 @@ _role:
driveCapacity: "雲端硬碟容量"
pinMax: "置頂貼文的最大數量"
antennaMax: "可建立的天線數量"
wordMuteMax: "靜音文字的最大字數"
webhookMax: "可建立的Webhook數量"
clipMax: "可建立的摘錄數量"
noteEachClipsMax: "摘錄內貼文的最大數量"
userListMax: "可建立的使用者清單數量"
userEachUserListsMax: "使用者清單內使用者的最大數量"
rateLimitFactor: "速率限制"
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
canHideAds: "不顯示廣告"
_condition:
isLocal: "本地使用者"
isRemote: "遠端使用者"

View File

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

View File

@@ -1,11 +0,0 @@
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

@@ -1,33 +0,0 @@
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

@@ -1,11 +0,0 @@
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,8 +7,6 @@
"start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.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",
"watch": "node watch.mjs",
"lint": "tsc --noEmit && eslint --quiet \"src/**/*.ts\"",
@@ -131,7 +129,6 @@
},
"devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.120",
"@swc/cli": "^0.1.59",
"@swc/core": "1.3.26",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",

View File

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

View File

@@ -17,9 +17,6 @@ import { JanitorService } from '@/daemons/JanitorService.js';
import { QueueStatsService } from '@/daemons/QueueStatsService.js';
import { ServerStatsService } from '@/daemons/ServerStatsService.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';
const _filename = fileURLToPath(import.meta.url);
@@ -73,15 +70,6 @@ export async function masterMain() {
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');
if (!envOption.disableClustering) {
@@ -90,10 +78,15 @@ export async function masterMain() {
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
app.get(ChartManagementService).start();
app.get(JanitorService).start();
app.get(QueueStatsService).start();
app.get(ServerStatsService).start();
if (!envOption.noDaemons) {
const daemons = await NestFactory.createApplicationContext(DaemonModule, {
logger: new NestLogger(),
});
daemons.enableShutdownHooks();
daemons.get(JanitorService).start();
daemons.get(QueueStatsService).start();
daemons.get(ServerStatsService).start();
}
}
function showEnvironment(): void {

View File

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

View File

@@ -1,114 +0,0 @@
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',
'noteClipped1',
'noteFavorited1',
'profileFilled',
'markedAsCat',
'following1',
'following10',
'following50',
'following100',
'following300',
'followers1',
'followers10',
'followers50',
'followers100',
'followers300',
'followers500',
'followers1000',
'collectAchievements30',
'iLoveMisskey',
'client30min',
'noteDeletedWithin1min',
'postedAtLateNight',
'postedAt0min0sec',
'selfQuote',
'htl20npm',
'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: string,
): 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,7 +4,6 @@ import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
import { AntennaService } from './AntennaService.js';
import { AppLockService } from './AppLockService.js';
import { AchievementService } from './AchievementService.js';
import { CaptchaService } from './CaptchaService.js';
import { CreateNotificationService } from './CreateNotificationService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js';
@@ -129,7 +128,6 @@ const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useEx
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService };
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
@@ -257,7 +255,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AiService,
AntennaService,
AppLockService,
AchievementService,
CaptchaService,
CreateNotificationService,
CreateSystemUserService,
@@ -379,7 +376,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AiService,
$AntennaService,
$AppLockService,
$AchievementService,
$CaptchaService,
$CreateNotificationService,
$CreateSystemUserService,
@@ -502,7 +498,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AiService,
AntennaService,
AppLockService,
AchievementService,
CaptchaService,
CreateNotificationService,
CreateSystemUserService,
@@ -623,7 +618,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AiService,
$AntennaService,
$AppLockService,
$AchievementService,
$CaptchaService,
$CreateNotificationService,
$CreateSystemUserService,

View File

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

View File

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

View File

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

View File

@@ -114,9 +114,6 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'groupInvited' ? {
invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!),
} : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
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 { ILocalUser, IRemoteUser, User } 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, UserProfile } 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 } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -343,7 +343,6 @@ export class UserEntityService implements OnModuleInit {
options?: {
detail?: D,
includeSecrets?: boolean,
userProfile?: UserProfile,
},
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
const opts = Object.assign({
@@ -376,7 +375,7 @@ export class UserEntityService implements OnModuleInit {
.innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC')
.getMany() : [];
const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
const profile = opts.detail ? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followingCount :
@@ -494,8 +493,6 @@ export class UserEntityService implements OnModuleInit {
mutingNotificationTypes: profile!.mutingNotificationTypes,
emailNotificationTypes: profile!.emailNotificationTypes,
showTimelineReplies: user.showTimelineReplies ?? falsy,
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
} : {}),
...(opts.includeSecrets ? {

View File

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

View File

@@ -64,7 +64,6 @@ export class Notification {
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
* groupInvited - グループに招待された
* achievementEarned - 実績を獲得
* app - アプリ通知
*/
@Index()
@@ -130,11 +129,6 @@ export class Notification {
})
public choice: number | null;
@Column('varchar', {
length: 128, nullable: true,
})
public achievement: string | null;
/**
* アプリ通知のbody
*/

View File

@@ -213,19 +213,6 @@ export class UserProfile {
})
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
@Index()
@Column('varchar', {

View File

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

View File

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

View File

@@ -175,7 +175,6 @@ 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_apps from './endpoints/i/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_deleteAccount from './endpoints/i/delete-account.js';
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
@@ -330,7 +329,6 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
import * as ep___users_search from './endpoints/users/search.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_achievements from './endpoints/users/achievements.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___retention from './endpoints/retention.js';
import { GetterService } from './GetterService.js';
@@ -511,7 +509,6 @@ 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_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_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_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 };
@@ -666,7 +663,6 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-
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_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 $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
@@ -851,7 +847,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_unregister,
$i_apps,
$i_authorizedApps,
$i_claimAchievement,
$i_changePassword,
$i_deleteAccount,
$i_exportBlocking,
@@ -1006,7 +1001,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search,
$users_show,
$users_stats,
$users_achievements,
$fetchRss,
$retention,
],
@@ -1185,7 +1179,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_unregister,
$i_apps,
$i_authorizedApps,
$i_claimAchievement,
$i_changePassword,
$i_deleteAccount,
$i_exportBlocking,
@@ -1338,7 +1331,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search,
$users_show,
$users_stats,
$users_achievements,
$fetchRss,
$retention,
],

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -29,36 +29,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, user, token) => {
const isSecure = token == null;
const now = new Date();
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!, {
// ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す
return await this.userEntityService.pack<true, true>(user.id, user, {
detail: true,
includeSecrets: isSecure,
userProfile,
});
});
}

View File

@@ -1,28 +0,0 @@
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

@@ -1,31 +0,0 @@
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

@@ -24,11 +24,6 @@
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
const localeVersion = localStorage.getItem('localeVersion');
const localeOutdated = (localeVersion == null || localeVersion !== v);

View File

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

View File

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

View File

@@ -1,224 +0,0 @@
<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>
<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"/>
<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" :style="{ justifyContent: align }">
<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>
<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="c.onClick">{{ c.text }}</MkButton>
<div v-else-if="c.type === 'buttons'" class="_buttons">
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
</div>
<MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate">
<template v-if="c.label" #label>{{ c.label }}</template>
@@ -41,7 +41,7 @@
</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 }">
<template v-for="child in c.children" :key="child">
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
</template>
</div>
</div>
@@ -62,10 +62,8 @@ const props = withDefaults(defineProps<{
component: AsUiComponent;
components: Ref<AsUiComponent>[];
size: 'small' | 'medium' | 'large';
align: 'left' | 'center' | 'right';
}>(), {
size: 'medium',
align: 'left',
});
const c = props.component;

View File

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

View File

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

View File

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

View File

@@ -35,8 +35,6 @@ import * as Misskey from 'misskey-js';
import * as os from '@/os';
import { stream } from '@/stream';
import { i18n } from '@/i18n';
import { claimAchievement } from '@/scripts/achievements';
import { $i } from '@/account';
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed,
@@ -92,21 +90,6 @@ async function onClick() {
userId: props.user.id,
});
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) {

View File

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

View File

@@ -45,8 +45,7 @@ onMounted(() => {
src: media.url,
w: media.properties.width,
h: media.properties.height,
alt: media.comment || media.name,
comment: media.comment || media.name,
alt: media.name,
};
if (media.properties.orientation != null && media.properties.orientation >= 5) {
[item.w, item.h] = [item.h, item.w];
@@ -70,7 +69,6 @@ onMounted(() => {
},
imageClickAction: 'close',
tapAction: 'toggle-controls',
bgOpacity: 1,
pswpModule: PhotoSwipe,
});
@@ -90,28 +88,9 @@ onMounted(() => {
[itemData.w, itemData.h] = [itemData.h, itemData.w];
}
itemData.msrc = file.thumbnailUrl;
itemData.alt = file.comment || file.name;
itemData.comment = file.comment || file.name;
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();
});
@@ -206,36 +185,5 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
// なぜか機能しない
//z-index: v-bind(pswpZIndex);
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>

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
<div ref="elRef" :class="$style.root">
<div v-once :class="$style.head">
<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/>
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
<div :class="[$style.subIcon, $style['t_' + notification.type]]">
@@ -15,7 +14,6 @@
<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 === '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使うと一部ブラウザで刺さるので念の為 -->
<MkReactionIcon
v-else-if="notification.type === 'reaction'"
@@ -30,7 +28,6 @@
<div :class="$style.tail">
<header :class="$style.header">
<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>
<span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
@@ -60,9 +57,6 @@
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i>
</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 === '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>
@@ -88,7 +82,6 @@ import { i18n } from '@/i18n';
import * as os from '@/os';
import { stream } from '@/stream';
import { useTooltip } from '@/scripts/use-tooltip';
import { $i } from '@/account';
const props = withDefaults(defineProps<{
notification: misskey.entities.Notification;
@@ -247,12 +240,6 @@ useTooltip(reactionRef, (showing) => {
pointer-events: none;
}
.t_achievementEarned {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
.tail {
flex: 1;
min-width: 0;
@@ -280,9 +267,9 @@ useTooltip(reactionRef, (showing) => {
}
.text {
display: flex;
width: 100%;
overflow: clip;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.quote {

View File

@@ -10,7 +10,7 @@
<template #default="{ items: notifications }">
<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"/>
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="false" class="_panel notification"/>
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
</MkDateSeparatedList>
</template>
</MkPagination>

View File

@@ -93,12 +93,11 @@ import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { $i, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
import { uploadFile } from '@/scripts/upload';
import { deepClone } from '@/scripts/clone';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage';
import { claimAchievement } from '@/scripts/achievements';
const modal = inject('modal');
@@ -628,34 +627,6 @@ async function post(ev?: MouseEvent) {
}
posting = false;
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 => {
posting = false;

View File

@@ -1,92 +0,0 @@
<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,7 +20,6 @@ import * as os from '@/os';
import { useTooltip } from '@/scripts/use-tooltip';
import { $i } from '@/account';
import MkReactionEffect from '@/components/MkReactionEffect.vue';
import { claimAchievement } from '@/scripts/achievements';
const props = defineProps<{
reaction: string;
@@ -53,9 +52,6 @@ const toggleReaction = () => {
noteId: props.note.id,
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,28 +11,20 @@
<script lang="ts" setup>
import * as misskey from 'misskey-js';
import { onMounted } from 'vue';
import MkMiniChart from '@/components/MkMiniChart.vue';
import * as os from '@/os';
import { acct } from '@/filters/user';
const props = withDefaults(defineProps<{
const props = defineProps<{
user: misskey.entities.User;
withChart: boolean;
}>(), {
withChart: true,
});
}>();
let chartValues = $ref<number[] | null>(null);
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);
chartValues = res.inc;
});
}
os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16 + 1, span: 'day' }).then(res => {
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
res.inc.splice(0, 1);
chartValues = res.inc;
});
</script>

View File

@@ -1,6 +1,5 @@
<template>
<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-if="isCustom" :class="[$style.root, $style.custom, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async"/>
<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>{{ emoji }}</span>
@@ -38,7 +37,6 @@ const url = computed(() => {
}
});
const alt = computed(() => isCustom.value ? `:${customEmojiName}:` : char.value);
let errored = $ref(false);
// 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 {

View File

@@ -44,7 +44,6 @@ import { reactionPicker } from '@/scripts/reaction-picker';
import { getUrlWithoutLoginId } from '@/scripts/login-id';
import { getAccountFromId } from '@/scripts/get-account-from-id';
import { miLocalStorage } from './local-storage';
import { claimAchievement, claimedAchievements } from './scripts/achievements';
(async () => {
console.info(`Misskey v${version}`);
@@ -346,82 +345,6 @@ import { claimAchievement, claimedAchievements } from './scripts/achievements';
});
}
if ($i.birthday) {
const now = new Date();
const m = now.getMonth() + 1;
const d = now.getDate();
const bm = parseInt($i.birthday.split('-')[1]);
const bd = parseInt($i.birthday.split('-')[2]);
if (m === bm && d === bd) {
claimAchievement('loggedInOnBirthday');
}
}
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');
if (lastUsed) {
const lastUsedDate = parseInt(lastUsed, 10);

View File

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

View File

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

View File

@@ -1,25 +0,0 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<MkSpacer :content-max="1200">
<MkAchievements :user="$i"/>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MkAchievements from '@/components/MkAchievements.vue';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { $i } from '@/account';
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>
<MkFolder>
<template #label>{{ i18n.ts._role.baseRole }}</template>
<div class="_gaps_s">
<div class="_gaps">
<MkFolder>
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
<template #suffix>{{ Math.floor(policies.rateLimitFactor * 100) }}%</template>

View File

@@ -16,7 +16,6 @@
<div class="_buttons">
<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 v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
</MkSpacer>
@@ -95,85 +94,6 @@ 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
// APIリクエストを行いローカルタイムラインを表示するプリセット
@@ -254,11 +174,6 @@ function selectPreset(ev: MouseEvent) {
action: () => {
script = PRESET_OMIKUJI;
},
}, {
text: 'Shuffle',
action: () => {
script = PRESET_SHUFFLE;
},
}, {
text: 'Timeline viewer',
action: () => {
@@ -297,19 +212,6 @@ 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 headerTabs = $computed(() => []);

View File

@@ -11,14 +11,12 @@
</div>
<div v-else-if="tab === 'my'" class="my">
<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">
<div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/>
</div>
</MkPagination>
</div>
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
<div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'liked'" class="">

View File

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

View File

@@ -85,7 +85,6 @@ import { i18n } from '@/i18n';
import { $i } from '@/account';
import { langmap } from '@/scripts/langmap';
import { definePageMetadata } from '@/scripts/page-metadata';
import { claimAchievement } from '@/scripts/achievements';
const profile = reactive({
name: $i.name,
@@ -134,13 +133,6 @@ function save() {
isCat: !!profile.isCat,
showTimelineReplies: !!profile.showTimelineReplies,
});
claimAchievement('profileFilled');
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
claimAchievement('setNameToSyuilo');
}
if (profile.isCat) {
claimAchievement('markedAsCat');
}
}
function changeAvatar(ev) {
@@ -163,7 +155,6 @@ function changeAvatar(ev) {
});
$i.avatarId = i.avatarId;
$i.avatarUrl = i.avatarUrl;
claimAchievement('profileFilled');
});
}

View File

@@ -113,8 +113,7 @@
<div v-for="role in info.roles" :key="role.id" :class="$style.roleItem">
<MkRolePreview :class="$style.role" :role="role"/>
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
<button class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
</div>
</div>
</MkFolder>

View File

@@ -427,10 +427,6 @@ export const routes = [{
path: '/my/favorites',
component: page(() => import('./pages/favorites.vue')),
loginRequired: true,
}, {
path: '/my/achievements',
component: page(() => import('./pages/achievements.vue')),
loginRequired: true,
}, {
name: 'messaging',
path: '/my/messaging',

View File

@@ -1,425 +0,0 @@
import * as os from '@/os';
import { $i } from '@/account';
export 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',
'noteClipped1',
'noteFavorited1',
'profileFilled',
'markedAsCat',
'following1',
'following10',
'following50',
'following100',
'following300',
'followers1',
'followers10',
'followers50',
'followers100',
'followers300',
'followers500',
'followers1000',
'collectAchievements30',
'iLoveMisskey',
'client30min',
'noteDeletedWithin1min',
'postedAtLateNight',
'postedAt0min0sec',
'selfQuote',
'htl20npm',
'driveFolderCircularReference',
'reactWithoutRead',
'clickedClickHere',
'justPlainLucky',
'setNameToSyuilo',
'cookieClicked',
'brainDiver',
] as const;
export const ACHIEVEMENT_BADGES = {
'notes1': {
img: '/fluent-emoji/1f4dd.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'notes10': {
img: '/fluent-emoji/1f4d1.png',
bg: null,
frame: 'bronze',
},
'notes100': {
img: '/fluent-emoji/1f4d2.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'notes500': {
img: '/fluent-emoji/1f4da.png',
bg: null,
frame: 'bronze',
},
'notes1000': {
img: '/fluent-emoji/1f5c3.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'notes5000': {
img: '/fluent-emoji/1f304.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'notes10000': {
img: '/fluent-emoji/1f3d9.png',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'silver',
},
'notes20000': {
img: '/fluent-emoji/1f307.png',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'silver',
},
'notes30000': {
img: '/fluent-emoji/1f306.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'notes40000': {
img: '/fluent-emoji/1f303.png',
bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
frame: 'silver',
},
'notes50000': {
img: '/fluent-emoji/1fa90.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'gold',
},
'notes60000': {
img: '/fluent-emoji/2604.png',
bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
frame: 'gold',
},
'notes70000': {
img: '/fluent-emoji/1f30c.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'gold',
},
'notes80000': {
img: '/fluent-emoji/1f30c.png',
bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
frame: 'gold',
},
'notes90000': {
img: '/fluent-emoji/1f30c.png',
bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
frame: 'gold',
},
'notes100000': {
img: '/fluent-emoji/267e.png',
bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
frame: 'platinum',
},
'login3': {
img: '/fluent-emoji/1f331.png',
bg: null,
frame: 'bronze',
},
'login7': {
img: '/fluent-emoji/1f331.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'login15': {
img: '/fluent-emoji/1f331.png',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'bronze',
},
'login30': {
img: '/fluent-emoji/1fab4.png',
bg: null,
frame: 'bronze',
},
'login60': {
img: '/fluent-emoji/1fab4.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'login100': {
img: '/fluent-emoji/1fab4.png',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'silver',
},
'login200': {
img: '/fluent-emoji/1f333.png',
bg: null,
frame: 'silver',
},
'login300': {
img: '/fluent-emoji/1f333.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'silver',
},
'login400': {
img: '/fluent-emoji/1f333.png',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'silver',
},
'login500': {
img: '/fluent-emoji/1f304.png',
bg: null,
frame: 'silver',
},
'login600': {
img: '/fluent-emoji/1f304.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'gold',
},
'login700': {
img: '/fluent-emoji/1f304.png',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'gold',
},
'login800': {
img: '/fluent-emoji/1f307.png',
bg: null,
frame: 'gold',
},
'login900': {
img: '/fluent-emoji/1f307.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'gold',
},
'login1000': {
img: '/fluent-emoji/1f307.png',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'platinum',
},
'noteClipped1': {
img: '/fluent-emoji/1f587.png',
bg: null,
frame: 'bronze',
},
'noteFavorited1': {
img: '/fluent-emoji/1f31f.png',
bg: null,
frame: 'bronze',
},
'profileFilled': {
img: '/fluent-emoji/1f44c.png',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
'markedAsCat': {
img: '/fluent-emoji/1f408.png',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
'following1': {
img: '/fluent-emoji/2618.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'following10': {
img: '/fluent-emoji/1f6b8.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'following50': {
img: '/fluent-emoji/1f91d.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'following100': {
img: '/fluent-emoji/1f4af.png',
bg: 'linear-gradient(0deg, rgb(255 53 184), rgb(255 206 69))',
frame: 'silver',
},
'following300': {
img: '/fluent-emoji/1f970.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'followers1': {
img: '/fluent-emoji/2618.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'followers10': {
img: '/fluent-emoji/1f44b.png',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'followers50': {
img: '/fluent-emoji/1f411.png',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
'followers100': {
img: '/fluent-emoji/1f396.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'followers300': {
img: '/fluent-emoji/1f3c6.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'followers500': {
img: '/fluent-emoji/1f4e1.png',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'gold',
},
'followers1000': {
img: '/fluent-emoji/1f451.png',
bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
frame: 'platinum',
},
'collectAchievements30': {
img: '/fluent-emoji/1f3c5.png',
bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
frame: 'silver',
},
'iLoveMisskey': {
img: '/fluent-emoji/2764.png',
bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
frame: 'silver',
},
'client30min': {
img: '/fluent-emoji/1f552.png',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
'noteDeletedWithin1min': {
img: '/fluent-emoji/1f5d1.png',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
'postedAtLateNight': {
img: '/fluent-emoji/1f319.png',
bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
frame: 'bronze',
},
'postedAt0min0sec': {
img: '/fluent-emoji/1f55b.png',
bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))',
frame: 'bronze',
},
'selfQuote': {
img: '/fluent-emoji/1f4dd.png',
bg: null,
frame: 'bronze',
},
'htl20npm': {
img: '/fluent-emoji/1f30a.png',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
'driveFolderCircularReference': {
img: '/fluent-emoji/1f4c2.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'bronze',
},
'reactWithoutRead': {
img: '/fluent-emoji/2753.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'bronze',
},
'clickedClickHere': {
img: '/fluent-emoji/2757.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'bronze',
},
'justPlainLucky': {
img: '/fluent-emoji/1f340.png',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'silver',
},
'setNameToSyuilo': {
img: '/fluent-emoji/1f36e.png',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
'passedSinceAccountCreated1': {
img: '/fluent-emoji/0031-20e3.png',
bg: null,
frame: 'bronze',
},
'passedSinceAccountCreated2': {
img: '/fluent-emoji/0032-20e3.png',
bg: null,
frame: 'silver',
},
'passedSinceAccountCreated3': {
img: '/fluent-emoji/0033-20e3.png',
bg: null,
frame: 'gold',
},
'loggedInOnBirthday': {
img: '/fluent-emoji/1f382.png',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'cookieClicked': {
img: '/fluent-emoji/1f36a.png',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
'brainDiver': {
img: '/fluent-emoji/1f9e0.png',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'bronze',
},
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
img: string;
bg: string | null;
frame: 'bronze' | 'silver' | 'gold' | 'platinum';
}>;
export const claimedAchievements = ($i && $i.achievements) ? $i.achievements.map(x => x.name) : [];
export function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) {
if (claimedAchievements.includes(type)) return;
os.api('i/claim-achievement', { name: type });
claimedAchievements.push(type);
}
if (_DEV_) {
(window as any).unlockAllAchievements = async () => {
for (const t of ACHIEVEMENT_TYPES) {
await new Promise(resolve => setTimeout(resolve, 100));
claimAchievement(t);
}
};
}

View File

@@ -50,7 +50,6 @@ export type AsUiButton = AsUiComponentBase & {
onClick?: () => void;
primary?: boolean;
rounded?: boolean;
disabled?: boolean;
};
export type AsUiButtons = AsUiComponentBase & {
@@ -303,8 +302,6 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn,
if (primary) utils.assertBoolean(primary);
const rounded = def.value.get('rounded');
if (rounded) utils.assertBoolean(rounded);
const disabled = button.value.get('disabled');
if (disabled) utils.assertBoolean(disabled);
return {
text: text?.value,
@@ -313,7 +310,6 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn,
},
primary: primary?.value,
rounded: rounded?.value,
disabled: disabled?.value,
};
}
@@ -334,8 +330,6 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn,
if (primary) utils.assertBoolean(primary);
const rounded = button.value.get('rounded');
if (rounded) utils.assertBoolean(rounded);
const disabled = button.value.get('disabled');
if (disabled) utils.assertBoolean(disabled);
return {
text: text.value,
@@ -344,7 +338,6 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn,
},
primary: primary?.value,
rounded: rounded?.value,
disabled: disabled?.value,
};
}) : [],
};

View File

@@ -45,7 +45,7 @@ export function api<E extends keyof Endpoints, P extends Endpoints[E]['req']>(en
}
// Implements Misskey.api.ApiClient.request
export function apiGet <E extends keyof Endpoints, P extends Endpoints[E]['req']>(endpoint: E, data: P = {} as any): Promise<Endpoints[E]['res']> {
export function apiGet<E extends keyof Endpoints, P extends Endpoints[E]['req']>(endpoint: E, data: P = {} as any): Promise<Endpoints[E]['res']> {
pendingApiRequestsCount.value++;
const onFinally = () => {

View File

@@ -1,7 +1,6 @@
import { defineAsyncComponent, Ref, inject } from 'vue';
import * as misskey from 'misskey-js';
import { pleaseLogin } from './please-login';
import { claimAchievement } from './achievements';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
@@ -39,10 +38,6 @@ export function getNoteMenu(props: {
os.api('notes/delete', {
noteId: appearNote.id,
});
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) {
claimAchievement('noteDeletedWithin1min');
}
});
}
@@ -58,15 +53,10 @@ export function getNoteMenu(props: {
});
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) {
claimAchievement('noteDeletedWithin1min');
}
});
}
function toggleFavorite(favorite: boolean): void {
claimAchievement('noteFavorited1');
os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
noteId: appearNote.id,
});
@@ -128,13 +118,11 @@ export function getNoteMenu(props: {
const clip = await os.apiWithDialog('clips/create', result);
claimAchievement('noteClipped1');
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
},
}, null, ...clips.map(clip => ({
text: clip.name,
action: () => {
claimAchievement('noteClipped1');
os.promiseDialog(
os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
null,
@@ -186,17 +174,9 @@ export function getNoteMenu(props: {
url: `${url}/notes/${appearNote.id}`,
});
}
function notedetails(): void {
os.pageWindow(`/notes/${appearNote.id}`);
}
function showReactions(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkReactedUsersDialog.vue')), {
noteId: appearNote.id,
}, {}, 'closed');
}
async function translate(): Promise<void> {
if (props.translation.value != null) return;
props.translating.value = true;
@@ -226,10 +206,6 @@ export function getNoteMenu(props: {
icon: 'ti ti-info-circle',
text: i18n.ts.details,
action: notedetails,
}, {
icon: 'ti ti-users',
text: i18n.ts.reactions,
action: showReactions,
}, {
icon: 'ti ti-copy',
text: i18n.ts.copyContent,

View File

@@ -97,7 +97,6 @@ export default defineConfig(({ command, mode }) => {
output: {
manualChunks: {
vue: ['vue'],
photoswipe: ['photoswipe', 'photoswipe/lightbox', 'photoswipe/style.css'],
},
},
},

711
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff