Merge pull request #16840 from misskey-dev/develop

Release: 2025.11.1
This commit is contained in:
misskey-release-bot[bot] 2025-11-28 10:04:09 +00:00 committed by GitHub
commit 994fc062cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 5606 additions and 5615 deletions

View File

@ -1 +1 @@
FROM mcr.microsoft.com/devcontainers/javascript-node:0-18
FROM mcr.microsoft.com/devcontainers/javascript-node:4.0.3-24-trixie

View File

@ -28,7 +28,7 @@ services:
db:
restart: unless-stopped
image: postgres:15-alpine
image: postgres:18-alpine
networks:
- internal_network
environment:

View File

@ -76,7 +76,7 @@ body:
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
* Misskey: 2025.x.x
* Node: 20.x.x
* PostgreSQL: 15.x.x
* PostgreSQL: 18.x.x
* Redis: 7.x.x
* OS and Architecture: Ubuntu 24.04.2 LTS aarch64
value: |

View File

@ -38,7 +38,7 @@ jobs:
services:
postgres:
image: postgres:15
image: postgres:18
ports:
- 54312:5432
env:
@ -117,7 +117,7 @@ jobs:
services:
postgres:
image: postgres:15
image: postgres:18
ports:
- 54312:5432
env:
@ -165,7 +165,7 @@ jobs:
services:
postgres:
image: postgres:15
image: postgres:18
ports:
- 54312:5432
env:

View File

@ -64,7 +64,7 @@ jobs:
services:
postgres:
image: postgres:15
image: postgres:18
ports:
- 54312:5432
env:

View File

@ -1,3 +1,33 @@
## 2025.11.1
### Client
- Enhance: リアクションの受け入れ設定にキャプションを追加 #15921
- Fix: ページの内容がはみ出ることがある問題を修正
- Fix: ナビゲーションバーを下に表示しているときに、項目数が多いと表示が崩れる問題を修正
- Fix: ヘッダーメニューのチャンネルの新規作成の項目でチャンネル作成ページに飛べない問題を修正 #16816
- Fix: ラジオボタンに空白の選択肢が表示される問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1105)
- Fix: 一部のシチュエーションで投稿フォームのツアーが正しく表示されない問題を修正
- Fix: 投稿フォームのリセットボタンで注釈がリセットされない問題を修正
- Fix: PlayのAiScriptバージョン判定v0.x系・v1.x系の判定が正しく動作しない問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1129)
- Fix: フォロー申請をキャンセルする際の確認ダイアログの文言が不正確な問題を修正
- Fix: 初回読み込み時にエラーになることがある問題を修正
- Fix: お気に入りクリップの一覧表示が正しく動作しない問題を修正
- Fix: AiScript Misskey 拡張APIにおいて、各種関数の引数で明示的に `null` が指定されている場合のハンドリングを修正
### Server
- Enhance: メモリ使用量を削減しました
- Enhance: 依存関係の更新
- Fix: ワードミュートの文字数計算を修正
- Fix: チャンネルのリアルタイム更新時に、ロックダウン設定にて非ログイン時にノートを表示しない設定にしている場合でもノートが表示されてしまう問題を修正
- Fix: DeepL APIのAPIキー指定方式変更に対応
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1096)
- 内部実装の変更にて対応可能な更新です。Misskey側の設定方法に変更はありません。
- Fix: DBレプリケーションを利用する環境でクエリーが失敗する問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1123)
## 2025.11.0
### General

View File

@ -27,7 +27,7 @@ spec:
ports:
- containerPort: 3000
- name: postgres
image: postgres:15-alpine
image: postgres:18-alpine
env:
- name: POSTGRES_USER
value: "example-misskey-user"

View File

@ -15,7 +15,7 @@ services:
db:
restart: always
image: postgres:15-alpine
image: postgres:18-alpine
ports:
- "5432:5432"
env_file:

View File

@ -37,7 +37,7 @@ services:
db:
restart: always
image: postgres:15-alpine
image: postgres:18-alpine
networks:
- internal_network
env_file:

View File

@ -83,6 +83,8 @@ files: "Fitxers"
download: "Descarregar"
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer també seran esborrades."
unfollowConfirm: "Segur que vols deixar de seguir a {name}?"
cancelFollowRequestConfirm: "Vols cancel·lar la teva sol·licitud de seguiment a {name}?"
rejectFollowRequestConfirm: "Vols rebutjar la sol·licitud de seguiment de {name}?"
exportRequested: "Has sol·licitat una exportació de dades. Això pot trigar una estona. S'afegirà a la teva unitat de disc un cop estigui completada."
importRequested: "Has sol·licitat una importació de dades. Això pot trigar una estona."
lists: "Llistes"

View File

@ -1556,6 +1556,8 @@ _preferencesProfile:
profileNameDescription: "Set a name that identifies this device."
profileNameDescription2: "Example: \"Main PC\", \"Smartphone\""
manageProfiles: "Manage Profiles"
shareSameProfileBetweenDevicesIsNotRecommended: "We do not recommend sharing the same profile across multiple devices."
useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "If there are settings you wish to synchronize across multiple devices, enable the “Synchronize across multiple devices” option individually for each device."
_preferencesBackup:
autoBackup: "Auto backup"
restoreFromBackup: "Restore from backup"

View File

@ -83,6 +83,8 @@ files: "Archivos"
download: "Descargar"
driveFileDeleteConfirm: "¿Desea borrar el archivo \"{name}\"? Las notas que tengan este archivo como adjunto serán eliminadas"
unfollowConfirm: "¿Desea dejar de seguir a {name}?"
cancelFollowRequestConfirm: "¿Desea cancelar su solicitud de seguimiento a {name}?"
rejectFollowRequestConfirm: "¿Desea rechazar la solicitud de seguimiento de {name}?"
exportRequested: "Has solicitado la exportación. Puede llevar un tiempo. Cuando termine la exportación, se añadirá al drive"
importRequested: "Has solicitado la importación. Puede llevar un tiempo."
lists: "Listas"
@ -1354,7 +1356,7 @@ textCount: "caracteres"
information: "Información"
chat: "Chat"
directMessage: "Chatear"
directMessage_short: "Mensaje"
directMessage_short: "Mensajes"
migrateOldSettings: "Migrar la configuración anterior"
migrateOldSettings_description: "Esto debería hacerse automáticamente, pero si por alguna razón la migración no ha tenido éxito, puede activar usted mismo el proceso de migración manualmente. Se sobrescribirá la información de configuración actual."
compress: "Compresión de la imagen"
@ -1457,7 +1459,7 @@ _order:
newest: "Los más recientes primero"
oldest: "Los más antiguos primero"
_chat:
messages: "Mensaje"
messages: "Mensajes"
noMessagesYet: "Aún no hay mensajes"
newMessage: "Mensajes nuevos"
individualChat: "Chat individual"

8
locales/index.d.ts vendored
View File

@ -350,6 +350,14 @@ export interface Locale extends ILocale {
* {name}
*/
"unfollowConfirm": ParameterizedString<"name">;
/**
* {name}
*/
"cancelFollowRequestConfirm": ParameterizedString<"name">;
/**
* {name}
*/
"rejectFollowRequestConfirm": ParameterizedString<"name">;
/**
*
*/

View File

@ -692,15 +692,15 @@ smtpSecure: "Usare SSL/TLS implicito per le connessioni SMTP"
smtpSecureInfo: "Disabilitare quando è attivo STARTTLS."
testEmail: "Verifica il funzionamento"
wordMute: "Parole silenziate"
wordMuteDescription: "Contrae le Note con la parola o la frase specificata. Permette di espandere le Note, cliccandole."
wordMuteDescription: "Comprimi le Note che hanno la parola o la regola specificata. Cliccale per espanderle e leggerne comunque il contenuto."
hardWordMute: "Filtro per parole"
showMutedWord: "Elenca le parole silenziate"
hardWordMuteDescription: "Ignora le Note con la parola o la frase specificata. A differenza delle parole silenziate, la Nota non verrà federata."
hardWordMuteDescription: "Ignora le Note con la parola o la regola specificata. A differenza delle \"Parole Silenziate\", queste Note non ti verranno proprio recapitate."
regexpError: "errore regex"
regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:"
instanceMute: "Silenziare l'istanza"
userSaysSomething: "{name} ha detto qualcosa"
userSaysSomethingAbout: "{name} ha Notato a riguardo di \"{word}\""
userSaysSomethingAbout: "{name} ha anNotato qualcosa su \"{word}\""
makeActive: "Attiva"
display: "Visualizza"
copy: "Copia"

View File

@ -83,6 +83,8 @@ files: "ファイル"
download: "ダウンロード"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。"
unfollowConfirm: "{name}のフォローを解除しますか?"
cancelFollowRequestConfirm: "{name}へのフォロー申請をキャンセルしますか?"
rejectFollowRequestConfirm: "{name}からのフォロー申請を拒否しますか?"
exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"
importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。"
lists: "リスト"

View File

@ -83,6 +83,8 @@ files: "파일"
download: "다운로드"
driveFileDeleteConfirm: "{name} 파일을 삭제하시겠습니까? 이 파일을 사용하는 일부 콘텐츠도 삭제됩니다."
unfollowConfirm: "{name}님을 언팔로우하시겠습니까?"
cancelFollowRequestConfirm: "{name}(으)로의 팔로우 신청을 취소하시겠습니까?"
rejectFollowRequestConfirm: "{name}(으)로부터의 팔로우 신청을 거부하시겠습니까?"
exportRequested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 \"드라이브\"에 추가됩니다."
importRequested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다."
lists: "리스트"
@ -1556,6 +1558,8 @@ _preferencesProfile:
profileNameDescription: "이 디바이스를 식별할 이름을 설정해 주세요."
profileNameDescription2: "예: '메인PC', '스마트폰' 등"
manageProfiles: "프로파일 관리"
shareSameProfileBetweenDevicesIsNotRecommended: "여러 장치에서 동일한 프로필을 공유하는 것은 권장하지 않습니다."
useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "여러 장치에서 동기화하고 싶은 설정 항목이 있는 경우에는 개별로 '여러 장치에서 동기화' 옵션을 활성화해 주십시오."
_preferencesBackup:
autoBackup: "자동 백업"
restoreFromBackup: "백업으로 복구"

View File

@ -83,6 +83,8 @@ files: "文件"
download: "下载"
driveFileDeleteConfirm: "要删除「{name}」文件吗?附加此文件的帖子也会被删除。"
unfollowConfirm: "要取消对 {name} 的关注吗?"
cancelFollowRequestConfirm: "要取消申请关注{name}吗?"
rejectFollowRequestConfirm: "要拒绝{name}的关注申请吗?"
exportRequested: "导出请求已提交,这可能需要花一些时间,导出的文件将保存到网盘中。"
importRequested: "导入请求已提交,这可能需要花一点时间。"
lists: "列表"
@ -474,7 +476,7 @@ passwordLessLogin: "无密码登录"
passwordLessLoginDescription: "不使用密码,仅使用安全密钥或 Passkey 登录"
resetPassword: "重置密码"
newPasswordIs: "新的密码是「{password}」"
reduceUiAnimation: "减少UI动画"
reduceUiAnimation: "减少 UI 动画"
share: "分享"
notFound: "未找到"
notFoundDescription: "没有与指定 URL 对应的页面。"
@ -795,7 +797,7 @@ makeExplorable: "使账号可见。"
makeExplorableDescription: "关闭时,账号不会显示在\"发现\"中。"
duplicate: "复制"
left: "左"
center: ""
center: "中"
wide: "宽"
narrow: "窄"
reloadToApplySetting: "页面刷新后设置才会生效。是否现在刷新页面?"
@ -817,7 +819,7 @@ advanced: "高级"
advancedSettings: "高级设置"
value: "值"
createdAt: "创建日期"
updatedAt: "更新时间"
updatedAt: "更新日期"
saveConfirm: "确定保存?"
deleteConfirm: "确定删除?"
invalidValue: "无效值。"
@ -1023,6 +1025,9 @@ pushNotificationAlreadySubscribed: "推送通知消息已启用"
pushNotificationNotSupported: "浏览器或服务器不支持推送通知消息"
sendPushNotificationReadMessage: "删除已读推送通知消息"
sendPushNotificationReadMessageCaption: "您终端设备的电池消耗可能会增加。"
pleaseAllowPushNotification: "请在浏览器中启用推送通知"
browserPushNotificationDisabled: "未能获取发送通知的权限"
browserPushNotificationDisabledDescription: "{serverName}无权限发送通知。请在浏览器设置中允许通知后重新尝试。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "还原"
@ -1207,7 +1212,7 @@ renotes: "转发"
loadReplies: "查看回复"
loadConversation: "查看对话"
pinnedList: "已置顶的列表"
keepScreenOn: "保持设备屏幕开启"
keepScreenOn: "保持屏幕常亮"
verifiedLink: "已验证的链接"
notifyNotes: "打开发帖通知"
unnotifyNotes: "关闭发帖通知"
@ -1332,7 +1337,7 @@ accessibility: "辅助功能"
preferencesProfile: "设置的配置"
copyPreferenceId: "复制设置 ID"
resetToDefaultValue: "重置为默认值"
overrideByAccount: "用账户覆盖"
overrideByAccount: "覆盖账号"
untitled: "未命名"
noName: "没有名字"
skip: "跳过"
@ -1398,13 +1403,14 @@ widgets: "小工具"
deviceInfo: "设备信息"
deviceInfoDescription: "咨询技术问题时,将以下信息一并发送有助于解决问题。"
youAreAdmin: "你是管理员"
frame: "边框"
presets: "预设值"
zeroPadding: "填充 0"
_imageEditing:
_vars:
caption: "文件标题"
filename: "文件名称"
filename_without_ext: "无扩展文件名"
filename_without_ext: "不带扩展名的文件名"
year: "拍摄年"
month: "拍摄月"
day: "拍摄日"
@ -1414,26 +1420,31 @@ _imageEditing:
camera_model: "相机名称"
camera_lens_model: "镜头型号"
camera_mm: "焦距"
camera_mm_35: "焦距35mm等效"
camera_f: "光圈"
camera_s: "快门速度"
camera_iso: "ISO"
gps_lat: "纬度"
gps_long: "经度"
_imageFrameEditor:
title: "编辑边框"
tip: "您可以通过添加包含边框和元数据的标签来装饰图片。"
header: "顶栏"
footer: "底部"
footer: "页脚"
borderThickness: "边框宽度"
labelThickness: "标签宽度"
labelScale: "标签比例"
centered: "居中"
captionMain: "标题(大)"
captionSub: "标题(小)"
availableVariables: "可变量"
availableVariables: "可修改的变量"
withQrCode: "二维码"
backgroundColor: "背景色"
textColor: "文色"
backgroundColor: "背景色"
textColor: "文本颜色"
font: "字体"
fontSerif: "衬线字体"
fontSansSerif: "无衬线字体"
quitWithoutSaveConfirm: "不保存就退出吗"
quitWithoutSaveConfirm: "放弃未保存的更改"
failedToLoadImage: "图片加载失败"
_compression:
_quality:
@ -1452,36 +1463,36 @@ _chat:
noMessagesYet: "还没有消息"
newMessage: "新消息"
individualChat: "私聊"
individualChat_description: "可以与特定用户进行一对一聊天。"
individualChat_description: "与特定的用户单独聊天。"
roomChat: "群聊"
roomChat_description: "支持多人同时进行消息交流。\n即使部分用户未开放私信权限只要接受了邀请仍可进行聊天。"
createRoom: "创建群"
inviteUserToChat: "邀请用户来开始聊天"
yourRooms: "创建的群组"
joiningRooms: "已加入的群"
roomChat_description: "支持多人同时聊天。\n即使对方不允许私聊只要接受邀请也能加入。"
createRoom: "创建群"
inviteUserToChat: "邀请用户来聊天"
yourRooms: "已创建的群聊"
joiningRooms: "已加入的群"
invitations: "邀请"
noInvitations: "没有邀请"
history: "历史"
noHistory: "没有历史记录"
noRooms: "没有群"
noRooms: "没有群"
inviteUser: "邀请用户"
sentInvitations: "已发送的邀请"
join: "加入"
ignore: "忽略"
leave: "退出房间"
leave: "退出群聊"
members: "成员"
searchMessages: "搜索消息"
home: "首页"
send: "发送"
newline: "换行"
muteThisRoom: "屏蔽该群"
deleteRoom: "删除群"
muteThisRoom: "屏蔽该群"
deleteRoom: "删除群"
chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。"
chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。"
chatNotAvailableInOtherAccount: "对方的账户当前无法使用私信。"
cannotChatWithTheUser: "无法私信该用户"
cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。"
youAreNotAMemberOfThisRoomButInvited: "您还未加入此房间,但已收到邀请。如要加入,请接受邀请。"
youAreNotAMemberOfThisRoomButInvited: "您尚未加入此群组,但已收到加入邀请。请接受邀请加入。"
doYouAcceptInvitation: "要接受邀请吗?"
chatWithThisUser: "私信"
thisUserAllowsChatOnlyFromFollowers: "此用户仅接受关注者发起的聊天。"
@ -1558,6 +1569,7 @@ _preferencesBackup:
youNeedToNameYourProfileToEnableAutoBackup: "需指定配置名以开启自动备份。"
autoPreferencesBackupIsNotEnabledForThisDevice: "此设备未开启自动备份"
backupFound: "已找到备份"
forceBackup: "强制备份设置"
_accountSettings:
requireSigninToViewContents: "需要登录才能显示内容"
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
@ -2219,7 +2231,7 @@ _instanceTicker:
_serverDisconnectedBehavior:
reload: "自动重载"
dialog: "对话框警告"
quiet: "静警告"
quiet: "警告"
_channel:
create: "创建频道"
edit: "编辑频道"
@ -2538,7 +2550,7 @@ _poll:
noOnlyOneChoice: "需要至少两个选项"
choiceN: "选项{n}"
noMore: "无法再添加更多了"
canMultipleVote: "允许选择"
canMultipleVote: "允许多选"
expiration: "截止时间"
infinite: "永久"
at: "指定日期"
@ -2547,13 +2559,13 @@ _poll:
deadlineTime: "时间"
duration: "期限"
votesCount: "{n}票"
totalVotes: "总票数 {n}"
totalVotes: "总计{n}票"
vote: "投票"
showResult: "显示结果"
showResult: "查看结果"
voted: "已投票"
closed: "已截止"
remainingDays: "{d}天{h}小时后截止"
remainingHours: "{h} 小时 {m} 分后截止"
remainingHours: "{h}小时{m}分后截止"
remainingMinutes: "{m}分{s}秒后截止"
remainingSeconds: "{s}秒后截止"
_visibility:
@ -2737,7 +2749,7 @@ _notification:
newNote: "新的帖子"
unreadAntennaNote: "天线 {name}"
roleAssigned: "授予的角色"
chatRoomInvitationReceived: "受邀加入聊天室"
chatRoomInvitationReceived: "您已被邀请加入群聊"
emptyPushNotificationMessage: "推送通知已更新"
achievementEarned: "获得成就"
testNotification: "测试通知"
@ -2768,7 +2780,7 @@ _notification:
receiveFollowRequest: "收到关注请求"
followRequestAccepted: "关注请求已通过"
roleAssigned: "授予的角色"
chatRoomInvitationReceived: "受邀加入聊天室"
chatRoomInvitationReceived: "您已被邀请加入群聊"
achievementEarned: "取得的成就"
exportCompleted: "已完成导出"
login: "登录"
@ -2864,6 +2876,8 @@ _abuseReport:
notifiedWebhook: "使用的 webhook"
deleteConfirm: "要删除通知吗?"
_moderationLogTypes:
clearQueue: "清除队列"
promoteQueue: "重新执行队列中的任务"
createRole: "创建角色"
deleteRole: "删除角色"
updateRole: "更新角色"
@ -2908,11 +2922,11 @@ _moderationLogTypes:
createAbuseReportNotificationRecipient: "新建了举报通知"
updateAbuseReportNotificationRecipient: "更新了举报通知"
deleteAbuseReportNotificationRecipient: "删除了举报通知"
deleteAccount: "删除了账户"
deletePage: "删除页面"
deleteFlash: "删除 Play"
deleteAccount: "删除户"
deletePage: "删除页面"
deleteFlash: "删除 Play"
deleteGalleryPost: "删除图集内容"
deleteChatRoom: "删除天室"
deleteChatRoom: "删除聊"
updateProxyAccountDescription: "更新代理账户的简介"
_fileViewer:
title: "文件信息"
@ -3185,7 +3199,7 @@ _search:
serverHostPlaceholder: "如misskey.example.com"
_serverSetupWizard:
installCompleted: "Misskey 安装完成!"
firstCreateAccount: "首先来创建管理员账号吧。"
firstCreateAccount: "首先,创建一个管理员帐户。"
accountCreated: "管理员账号已创建!"
serverSetting: "服务器设置"
youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "用此向导来轻松地以最佳方式配置服务器。"
@ -3195,7 +3209,7 @@ _serverSetupWizard:
single: "单用户服务器"
single_description: "仅供自己使用的单人服务器"
single_youCanCreateMultipleAccounts: "使用单用户服务器模式使用时,也可以根据需要创建多个账号。"
group: "小圈子服务器"
group: "群组服务器"
group_description: "邀请其他可信用户一起使用的多人服务器"
open: "开放服务器"
open_description: "以容纳不限定数量的用户的模式运行"
@ -3232,7 +3246,7 @@ _uploader:
compressedToX: "压缩 {x}"
savedXPercent: "节省了 {x}% 的空间"
abortConfirm: "还有未上传的文件,要中止吗?"
doneConfirm: "还有未上传的文件,要完成吗"
doneConfirm: "部分文件尚未上传,是否继续"
maxFileSizeIsX: "可上传最大 {x} 的文件。"
allowedTypes: "可上传的文件类型"
tip: "文件还没有被上传。可在此对话框中进行上传前确认、重命名、压缩、裁剪等操作。准备完成后,点击「上传」即可开始上传。"
@ -3252,7 +3266,7 @@ watermark: "水印"
defaultPreset: "默认预设"
_watermarkEditor:
tip: "可在图像内增加包含作者等信息的水印。"
quitWithoutSaveConfirm: "不保存就退出吗"
quitWithoutSaveConfirm: "放弃未保存的更改"
driveFileTypeWarn: "不支持此文件"
driveFileTypeWarnDescription: "请选择图像文件"
title: "编辑水印"

View File

@ -83,6 +83,8 @@ files: "檔案"
download: "下載"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此檔案的貼文也會跟著被刪除。"
unfollowConfirm: "確定要取消追隨{name}嗎?"
cancelFollowRequestConfirm: "要取消向 {name} 送出的追隨申請嗎?"
rejectFollowRequestConfirm: "要拒絕來自 {name} 的追隨申請嗎?"
exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端硬碟裡。"
importRequested: "已請求匯入。這可能會花一點時間。"
lists: "清單"
@ -172,7 +174,7 @@ emojiUrl: "表情符號 URL"
addEmoji: "新增表情符號"
settingGuide: "推薦設定"
cacheRemoteFiles: "快取遠端檔案"
cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但建議將 default.yml 的 proxyRemoteFiles 設為 true以便產生圖片的縮圖並保護使用者的隱私。"
cacheRemoteFilesDescription: "啟用這個設定後,遠端檔案會被快取到這台伺服器的儲存空間中。這樣能加快圖片的顯示速度,但會多占用伺服器的儲存容量。遠端使用者能保留多少快取,取決於其角色所設定的硬碟容量上限。若超過這個上限,系統會從最舊的檔案開始刪除快取並改成連結。若停用這個設定,遠端檔案一開始就只會以連結的形式保留。"
youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全部刪除。"
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"

View File

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2025.11.0",
"version": "2025.11.1",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.20.0",
"packageManager": "pnpm@10.22.0",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@ -26,6 +26,7 @@
"build-storybook": "pnpm --filter frontend build-storybook",
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
"start:inspect": "cd packages/backend && node --inspect ./built/boot/entry.js",
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
"cli": "cd packages/backend && pnpm cli",
"init": "pnpm migrate",
@ -54,30 +55,30 @@
},
"dependencies": {
"cssnano": "7.1.2",
"esbuild": "0.25.11",
"esbuild": "0.27.0",
"execa": "9.6.0",
"fast-glob": "3.3.3",
"glob": "11.0.3",
"glob": "13.0.0",
"ignore-walk": "8.0.0",
"js-yaml": "4.1.0",
"js-yaml": "4.1.1",
"postcss": "8.5.6",
"tar": "7.5.2",
"terser": "5.44.0",
"terser": "5.44.1",
"typescript": "5.9.3"
},
"devDependencies": {
"@eslint/js": "9.39.0",
"@misskey-dev/eslint-plugin": "2.1.0",
"@eslint/js": "9.39.1",
"@misskey-dev/eslint-plugin": "2.2.0",
"@types/js-yaml": "4.0.9",
"@types/node": "24.9.2",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"@types/node": "24.10.1",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"cross-env": "10.1.0",
"cypress": "15.5.0",
"eslint": "9.39.0",
"cypress": "15.6.0",
"eslint": "9.39.1",
"globals": "16.5.0",
"ncp": "2.0.0",
"pnpm": "10.20.0",
"pnpm": "10.22.0",
"start-server-and-test": "2.1.2"
},
"optionalDependencies": {

View File

@ -8,6 +8,7 @@
},
"scripts": {
"start": "node ./built/boot/entry.js",
"start:inspect": "node --inspect ./built/boot/entry.js",
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
@ -39,17 +40,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.14.0",
"@swc/core-darwin-x64": "1.14.0",
"@swc/core-darwin-arm64": "1.15.2",
"@swc/core-darwin-x64": "1.15.2",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.14.0",
"@swc/core-linux-arm64-gnu": "1.14.0",
"@swc/core-linux-arm64-musl": "1.14.0",
"@swc/core-linux-x64-gnu": "1.14.0",
"@swc/core-linux-x64-musl": "1.14.0",
"@swc/core-win32-arm64-msvc": "1.14.0",
"@swc/core-win32-ia32-msvc": "1.14.0",
"@swc/core-win32-x64-msvc": "1.14.0",
"@swc/core-linux-arm-gnueabihf": "1.15.2",
"@swc/core-linux-arm64-gnu": "1.15.2",
"@swc/core-linux-arm64-musl": "1.15.2",
"@swc/core-linux-x64-gnu": "1.15.2",
"@swc/core-linux-x64-musl": "1.15.2",
"@swc/core-win32-arm64-msvc": "1.15.2",
"@swc/core-win32-ia32-msvc": "1.15.2",
"@swc/core-win32-x64-msvc": "1.15.2",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
@ -69,8 +70,8 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@aws-sdk/client-s3": "3.922.0",
"@aws-sdk/lib-storage": "3.922.0",
"@aws-sdk/client-s3": "3.936.0",
"@aws-sdk/lib-storage": "3.936.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.3",
"@fastify/cookie": "11.0.2",
@ -82,18 +83,18 @@
"@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.5",
"@napi-rs/canvas": "0.1.81",
"@nestjs/common": "11.1.8",
"@nestjs/core": "11.1.8",
"@nestjs/testing": "11.1.8",
"@napi-rs/canvas": "0.1.82",
"@nestjs/common": "11.1.9",
"@nestjs/core": "11.1.9",
"@nestjs/testing": "11.1.9",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "10.22.0",
"@sentry/profiling-node": "10.22.0",
"@sentry/node": "10.26.0",
"@sentry/profiling-node": "10.26.0",
"@simplewebauthn/server": "12.0.0",
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.8",
"@swc/core": "1.14.0",
"@swc/cli": "0.7.9",
"@swc/core": "1.15.2",
"@twemoji/parser": "16.0.0",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
@ -103,24 +104,23 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.63.0",
"bullmq": "5.63.2",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
"chokidar": "4.0.3",
"cli-highlight": "2.1.11",
"color-convert": "2.0.1",
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "5.6.1",
"fastify": "5.6.2",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "21.0.0",
"file-type": "21.1.1",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.4",
"got": "14.6.1",
"form-data": "4.0.5",
"got": "14.6.4",
"happy-dom": "20.0.10",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
@ -129,7 +129,7 @@
"ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0",
"is-svg": "5.1.0",
"js-yaml": "4.1.0",
"js-yaml": "4.1.1",
"jsdom": "26.1.0",
"json5": "2.2.3",
"jsonld": "8.3.3",
@ -161,7 +161,7 @@
"qrcode": "1.5.4",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.22.1",
"re2": "1.22.3",
"redis-info": "3.1.0",
"redis-lock": "0.1.4",
"reflect-metadata": "0.2.2",
@ -170,8 +170,8 @@
"rxjs": "7.8.2",
"sanitize-html": "2.17.0",
"secure-json-parse": "3.0.2",
"sharp": "0.33.5",
"semver": "7.7.3",
"sharp": "0.33.5",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
@ -191,7 +191,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.20",
"@sentry/vue": "10.22.0",
"@sentry/vue": "10.26.0",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.39",
"@types/accepts": "1.3.7",
@ -210,7 +210,7 @@
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "24.9.2",
"@types/node": "24.10.1",
"@types/nodemailer": "6.4.21",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
@ -231,8 +231,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0",
@ -240,7 +240,7 @@
"fkill": "9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.1.10",
"nodemon": "3.1.11",
"pid-port": "1.0.2",
"simple-oauth2": "5.1.0",
"supertest": "7.1.4"

View File

@ -41,7 +41,7 @@ function greet() {
//#endregion
console.log(' Misskey is an open-source decentralized microblogging platform.');
console.log(chalk.rgb(255, 136, 0)(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
console.log(chalk.rgb(255, 136, 0)(' If you like Misskey, please consider donating to support dev. https://misskey-hub.net/docs/donate/'));
console.log('');
console.log(chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`);

View File

@ -7,11 +7,11 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Injectable } from '@nestjs/common';
import * as nsfw from 'nsfwjs';
import si from 'systeminformation';
import { Mutex } from 'async-mutex';
import fetch from 'node-fetch';
import { bindThis } from '@/decorators.js';
import type { NSFWJS, PredictionType } from 'nsfwjs';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@ -21,7 +21,7 @@ let isSupportedCpu: undefined | boolean = undefined;
@Injectable()
export class AiService {
private model: nsfw.NSFWJS;
private model: NSFWJS;
private modelLoadMutex: Mutex = new Mutex();
constructor(
@ -29,7 +29,7 @@ export class AiService {
}
@bindThis
public async detectSensitive(source: string | Buffer): Promise<nsfw.PredictionType[] | null> {
public async detectSensitive(source: string | Buffer): Promise<PredictionType[] | null> {
try {
if (isSupportedCpu === undefined) {
isSupportedCpu = await this.computeIsSupportedCpu();
@ -44,6 +44,7 @@ export class AiService {
tf.env().global.fetch = fetch;
if (this.model == null) {
const nsfw = await import('nsfwjs');
await this.modelLoadMutex.runExclusive(async () => {
if (this.model == null) {
this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });

View File

@ -5,18 +5,9 @@
import {
FindOneOptions,
InsertQueryBuilder,
ObjectLiteral,
QueryRunner,
Repository,
SelectQueryBuilder,
} from 'typeorm';
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
import {
RawSqlResultsToEntityTransformer,
} from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAccessToken } from '@/models/AccessToken.js';
@ -96,66 +87,12 @@ import { MiWebhook } from '@/models/Webhook.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
export interface MiRepository<T extends ObjectLiteral> {
createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
insertOneImpl(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>, queryRunner?: QueryRunner): Promise<T>;
selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
}
export const miRepository = {
createTableColumnNames() {
return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
},
async insertOne(entity, findOptions?) {
const opt = this.manager.connection.options as PostgresConnectionOptions;
if (opt.replication) {
const queryRunner = this.manager.connection.createQueryRunner('master');
try {
return this.insertOneImpl(entity, findOptions, queryRunner);
} finally {
await queryRunner.release();
}
} else {
return this.insertOneImpl(entity, findOptions);
}
},
async insertOneImpl(entity, findOptions?, queryRunner?) {
// ---- insert + returningの結果を共通テーブル式(CTE)に保持するクエリを生成 ----
const queryBuilder = this.createQueryBuilder().insert().values(entity);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const mainAlias = queryBuilder.expressionMap.mainAlias!;
const name = mainAlias.name;
mainAlias.name = 't';
const columnNames = this.createTableColumnNames();
queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
// ---- 共通テーブル式(CTE)から結果を取得 ----
const builder = this.createQueryBuilder(undefined, queryRunner).addCommonTableExpression(queryBuilder, 'cte', { columnNames });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
builder.expressionMap.mainAlias!.tablePath = 'cte';
this.selectAliasColumnNames(queryBuilder, builder);
if (findOptions) {
builder.setFindOptions(findOptions);
}
const raw = await builder.execute();
mainAlias.name = name;
const relationId = await new RelationIdLoader(builder.connection, this.queryRunner, builder.expressionMap.relationIdAttributes).load(raw);
const relationCount = await new RelationCountLoader(builder.connection, this.queryRunner, builder.expressionMap.relationCountAttributes).load(raw);
const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, this.queryRunner).transform(raw, mainAlias);
return result[0];
},
selectAliasColumnNames(queryBuilder, builder) {
let selectOrAddSelect = (selection: string, selectionAliasName?: string) => {
selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
return builder.select(selection, selectionAliasName);
};
for (const columnName of this.createTableColumnNames()) {
selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
}
return await this.insert(entity).then(x => this.findOneOrFail({ where: x.identifiers[0], ...findOptions }));
},
} satisfies MiRepository<ObjectLiteral>;

View File

@ -6,7 +6,6 @@
// https://github.com/typeorm/typeorm/issues/2400
import pg from 'pg';
import { DataSource, Logger, type QueryRunner } from 'typeorm';
import * as highlight from 'cli-highlight';
import { entities as charts } from '@/core/chart/entities.js';
import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
@ -25,7 +24,7 @@ import { MiAuthSession } from '@/models/AuthSession.js';
import { MiBlocking } from '@/models/Blocking.js';
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
import { MiChannelMuting } from "@/models/ChannelMuting.js";
import { MiChannelMuting } from '@/models/ChannelMuting.js';
import { MiClip } from '@/models/Clip.js';
import { MiClipNote } from '@/models/ClipNote.js';
import { MiClipFavorite } from '@/models/ClipFavorite.js';
@ -101,12 +100,6 @@ export type LoggerProps = {
printReplicationMode?: boolean,
};
function highlightSql(sql: string) {
return highlight.highlight(sql, {
language: 'sql', ignoreIllegals: true,
});
}
function truncateSql(sql: string) {
return sql.length > 100 ? `${sql.substring(0, 100)}...` : sql;
}
@ -132,7 +125,7 @@ class MyCustomLogger implements Logger {
modded = truncateSql(modded);
}
return highlightSql(modded);
return modded;
}
@bindThis

View File

@ -295,8 +295,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope;
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
// TODO: ちゃんと数える
const length = JSON.stringify(mutedWords).length;
const count = (arr: (string[] | string)[]) => {
let length = 0;
for (const item of arr) {
if (typeof item === 'string') {
length += item.length;
} else if (Array.isArray(item)) {
for (const subItem of item) {
length += subItem.length;
}
}
}
return length;
};
const length = count(mutedWords);
if (length > limit) {
throw new ApiError(meta.errors.tooManyMutedWords);
}

View File

@ -135,6 +135,18 @@ export const meta = {
code: 'CANNOT_RENOTE_TO_EXTERNAL',
id: 'ed1952ac-2d26-4957-8b30-2deda76bedf7',
},
scheduledAtRequired: {
message: 'scheduledAt is required when isActuallyScheduled is true.',
code: 'SCHEDULED_AT_REQUIRED',
id: '15e28a55-e74c-4d65-89b7-8880cdaaa87d',
},
scheduledAtMustBeInFuture: {
message: 'scheduledAt must be in the future.',
code: 'SCHEDULED_AT_MUST_BE_IN_FUTURE',
id: 'e4bed6c9-017e-4934-aed0-01c22cc60ec1',
},
},
limit: {
@ -252,6 +264,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
case 'c3275f19-4558-4c59-83e1-4f684b5fab66':
throw new ApiError(meta.errors.tooManyScheduledNotes);
case '94a89a43-3591-400a-9c17-dd166e71fdfa':
throw new ApiError(meta.errors.scheduledAtRequired);
case 'b34d0c1b-996f-4e34-a428-c636d98df457':
throw new ApiError(meta.errors.scheduledAtMustBeInFuture);
default:
throw err;
}

View File

@ -165,6 +165,18 @@ export const meta = {
code: 'TOO_MANY_SCHEDULED_NOTES',
id: '02f5df79-08ae-4a33-8524-f1503c8f6212',
},
scheduledAtRequired: {
message: 'scheduledAt is required when isActuallyScheduled is true.',
code: 'SCHEDULED_AT_REQUIRED',
id: 'fe9737d5-cc41-498c-af9d-149207307530',
},
scheduledAtMustBeInFuture: {
message: 'scheduledAt must be in the future.',
code: 'SCHEDULED_AT_MUST_BE_IN_FUTURE',
id: 'ed1a6673-d0d1-4364-aaae-9bf3f139cbc5',
},
},
limit: {
@ -295,6 +307,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsTooManyMentions);
case 'bacdf856-5c51-4159-b88a-804fa5103be5':
throw new ApiError(meta.errors.tooManyScheduledNotes);
case '94a89a43-3591-400a-9c17-dd166e71fdfa':
throw new ApiError(meta.errors.scheduledAtRequired);
case 'b34d0c1b-996f-4e34-a428-c636d98df457':
throw new ApiError(meta.errors.scheduledAtMustBeInFuture);
default:
throw err;
}

View File

@ -95,7 +95,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
const params = new URLSearchParams();
params.append('auth_key', this.serverSettings.deeplAuthKey);
params.append('text', note.text);
params.append('target_lang', targetLang);
@ -104,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const res = await this.httpRequestService.send(endpoint, {
method: 'POST',
headers: {
'Authorization': `DeepL-Auth-Key ${this.serverSettings.deeplAuthKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json, */*',
},

View File

@ -41,6 +41,10 @@ class ChannelChannel extends Channel {
private async onNote(note: Packed<'Note'>) {
if (note.channelId !== this.channelId) return;
if (note.user.requireSigninToViewContents && this.user == null) return;
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {

View File

@ -50,7 +50,7 @@ import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntitySer
import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js';
import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@ -918,7 +918,7 @@ export class ClientServerService {
return await renderBase(reply);
});
fastify.setErrorHandler(async (error, request, reply) => {
fastify.setErrorHandler<FastifyError>(async (error, request, reply) => {
const errId = randomUUID();
this.clientLoggerService.logger.error(`Internal error occurred in ${request.routeOptions.url}: ${error.message}`, {
path: request.routeOptions.url,

View File

@ -55,7 +55,7 @@
//#region Script
async function importAppScript() {
await import(CLIENT_ENTRY ? `/embed_vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/embed_vite/src/_boot_.ts')
await import(CLIENT_ENTRY ? `/embed_vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/embed_vite/src/boot.ts')
.catch(async e => {
console.error(e);
renderError('APP_IMPORT');

View File

@ -28,7 +28,7 @@ html
meta(name='theme-color-orig' content= themeColor || '#86b300')
meta(property='og:site_name' content= instanceName || 'Misskey')
meta(property='instance_url' content= instanceUrl)
meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover')
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')

View File

@ -95,7 +95,7 @@ services:
retries: 20
db:
image: postgres:15-alpine
image: postgres:18-alpine
env_file:
- ./.config/docker.env
volumes:

View File

@ -5,7 +5,7 @@ services:
- "127.0.0.1:56312:6379"
dbtest:
image: postgres:15
image: postgres:18
ports:
- "127.0.0.1:54312:5432"
environment:

View File

@ -11,15 +11,15 @@
},
"devDependencies": {
"@types/estree": "1.0.8",
"@types/node": "24.9.2",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"rollup": "4.52.5",
"@types/node": "24.10.1",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"rollup": "4.53.3",
"typescript": "5.9.3"
},
"dependencies": {
"estree-walker": "3.0.3",
"magic-string": "0.30.21",
"vite": "7.1.11"
"vite": "7.2.2"
}
}

View File

@ -15,8 +15,8 @@
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.22",
"@vitejs/plugin-vue": "6.0.2",
"@vue/compiler-sfc": "3.5.24",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@ -26,16 +26,16 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.52.5",
"sass": "1.93.3",
"shiki": "3.14.0",
"rollup": "4.53.3",
"sass": "1.94.1",
"shiki": "3.15.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.3",
"uuid": "13.0.0",
"vite": "7.1.11",
"vue": "3.5.22"
"vite": "7.2.2",
"vue": "3.5.24"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
@ -43,14 +43,14 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.10",
"@types/node": "24.9.2",
"@types/node": "24.10.1",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.22",
"@vue/runtime-core": "3.5.24",
"acorn": "8.15.0",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
@ -59,14 +59,14 @@
"happy-dom": "20.0.10",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.11.6",
"nodemon": "3.1.10",
"msw": "2.12.2",
"nodemon": "3.1.11",
"prettier": "3.6.2",
"start-server-and-test": "2.1.2",
"tsx": "4.20.6",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.1.2",
"vue-component-type-helpers": "3.1.4",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.2"
"vue-tsc": "3.1.4"
}
}

View File

@ -21,12 +21,12 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.9.2",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"esbuild": "0.25.11",
"@types/node": "24.10.1",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"esbuild": "0.27.0",
"eslint-plugin-vue": "10.5.1",
"nodemon": "3.1.10",
"nodemon": "3.1.11",
"typescript": "5.9.3",
"vue-eslint-parser": "10.2.0"
},
@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.5.22"
"vue": "3.5.24"
}
}

View File

@ -24,12 +24,12 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.22.0",
"@syuilo/aiscript": "1.1.2",
"@sentry/vue": "10.26.0",
"@syuilo/aiscript": "1.2.0",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.22",
"@vitejs/plugin-vue": "6.0.2",
"@vue/compiler-sfc": "3.5.24",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.19",
"astring": "1.9.0",
@ -41,7 +41,7 @@
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "13.3.3",
"chromatic": "13.3.4",
"compare-versions": "6.1.1",
"cropperjs": "2.1.0",
"date-fns": "4.1.0",
@ -58,7 +58,7 @@
"json5": "2.2.3",
"magic-string": "0.30.21",
"matter-js": "0.20.0",
"mediabunny": "1.24.2",
"mediabunny": "1.25.0",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@ -67,21 +67,21 @@
"punycode.js": "2.3.1",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"rollup": "4.52.5",
"rollup": "4.53.3",
"sanitize-html": "2.17.0",
"sass": "1.93.3",
"shiki": "3.14.0",
"sass": "1.94.1",
"shiki": "3.15.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.181.0",
"three": "0.181.2",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.3",
"v-code-diff": "1.13.1",
"vite": "7.1.11",
"vue": "3.5.22",
"vite": "7.2.2",
"vue": "3.5.24",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
@ -110,21 +110,21 @@
"@types/estree": "1.0.8",
"@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.10",
"@types/node": "24.9.2",
"@types/node": "24.10.1",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.22",
"@vue/runtime-core": "3.5.22",
"@vue/compiler-core": "3.5.24",
"@vue/runtime-core": "3.5.24",
"acorn": "8.15.0",
"cross-env": "10.1.0",
"cypress": "15.5.0",
"cypress": "15.6.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.5.1",
"fast-glob": "3.3.3",
@ -132,9 +132,9 @@
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.1.1",
"msw": "2.11.6",
"msw": "2.12.2",
"msw-storybook-addon": "2.0.6",
"nodemon": "3.1.10",
"nodemon": "3.1.11",
"prettier": "3.6.2",
"react": "19.2.0",
"react-dom": "19.2.0",
@ -147,8 +147,8 @@
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.1.2",
"vue-component-type-helpers": "3.1.4",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.2"
"vue-tsc": "3.1.4"
}
}

View File

@ -40,29 +40,77 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
LOCALE: values.STR(lang),
SERVER_URL: values.STR(url),
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
utils.assertString(title);
utils.assertString(text);
if (type != null) {
assertStringAndIsIn(type, DIALOG_TYPES);
'Mk:dialog': values.FN_NATIVE(async ([_title, _text, _type]) => {
let title: string | undefined = undefined;
let text: string | undefined = undefined;
let type: typeof DIALOG_TYPES[number] = 'info';
if (_title != null) {
if (utils.isString(_title)) {
title = _title.value;
} else {
utils.assertNull(_title);
}
}
if (_text != null) {
if (utils.isString(_text)) {
text = _text.value;
} else {
utils.assertNull(_text);
}
}
if (_type != null) {
if (utils.isString(_type)) {
assertStringAndIsIn(_type, DIALOG_TYPES);
type = _type.value;
} else {
utils.assertNull(_type);
}
}
await os.alert({
type: type ? type.value : 'info',
title: title.value,
text: text.value,
type,
title,
text,
});
return values.NULL;
}),
'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
utils.assertString(title);
utils.assertString(text);
if (type != null) {
assertStringAndIsIn(type, DIALOG_TYPES);
'Mk:confirm': values.FN_NATIVE(async ([_title, _text, _type]) => {
let title: string | undefined = undefined;
let text: string | undefined = undefined;
let type: typeof DIALOG_TYPES[number] = 'question';
if (_title != null) {
if (utils.isString(_title)) {
title = _title.value;
} else {
utils.assertNull(_title);
}
}
if (_text != null) {
if (utils.isString(_text)) {
text = _text.value;
} else {
utils.assertNull(_text);
}
}
if (_type != null) {
if (utils.isString(_type)) {
assertStringAndIsIn(_type, DIALOG_TYPES);
type = _type.value;
} else {
utils.assertNull(_type);
}
}
const confirm = await os.confirm({
type: type ? type.value : 'question',
title: title.value,
text: text.value,
type,
title,
text,
});
return confirm.canceled ? values.FALSE : values.TRUE;
}),
@ -76,15 +124,23 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
if (ep.value.includes('://') || ep.value.includes('..')) {
throw new errors.AiScriptRuntimeError('invalid endpoint');
}
if (token) {
let actualToken: string | null = null;
if (token != null && !utils.isNull(token)) {
utils.assertString(token);
// バグがあればundefinedもあり得るため念のため
if (typeof token.value !== 'string') throw new Error('invalid token');
if (typeof token.value !== 'string') throw new errors.AiScriptRuntimeError('invalid token');
actualToken = token.value;
}
const actualToken: string | null = token?.value ?? opts.token ?? null;
if (actualToken == null) {
actualToken = opts.token ?? null;
}
if (param == null) {
throw new errors.AiScriptRuntimeError('expected param');
}
utils.assertObject(param);
return misskeyApi(ep.value as keyof Misskey.Endpoints, utils.valToJs(param) as object, actualToken).then(res => {
return utils.jsToVal(res);

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed, watch, version as vueVersion } from 'vue';
import { watch, version as vueVersion } from 'vue';
import { compareVersions } from 'compare-versions';
import { version, lang, apiUrl, isSafeMode } from '@@/js/config.js';
import defaultLightTheme from '@@/themes/l-light.json5';
@ -15,11 +15,11 @@ import directives from '@/directives/index.js';
import components from '@/components/index.js';
import { applyTheme } from '@/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { updateI18n, i18n } from '@/i18n.js';
import { i18n } from '@/i18n.js';
import { refreshCurrentAccount, login } from '@/accounts.js';
import { store } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js';
import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js';
import { updateDeviceKind } from '@/utility/device-kind.js';
import { reloadChannel } from '@/utility/unison-reload.js';
import { getUrlWithoutLoginId } from '@/utility/login-id.js';
import { getAccountFromId } from '@/utility/get-account-from-id.js';
@ -109,13 +109,6 @@ export async function common(createVue: () => Promise<App<Element>>) {
else window.location.reload();
});
// If mobile, insert the viewport meta tag
if (['smartphone', 'tablet'].includes(deviceKind)) {
const viewport = window.document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
}
//#region Set lang attr
const html = window.document.documentElement;
html.setAttribute('lang', lang);
@ -160,8 +153,15 @@ export async function common(createVue: () => Promise<App<Element>>) {
});
//#endregion
if (!isSafeMode) {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
}
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重applyTheme発火を防ぐため)
// see: https://github.com/misskey-dev/misskey/issues/16562
watch(store.r.darkMode, (darkMode) => {
const theme = (() => {
@ -178,26 +178,17 @@ export async function common(createVue: () => Promise<App<Element>>) {
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
if (!isSafeMode) {
const darkTheme = prefer.model('darkTheme');
const lightTheme = prefer.model('lightTheme');
watch(darkTheme, (theme) => {
watch(prefer.r.darkTheme, (theme) => {
if (store.s.darkMode) {
applyTheme(theme ?? defaultDarkTheme);
}
});
watch(lightTheme, (theme) => {
watch(prefer.r.lightTheme, (theme) => {
if (!store.s.darkMode) {
applyTheme(theme ?? defaultLightTheme);
}
});
fetchInstanceMetaPromise.then(() => {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
});
}
watch(prefer.r.overridedDeviceKind, (kind) => {

View File

@ -102,6 +102,21 @@ async function onClick() {
await misskeyApi('following/delete', {
userId: props.user.id,
});
} else if (hasPendingFollowRequestFromYou.value) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.cancelFollowRequestConfirm({ name: props.user.name || props.user.username }),
});
if (canceled) {
wait.value = false;
return;
}
await misskeyApi('following/requests/cancel', {
userId: props.user.id,
});
hasPendingFollowRequestFromYou.value = false;
} else {
if (prefer.s.alwaysConfirmFollow) {
const { canceled } = await os.confirm({
@ -115,41 +130,34 @@ async function onClick() {
}
}
if (hasPendingFollowRequestFromYou.value) {
await misskeyApi('following/requests/cancel', {
userId: props.user.id,
});
hasPendingFollowRequestFromYou.value = false;
} else {
await misskeyApi('following/create', {
userId: props.user.id,
withReplies: prefer.s.defaultFollowWithReplies,
});
emit('update:user', {
...props.user,
withReplies: prefer.s.defaultFollowWithReplies,
});
hasPendingFollowRequestFromYou.value = true;
await misskeyApi('following/create', {
userId: props.user.id,
withReplies: prefer.s.defaultFollowWithReplies,
});
emit('update:user', {
...props.user,
withReplies: prefer.s.defaultFollowWithReplies,
});
hasPendingFollowRequestFromYou.value = true;
if ($i == null) {
wait.value = false;
return;
}
if ($i == null) {
wait.value = false;
return;
}
claimAchievement('following1');
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');
}
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

@ -608,11 +608,30 @@ async function toggleReactionAcceptance() {
//#region popup
function showOtherSettings() {
let reactionAcceptanceIcon = 'ti ti-icons';
let reactionAcceptanceCaption = '';
if (reactionAcceptance.value === 'likeOnly') {
reactionAcceptanceIcon = 'ti ti-heart _love';
} else if (reactionAcceptance.value === 'likeOnlyForRemote') {
reactionAcceptanceIcon = 'ti ti-heart-plus';
switch (reactionAcceptance.value) {
case 'likeOnly':
reactionAcceptanceIcon = 'ti ti-heart _love';
reactionAcceptanceCaption = i18n.ts.likeOnly;
break;
case 'likeOnlyForRemote':
reactionAcceptanceIcon = 'ti ti-heart-plus';
reactionAcceptanceCaption = i18n.ts.likeOnlyForRemote;
break;
case 'nonSensitiveOnly':
reactionAcceptanceCaption = i18n.ts.nonSensitiveOnly;
break;
case 'nonSensitiveOnlyForLocalLikeOnlyForRemote':
reactionAcceptanceCaption = i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote;
break;
default:
reactionAcceptanceCaption = i18n.ts.all;
break;
}
const menuItems = [{
@ -624,6 +643,7 @@ function showOtherSettings() {
}, { type: 'divider' }, {
icon: reactionAcceptanceIcon,
text: i18n.ts.reactionAcceptance,
caption: reactionAcceptanceCaption,
action: () => {
toggleReactionAcceptance();
},
@ -692,6 +712,7 @@ function removeVisibleUser(user) {
function clear() {
text.value = '';
cw.value = null;
files.value = [];
poll.value = null;
quoteId.value = null;

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<script lang="ts">
import { defineComponent, h, ref, watch } from 'vue';
import { Comment, defineComponent, h, ref, watch } from 'vue';
import MkRadio from './MkRadio.vue';
import type { VNode } from 'vue';
@ -35,7 +35,7 @@ export default defineComponent({
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
// vnodev-if=false(trueoptiontype)
options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if'));
options = options.filter(vnode => vnode.type !== Comment);
return () => h('div', {
class: [

View File

@ -110,7 +110,11 @@ onUnmounted(() => {
<style lang="scss" module>
.root {
position: absolute;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.bg {

View File

@ -5,31 +5,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<svg v-if="type === 'info'" :class="[$style.icon, $style.info]" viewBox="0 0 160 160">
<path d="M80,108L80,72" style="--l:37;" :class="[$style.line, $style.animLine]"/>
<path d="M80,108L80,72" pathLength="1" :class="[$style.line, $style.animLine]"/>
<path d="M80,52L80,52" :class="[$style.line, $style.animFade]"/>
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircle]"/>
<circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircle]"/>
</svg>
<svg v-else-if="type === 'question'" :class="[$style.icon, $style.question]" viewBox="0 0 160 160">
<path d="M80,92L79.991,84C88.799,83.98 96,76.962 96,68C96,59.038 88.953,52 79.991,52C71.03,52 64,59.038 64,68" style="--l:85;" :class="[$style.line, $style.animLine]"/>
<path d="M80,92L79.991,84C88.799,83.98 96,76.962 96,68C96,59.038 88.953,52 79.991,52C71.03,52 64,59.038 64,68" pathLength="1" :class="[$style.line, $style.animLine]"/>
<path d="M80,108L80,108" :class="[$style.line, $style.animFade]"/>
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircle]"/>
<circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircle]"/>
</svg>
<svg v-else-if="type === 'success'" :class="[$style.icon, $style.success]" viewBox="0 0 160 160">
<path d="M62,80L74,92L98,68" style="--l:50;" :class="[$style.line, $style.animLine]"/>
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircle]"/>
<path d="M62,80L74,92L98,68" pathLength="1" :class="[$style.line, $style.animLine]"/>
<circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircle]"/>
</svg>
<svg v-else-if="type === 'warn'" :class="[$style.icon, $style.warn]" viewBox="0 0 160 160">
<path d="M80,64L80,88" style="--l:27;" :class="[$style.line, $style.animLine]"/>
<path d="M80,64L80,88" pathLength="1" :class="[$style.line, $style.animLine]"/>
<path d="M80,108L80,108" :class="[$style.line, $style.animFade]"/>
<path d="M92,28L144,116C148.709,124.65 144.083,135.82 136,136L24,136C15.917,135.82 11.291,124.65 16,116L68,28C73.498,19.945 86.771,19.945 92,28Z" style="--l:395;" :class="[$style.line, $style.animLine]"/>
<path d="M92,28L144,116C148.709,124.65 144.083,135.82 136,136L24,136C15.917,135.82 11.291,124.65 16,116L68,28C73.498,19.945 86.771,19.945 92,28Z" pathLength="1" :class="[$style.line, $style.animLine]"/>
</svg>
<svg v-else-if="type === 'error'" :class="[$style.icon, $style.error]" viewBox="0 0 160 160">
<path d="M63,63L96,96" style="--l:47;--duration:0.3s;" :class="[$style.line, $style.animLine]"/>
<path d="M96,63L63,96" style="--l:47;--duration:0.3s;--delay:0.2s;" :class="[$style.line, $style.animLine]"/>
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircle]"/>
<path d="M63,63L96,96" pathLength="1" style="--duration:0.3s;" :class="[$style.line, $style.animLine]"/>
<path d="M96,63L63,96" pathLength="1" style="--duration:0.3s;--delay:0.2s;" :class="[$style.line, $style.animLine]"/>
<circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircle]"/>
</svg>
<svg v-else-if="type === 'waiting'" :class="[$style.icon, $style.waiting]" viewBox="0 0 160 160">
<circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircleWaiting]"/>
<circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircleWaiting]"/>
<circle cx="80" cy="80" r="56" style="opacity: 0.25;" :class="[$style.line]"/>
</svg>
</template>
@ -80,15 +80,15 @@ const props = defineProps<{
}
.animLine {
stroke-dasharray: var(--l);
stroke-dashoffset: var(--l);
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: line var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards;
animation-delay: var(--delay, 0s);
}
.animCircle {
stroke-dasharray: var(--l);
stroke-dashoffset: var(--l);
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: line var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards;
animation-delay: var(--delay, 0s);
transform-origin: center;
@ -96,8 +96,8 @@ const props = defineProps<{
}
.animCircleWaiting {
stroke-dasharray: var(--l);
stroke-dashoffset: calc(var(--l) / 1.5);
stroke-dasharray: 1;
stroke-dashoffset: calc(1 / 1.5);
animation: waiting 0.75s linear infinite;
transform-origin: center;
}
@ -110,7 +110,7 @@ const props = defineProps<{
@keyframes line {
0% {
stroke-dashoffset: var(--l);
stroke-dashoffset: 1;
opacity: 0;
}
100% {

View File

@ -447,7 +447,6 @@ const headerTabs = computed(() => room.value ? [{
const headerActions = computed<PageHeaderItem[]>(() => [{
icon: 'ti ti-dots',
text: '',
handler: showMenu,
}]);

View File

@ -63,6 +63,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, onDeactivated, onUnmounted, ref, watch, shallowRef, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { utils } from '@syuilo/aiscript';
import { compareVersions } from 'compare-versions';
import { url } from '@@/js/config.js';
import type { Ref } from 'vue';
import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
@ -190,11 +192,21 @@ function start() {
run();
}
function getIsLegacy(version: string | null): boolean {
if (version == null) return false;
try {
return compareVersions(version, '1.0.0') < 0;
} catch {
return false;
}
}
async function run() {
if (aiscript.value) aiscript.value.abort();
if (!flash.value) return;
const isLegacy = !flash.value.script.replaceAll(' ', '').startsWith('///@1.0.0');
const version = utils.getLangVersion(flash.value.script);
const isLegacy = version != null && getIsLegacy(version);
const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');

View File

@ -63,14 +63,28 @@ function accept(user: Misskey.entities.UserLite) {
});
}
function reject(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
async function reject(user: Misskey.entities.UserLite) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.rejectFollowRequestConfirm({ name: user.name || user.username }),
});
if (canceled) return;
await os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
paginator.reload();
});
}
function cancel(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
async function cancel(user: Misskey.entities.UserLite) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.cancelFollowRequestConfirm({ name: user.name || user.username }),
});
if (canceled) return;
await os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
paginator.reload();
});
}

View File

@ -43,6 +43,8 @@ const paginator = markRaw(new Paginator('clips/list', {
}));
const favoritesPaginator = markRaw(new Paginator('clips/my-favorites', {
//
noPaging: true,
}));
async function create() {

View File

@ -465,6 +465,7 @@ definePage(() => ({
}
.pageContent {
contain: content;
margin-bottom: 1.5rem;
}

View File

@ -163,7 +163,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
type: 'link',
icon: 'ti ti-plus',
text: i18n.ts.createNew,
to: '/channels',
to: '/channels/new',
},
];
os.popupMenu(items.filter(i => i != null), ev.currentTarget ?? ev.target);

View File

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { BroadcastChannel } from 'broadcast-channel';
import type { StorageProvider } from '@/preferences/manager.js';
import { cloudBackup } from '@/preferences/utility.js';
import { miLocalStorage } from '@/local-storage.js';
@ -12,6 +13,7 @@ import { $i } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { TAB_ID } from '@/tab-id.js';
// クラウド同期用グループ名
const syncGroup = 'default';
const io: StorageProvider = {
@ -26,7 +28,6 @@ const io: StorageProvider = {
save: (ctx) => {
miLocalStorage.setItem('preferences', JSON.stringify(ctx.profile));
miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`);
},
cloudGet: async (ctx) => {
@ -99,33 +100,47 @@ const io: StorageProvider = {
export const prefer = new PreferencesManager(io, $i);
let latestSyncedAt = Date.now();
//#region タブ間同期
let latestPreferencesUpdate: {
tabId: string;
timestamp: number;
} | null = null;
function syncBetweenTabs() {
const latest = miLocalStorage.getItem('latestPreferencesUpdate');
if (latest == null) return;
const preferencesChannel = new BroadcastChannel<{
type: 'preferencesUpdate';
tabId: string;
timestamp: number;
}>('preferences');
const latestTab = latest.split('/')[0];
const latestAt = parseInt(latest.split('/')[1]);
if (latestTab === TAB_ID) return;
if (latestAt <= latestSyncedAt) return;
prefer.reloadProfile();
latestSyncedAt = Date.now();
if (_DEV_) console.log('prefer:synced');
}
window.setInterval(syncBetweenTabs, 5000);
window.document.addEventListener('visibilitychange', () => {
if (window.document.visibilityState === 'visible') {
syncBetweenTabs();
}
prefer.on('committed', () => {
latestPreferencesUpdate = {
tabId: TAB_ID,
timestamp: Date.now(),
};
preferencesChannel.postMessage({
type: 'preferencesUpdate',
tabId: TAB_ID,
timestamp: latestPreferencesUpdate.timestamp,
});
});
preferencesChannel.addEventListener('message', (msg) => {
if (msg.type === 'preferencesUpdate') {
if (msg.tabId === TAB_ID) return;
if (latestPreferencesUpdate != null) {
if (msg.timestamp <= latestPreferencesUpdate.timestamp) return;
}
prefer.reloadProfile();
if (_DEV_) console.log('prefer:received update from other tab');
latestPreferencesUpdate = {
tabId: msg.tabId,
timestamp: msg.timestamp,
};
}
});
//#endregion
//#region 定期クラウドバックアップ
let latestBackupAt = 0;
window.setInterval(() => {
@ -138,6 +153,7 @@ window.setInterval(() => {
latestBackupAt = Date.now();
});
}, 1000 * 60 * 3);
//#endregion
if (_DEV_) {
(window as any).prefer = prefer;

View File

@ -234,7 +234,7 @@ export const PREF_DEF = definePreferences({
default: false,
},
disableShowingAnimatedImages: {
default: prefersReducedMotion,
default: false,
},
emojiStyle: {
default: 'twemoji', // twemoji / fluentEmoji / native

View File

@ -4,6 +4,7 @@
*/
import { computed, onUnmounted, ref, watch } from 'vue';
import { EventEmitter } from 'eventemitter3';
import { host, version } from '@@/js/config.js';
import { PREF_DEF } from './def.js';
import type { Ref, WritableComputedRef } from 'vue';
@ -100,6 +101,14 @@ type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) =>
export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>;
type PreferencesManagerEvents = {
'committed': <K extends keyof PREF>(ctx: {
key: K;
value: ValueOf<K>;
oldValue: ValueOf<K>;
}) => void;
};
export function definePreferences<T extends Record<string, unknown>>(x: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>
}): {
@ -180,7 +189,7 @@ function normalizePreferences(preferences: PossiblyNonNormalizedPreferencesProfi
// TODO: PreferencesManagerForGuest のような非ログイン専用のクラスを分離すればthis.currentAccountのnullチェックやaccountがnullであるスコープのレコード挿入などが不要になり綺麗になるかもしれない
// と思ったけど操作アカウントが存在しない場合も考慮する現在の設計の方が汎用的かつ堅牢かもしれない
// NOTE: accountDependentな設定は初期状態であってもアカウントごとのスコープでレコードを作成しておかないと、サーバー同期する際に正しく動作しなくなる
export class PreferencesManager {
export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
private io: StorageProvider;
private currentAccount: { id: string } | null;
public profile: PreferencesProfile;
@ -201,6 +210,8 @@ export class PreferencesManager {
};
constructor(io: StorageProvider, currentAccount: { id: string } | null) {
super();
this.io = io;
this.currentAccount = currentAccount;
@ -246,6 +257,12 @@ export class PreferencesManager {
this.rewriteRawState(key, v);
this.emit('committed', {
key,
value: v,
oldValue: this.s[key],
});
const record = this.getMatchedRecordOf(key);
if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) {

View File

@ -5,7 +5,5 @@
import { genId } from '@/utility/id.js';
// HMR有効時にバグか知らんけど複数回実行されるのでその対策
export const TAB_ID = window.sessionStorage.getItem('TAB_ID') ?? genId();
window.sessionStorage.setItem('TAB_ID', TAB_ID);
export const TAB_ID = genId();
if (_DEV_) console.log('TAB_ID', TAB_ID);

View File

@ -158,6 +158,8 @@ export function applyTheme(theme: Theme, persist = true) {
// 様々な理由により startViewTransition は失敗することがある
// ref. https://github.com/misskey-dev/misskey/issues/16562
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
console.error(err);
window.document.documentElement.classList.remove('_themeChanging_');

View File

@ -4,8 +4,8 @@
*/
export type PageHeaderItem = {
text: string;
icon: string;
highlighted?: boolean;
handler: (ev: MouseEvent) => void;
text?: string;
icon: string;
highlighted?: boolean;
handler: (ev: MouseEvent) => void;
};

View File

@ -4,40 +4,40 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="azykntjl">
<div class="body">
<div class="left">
<button v-click-anime class="item _button instance" @click="openInstanceMenu">
<img :src="instance.iconUrl ?? '/favicon.ico'" draggable="false"/>
<div :class="[$style.root, acrylic ? $style.acrylic : null]">
<div :class="$style.body">
<div :class="$style.left">
<button v-click-anime :class="[$style.item, $style.instance]" class="_button" @click="openInstanceMenu">
<img :class="$style.instanceIcon" :src="instance.iconUrl ?? '/favicon.ico'" draggable="false"/>
</button>
<MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact>
<i class="ti ti-home ti-fw"></i>
<MkA v-click-anime v-tooltip="i18n.ts.timeline" :class="$style.item" activeClass="active" to="/" exact>
<i :class="$style.itemIcon" class="ti ti-home ti-fw"></i>
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" class="divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="navbarItemDef[item].icon"></i>
<span v-if="navbarItemDef[item].indicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
<div v-if="item === '-'" :class="$style.divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="_button" :class="$style.item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i :class="[$style.itemIcon, navbarItemDef[item].icon]" class="ti-fw"></i>
<span v-if="navbarItemDef[item].indicated" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</component>
</template>
<div class="divider"></div>
<div :class="$style.divider"></div>
<MkA v-if="$i && ($i.isAdmin || $i.isModerator)" v-click-anime v-tooltip="i18n.ts.controlPanel" class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null">
<i class="ti ti-dashboard ti-fw"></i>
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i>
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="ti ti-dots ti-fw"></i>
<span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
<button v-click-anime :class="$style.item" class="_button" @click="more">
<i :class="$style.itemIcon" class="ti ti-dots ti-fw"></i>
<span v-if="otherNavItemIndicated" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</button>
</div>
<div class="right">
<MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
<i class="ti ti-settings ti-fw"></i>
<div :class="$style.right">
<MkA v-click-anime v-tooltip="i18n.ts.settings" :class="$style.item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i>
</MkA>
<button v-if="$i" v-click-anime class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/>
<button v-if="$i" v-click-anime :class="[$style.item, $style.account]" class="_button" @click="openAccountMenu">
<MkAvatar :user="$i" :class="$style.avatar"/><MkAcct :class="$style.acct" :user="$i"/>
</button>
<div class="post" @click="os.post()">
<MkButton class="button" gradate full rounded>
<div :class="$style.post" @click="os.post()">
<MkButton :class="$style.postButton" gradate rounded>
<i class="ti ti-pencil ti-fw"></i>
</MkButton>
</div>
@ -61,6 +61,10 @@ import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
const WINDOW_THRESHOLD = 1400;
const props = defineProps<{
acrylic?: boolean;
}>();
const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
const menu = ref(prefer.s.menu);
// const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
@ -100,121 +104,140 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
.azykntjl {
$height: 60px;
$avatar-size: 32px;
$avatar-margin: 8px;
<style lang="scss" module>
.root {
--height: 60px;
position: sticky;
top: 0;
z-index: 1000;
width: 100%;
height: $height;
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
height: var(--height);
contain: strict;
background: var(--MI_THEME-navBg);
> .body {
max-width: 1380px;
margin: 0 auto;
display: flex;
> .right,
> .left {
> .item {
position: relative;
font-size: 0.9em;
display: inline-block;
padding: 0 12px;
line-height: $height;
> i,
> .avatar {
margin-right: 0;
}
> i {
left: 10px;
}
> .avatar {
width: $avatar-size;
height: $avatar-size;
vertical-align: middle;
}
> .indicator {
position: absolute;
top: 0;
left: 0;
color: var(--MI_THEME-navIndicator);
font-size: 8px;
}
&:hover {
text-decoration: none;
color: light-dark(hsl(from var(--MI_THEME-navFg) h s calc(l - 17)), hsl(from var(--MI_THEME-navFg) h s calc(l + 17)));
}
&.active {
color: var(--MI_THEME-navActive);
}
}
> .divider {
display: inline-block;
height: 16px;
margin: 0 10px;
border-right: solid 0.5px var(--MI_THEME-divider);
}
> .instance {
display: inline-block;
position: relative;
width: 56px;
height: 100%;
vertical-align: bottom;
> img {
display: inline-block;
width: 24px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
}
> .post {
display: inline-block;
> .button {
width: 40px;
height: 40px;
padding: 0;
min-width: 0;
}
}
> .account {
display: inline-flex;
align-items: center;
vertical-align: top;
margin-right: 8px;
> .acct {
margin-left: 8px;
}
}
}
> .right {
margin-left: auto;
}
&.acrylic {
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
}
}
.body {
max-width: 1380px;
margin: 0 auto;
display: flex;
overflow: auto;
overflow-y: clip;
white-space: nowrap;
}
.item {
position: relative;
font-size: 0.9em;
display: inline-block;
padding: 0 12px;
line-height: var(--height);
&:hover {
text-decoration: none;
color: light-dark(hsl(from var(--MI_THEME-navFg) h s calc(l - 17)), hsl(from var(--MI_THEME-navFg) h s calc(l + 17)));
}
&.active {
color: var(--MI_THEME-navActive);
}
}
.itemIcon {
margin-right: 0;
left: 10px;
}
.avatar {
margin-right: 0;
width: 32px;
height: 32px;
vertical-align: middle;
}
.acct {
margin-left: 8px;
@media (max-width: 1200px) {
display: none;
}
}
.indicator {
position: absolute;
top: 0;
left: 0;
color: var(--MI_THEME-navIndicator);
font-size: 8px;
}
.divider {
display: inline-block;
height: 16px;
margin: 0 10px;
border-right: solid 0.5px var(--MI_THEME-divider);
}
.instance {
display: inline-block;
position: relative;
width: 56px;
height: 100%;
vertical-align: bottom;
position: sticky;
top: 0;
left: 0;
z-index: 1;
}
.instanceIcon {
display: inline-block;
width: 24px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
.right {
display: flex;
align-items: center;
margin-left: auto;
position: sticky;
top: 0;
right: 0;
z-index: 1;
contain: content;
background: var(--MI_THEME-navBg);
}
.acrylic .right {
background: transparent;
}
.post {
display: inline-block;
margin-right: 8px;
}
.postButton {
width: 40px;
height: 40px;
padding: 0;
min-width: 0;
}
.account {
display: inline-flex;
align-items: center;
vertical-align: top;
margin-right: 8px;
}
</style>

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XSidebar v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'left'"/>
<div :class="[$style.main, { [$style.withWallpaper]: withWallpaper, [$style.withSidebarAndTitlebar]: !isMobile && prefer.r['deck.navbarPosition'].value === 'left' && prefer.r.showTitlebar.value }]" :style="{ backgroundImage: prefer.s['deck.wallpaper'] != null ? `url(${ prefer.s['deck.wallpaper'] })` : '' }">
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/>
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'" :acrylic="withWallpaper"/>
<XReloadSuggestion v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'bottom'"/>
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'bottom'" :acrylic="withWallpaper"/>
<XMobileFooterMenu v-if="isMobile" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed, ref, shallowRef, watch } from 'vue';
import { computed, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
import * as os from '@/os.js';
type TourStep = {
@ -26,7 +26,7 @@ export function startTour(steps: TourStep[]) {
anchorElementRef.value = step.element;
});
const { dispose } = os.popup(await import('@/components/MkSpot.vue').then(x => x.default), {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSpot.vue')), {
title: titleRef,
description: descriptionRef,
anchorElement: anchorElementRef,

View File

@ -11,10 +11,10 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.9.2",
"@types/node": "24.10.1",
"@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2"
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0"
},
"dependencies": {
"@tabler/icons-webfont": "3.35.0",

View File

@ -25,14 +25,14 @@
},
"devDependencies": {
"@types/matter-js": "0.20.2",
"@types/node": "24.9.2",
"@types/node": "24.10.1",
"@types/seedrandom": "3.0.8",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"esbuild": "0.25.11",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"esbuild": "0.27.0",
"execa": "9.6.0",
"glob": "11.0.3",
"nodemon": "3.1.10",
"glob": "11.1.0",
"nodemon": "3.1.11",
"typescript": "5.9.3"
},
"files": [

View File

@ -7,16 +7,16 @@
"generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix"
},
"devDependencies": {
"@readme/openapi-parser": "5.2.0",
"@types/node": "24.9.2",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"@readme/openapi-parser": "5.2.1",
"@types/node": "24.10.1",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"openapi-types": "12.1.3",
"openapi-typescript": "7.10.1",
"ts-case-convert": "2.1.0",
"tsx": "4.20.6",
"typescript": "5.9.3",
"eslint": "9.39.0"
"eslint": "9.39.1"
},
"files": [
"built"

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.11.0",
"version": "2025.11.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@ -37,19 +37,19 @@
"directory": "packages/misskey-js"
},
"devDependencies": {
"@microsoft/api-extractor": "7.53.3",
"@types/node": "24.9.2",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"@vitest/coverage-v8": "3.2.4",
"esbuild": "0.25.11",
"@microsoft/api-extractor": "7.55.0",
"@types/node": "24.10.1",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/coverage-v8": "4.0.10",
"esbuild": "0.27.0",
"execa": "9.6.0",
"glob": "11.0.3",
"glob": "13.0.0",
"ncp": "2.0.0",
"nodemon": "3.1.10",
"nodemon": "3.1.11",
"tsd": "0.33.0",
"typescript": "5.9.3",
"vitest": "3.2.4",
"vitest": "4.0.10",
"vitest-websocket-mock": "0.5.0"
},
"files": [

View File

@ -24,13 +24,13 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.9.2",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"esbuild": "0.25.11",
"@types/node": "24.10.1",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"esbuild": "0.27.0",
"execa": "9.6.0",
"glob": "11.0.3",
"nodemon": "3.1.10",
"glob": "11.1.0",
"nodemon": "3.1.11",
"typescript": "5.9.3"
},
"files": [

View File

@ -9,15 +9,15 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"esbuild": "0.25.11",
"esbuild": "0.27.0",
"idb-keyval": "6.2.2",
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "8.46.2",
"@typescript-eslint/parser": "8.47.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
"eslint-plugin-import": "2.32.0",
"nodemon": "3.1.10",
"nodemon": "3.1.11",
"typescript": "5.9.3"
},
"type": "module"

File diff suppressed because it is too large Load Diff

View File

@ -32,3 +32,4 @@ onlyBuiltDependencies:
ignorePatchFailures: false
minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack
minimumReleaseAgeExclude:
- '@syuilo/aiscript'

View File

@ -106,6 +106,18 @@
'.devcontainer/**',
],
},
{
// devcontainer (Dockerfile用の設定)
groupName: '[devcontainer] Update dependencies',
matchFileNames: ['.devcontainer/Dockerfile'],
matchDepNames: ['mcr.microsoft.com/devcontainers/javascript-node'],
allowedVersions: '/-[0-9]+-trixie$/',
// major/minor/patch: devcontainer の semver
// build: Node メジャー
// dist: Debian codename (e.g., bullseye, bookworm)(比較には使わない)
versioning: 'regex:^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)-(?<build>\\d+)-(?<dist>.+)$',
},
],
customManagers: [
{

File diff suppressed because it is too large Load Diff

View File

@ -11,14 +11,14 @@
"devDependencies": {
"@types/mdast": "4.0.4",
"@types/node": "24.9.1",
"@vitest/coverage-v8": "3.2.4",
"@vitest/coverage-v8": "4.0.10",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "5.9.3",
"unified": "11.0.5",
"vite": "6.4.1",
"vite-node": "3.2.4",
"vitest": "3.2.4"
"vite": "7.2.2",
"vite-node": "5.2.0",
"vitest": "4.0.10"
}
}