Compare commits

...

19 Commits

Author SHA1 Message Date
taichan 2b8bd591a9
Merge 48232ca57b into fe38115883 2025-10-07 03:36:59 +09:00
syuilo fe38115883 lint 2025-10-06 20:01:19 +09:00
syuilo 6fba73ca13 Update pnpm-lock.yaml 2025-10-06 19:21:21 +09:00
syuilo 0d33e1f839 fix notes\drafts\create param defs 2025-10-06 19:21:17 +09:00
syuilo 74f33157a3
New Crowdin updates (#16601)
* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Turkish)

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

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Greek)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Ukrainian)

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

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

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

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

* New translations ja-jp.yml (Catalan)
2025-10-06 15:39:18 +09:00
syuilo ae10cad9a7 perf(frontend): improve about#emojis performancce 2025-10-06 10:21:46 +09:00
syuilo ba9924abdb refactor(frontend): use useTemplateRef 2025-10-06 10:18:14 +09:00
github-actions[bot] 99686801a0 Bump version to 2025.10.0-beta.2 2025-10-06 01:12:30 +00:00
かっこかり f3e0713501
enhance(frontend): お問い合わせページからデバイス情報を出力できるように (#16598)
* enhance(frontend): デバイス情報を出力できるように

* fix lint

* Update Changelog

* enhance: getHighEntropyValuesが使用できなかった場合のフォールバックを追加

* fix lint

* fix: getHighEntropyValuesが使用できない場合は生のUAを返すように

* enhance: getHighEntropyValuesが使用できる場合でも生のUAを含めるように

* ✌️

* onHeaderClicked -> onOpened
2025-10-06 10:06:53 +09:00
かっこかり 7fcbf57a9d
fix(frontend): 存在しない翻訳を修正 (#16604) 2025-10-06 10:06:20 +09:00
taichanne30 48232ca57b
ユーザーTLではFTTのソースが空の際にDBにFallbackしないように 2024-07-25 16:01:41 +09:00
taichanne30 3564bf5c66
Refactor: const naming 2024-07-25 14:37:09 +09:00
taichanne30 685fc2bd9d
Fix: shouldFallbackToDbがすでにtrueの場合にそれが無視される 2024-07-25 14:30:10 +09:00
taichanne30 6cc0138d1e
Merge branch 'develop' of https://github.com/misskey-dev/misskey into fix-stl-note-fetch 2024-07-25 14:23:31 +09:00
taichan d3228d5570
Merge branch 'develop' into fix-stl-note-fetch 2024-03-07 02:04:23 +09:00
taichanne30 4a8ffe20a7
Fix timeline fetch when using sinceId 2024-03-07 01:55:57 +09:00
taichanne30 5615675991
Update CHANGELOG.md 2024-03-02 15:35:39 +09:00
taichan 0c65b8058a
Merge branch 'develop' into fix-stl-note-fetch 2024-03-02 15:28:45 +09:00
taichanne30 82bec76cd4
fix(backend): DBフォールバック有効時、複数のFTTソースから取得するタイムラインで取得漏れが起きる現象の修正 2024-03-02 15:23:25 +09:00
47 changed files with 514 additions and 729 deletions

View File

@ -20,7 +20,9 @@
- Enhance: ウォーターマークにアカウントのQRコードを追加できるように
- Enhance: テーマをドラッグ&ドロップできるように
- Enhance: 絵文字ピッカーのサイズをより大きくできるように
- Enhance: カスタム絵文字が多い場合にサーバーの絵文字一覧ページがフリーズしないように
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
- Enhance: 「お問い合わせ」ページから、バグの調査等に役立つ情報OSやブラウザのバージョン等を取得・コピーできるように
- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
- Fix: アクティビティウィジェットのグラフモードが動作しない問題を修正
@ -1073,7 +1075,7 @@
- Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487
### Server
-
- Fix: FTT有効かつDBフォールバック有効時、STLのようにタイムラインのソースが複数だとFTTとDBのフォールバック間で取得されないートがある問題
## 2024.3.0

View File

@ -1010,6 +1010,7 @@ postForm: "أنشئ ملاحظة"
information: "عن"
inMinutes: "د"
inDays: "ي"
widgets: "التطبيقات المُصغّرة"
_chat:
invitations: "دعوة"
noHistory: "السجل فارغ"

View File

@ -850,6 +850,7 @@ postForm: "নোট লিখুন"
information: "আপনার সম্পর্কে"
inMinutes: "মিনিট"
inDays: "দিন"
widgets: "উইজেটগুলি"
_chat:
invitations: "আমন্ত্রণ"
noHistory: "কোনো ইতিহাস নেই"

View File

@ -334,6 +334,7 @@ fileName: "Nom del Fitxer"
selectFile: "Selecciona un fitxer"
selectFiles: "Selecciona fitxers"
selectFolder: "Selecció de carpeta"
unselectFolder: "Deixa de seleccionar la carpeta"
selectFolders: "Selecció de carpetes"
fileNotSelected: "Cap fitxer seleccionat"
renameFile: "Canvia el nom del fitxer"
@ -346,6 +347,7 @@ addFile: "Afegeix un fitxer"
showFile: "Mostrar fitxer"
emptyDrive: "El teu Disc és buit"
emptyFolder: "La carpeta està buida"
dropHereToUpload: "Arrossega els arxius fins aquí per pujar-los al servidor"
unableToDelete: "No es pot eliminar"
inputNewFileName: "Introduïu el nom de fitxer nou"
inputNewDescription: "Escriu el peu de foto."
@ -1389,6 +1391,9 @@ scheduleToPostOnX: "Programar una nota per {x}"
scheduledToPostOnX: "S'ha programat la nota per {x}"
schedule: "Programa"
scheduled: "Programat"
widgets: "Ginys"
deviceInfo: "Informació del dispositiu"
deviceInfoDescription: "En fer consultes tècniques influir la següent informació pot ajudar a resoldre'l més ràpidament."
_compression:
_quality:
high: "Qualitat alta"
@ -2431,6 +2436,7 @@ _auth:
scopeUser: "Opera com si fossis aquest usuari"
pleaseLogin: "Si us plau, identificat per autoritzar l'aplicació."
byClickingYouWillBeRedirectedToThisUrl: "Si es garanteix l'accés, seràs redirigit automàticament a la següent adreça URL"
alreadyAuthorized: "Aquesta aplicació ja té accés."
_antennaSources:
all: "Totes les publicacions"
homeTimeline: "Publicacions dels usuaris seguits"
@ -2697,6 +2703,8 @@ _notification:
quote: "Citar"
reaction: "Reaccions"
pollEnded: "Enquesta terminada"
scheduledNotePosted: "Nota programada amb èxit "
scheduledNotePostFailed: "Ha fallat la programació de la nota"
receiveFollowRequest: "Rebuda una petició de seguiment"
followRequestAccepted: "Petició de seguiment acceptada"
roleAssigned: "Rol donat"

View File

@ -1109,6 +1109,7 @@ postForm: "Formulář pro odeslání"
information: "Informace"
inMinutes: "Minut"
inDays: "Dnů"
widgets: "Widgety"
_chat:
invitations: "Pozvat"
noHistory: "Žádná historie"

View File

@ -1370,6 +1370,7 @@ defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe"
defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße. <br>Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität."
inMinutes: "Minute(n)"
inDays: "Tag(en)"
widgets: "Widgets"
_order:
newest: "Neueste zuerst"
oldest: "Älteste zuerst"

View File

@ -288,6 +288,7 @@ replies: "Απάντηση"
renotes: "Κοινοποίηση σημειώματος"
postForm: "Φόρμα δημοσίευσης"
information: "Πληροφορίες"
widgets: "Μαραφέτια"
_chat:
members: "Μέλη"
home: "Κεντρικό"

View File

@ -1389,6 +1389,7 @@ scheduleToPostOnX: "Scheduled to note on {x}"
scheduledToPostOnX: "Note is scheduled for {x}"
schedule: "Schedule"
scheduled: "Scheduled"
widgets: "Widgets"
_compression:
_quality:
high: "High quality"

View File

@ -1389,6 +1389,7 @@ scheduleToPostOnX: "Programar una nota para {x}"
scheduledToPostOnX: "La nota está programada para {x}."
schedule: "Programado"
scheduled: "Programado"
widgets: "Widgets"
_compression:
_quality:
high: "Calidad alta"

View File

@ -1273,6 +1273,7 @@ postForm: "Formulaire de publication"
information: "Informations"
inMinutes: "min"
inDays: "j"
widgets: "Widgets"
_chat:
invitations: "Inviter"
noHistory: "Pas d'historique"

View File

@ -1264,6 +1264,7 @@ postForm: "Buat catatan"
information: "Informasi"
inMinutes: "menit"
inDays: "hari"
widgets: "Widget"
_chat:
invitations: "Undang"
noHistory: "Tidak ada riwayat"

20
locales/index.d.ts vendored
View File

@ -1354,6 +1354,10 @@ export interface Locale extends ILocale {
*
*/
"selectFolder": string;
/**
*
*/
"unselectFolder": string;
/**
*
*/
@ -1402,6 +1406,10 @@ export interface Locale extends ILocale {
*
*/
"emptyFolder": string;
/**
*
*/
"dropHereToUpload": string;
/**
*
*/
@ -5581,6 +5589,14 @@ export interface Locale extends ILocale {
*
*/
"widgets": string;
/**
*
*/
"deviceInfo": string;
/**
*
*/
"deviceInfoDescription": string;
"_compression": {
"_quality": {
/**
@ -9460,6 +9476,10 @@ export interface Locale extends ILocale {
* URLに遷移します
*/
"byClickingYouWillBeRedirectedToThisUrl": string;
/**
*
*/
"alreadyAuthorized": string;
};
"_antennaSources": {
/**

View File

@ -1389,6 +1389,7 @@ scheduleToPostOnX: "Pianificare la pubblicazione {x}"
scheduledToPostOnX: "Pubblicazione pianificata {x}"
schedule: "Pianificare"
scheduled: "Pianificata"
widgets: "Riquadri"
_compression:
_quality:
high: "Alta qualità"

View File

@ -334,6 +334,7 @@ fileName: "ファイル名"
selectFile: "ファイルを選択"
selectFiles: "ファイルを選択"
selectFolder: "フォルダーを選択"
unselectFolder: "フォルダーの選択を解除"
selectFolders: "フォルダーを選択"
fileNotSelected: "ファイルが選択されていません"
renameFile: "ファイル名を変更"
@ -346,6 +347,7 @@ addFile: "ファイルを追加"
showFile: "ファイルを表示"
emptyDrive: "ドライブは空です"
emptyFolder: "フォルダーは空です"
dropHereToUpload: "ここにファイルをドロップしてアップロード"
unableToDelete: "削除できません"
inputNewFileName: "新しいファイル名を入力してください"
inputNewDescription: "新しいキャプションを入力してください"
@ -1390,6 +1392,8 @@ scheduledToPostOnX: "{x}に投稿が予約されています"
schedule: "予約"
scheduled: "予約"
widgets: "ウィジェット"
deviceInfo: "デバイス情報"
deviceInfoDescription: "技術的なお問い合わせの際に、以下の情報を併記すると問題の解決に役立つことがあります。"
_compression:
_quality:
@ -2484,6 +2488,7 @@ _auth:
scopeUser: "以下のユーザーとして操作しています"
pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。"
byClickingYouWillBeRedirectedToThisUrl: "アクセスを許可すると、自動で以下のURLに遷移します"
alreadyAuthorized: "このアプリケーションは既にアクセスが許可されています。"
_antennaSources:
all: "全てのノート"

View File

@ -1337,6 +1337,7 @@ safeModeEnabled: "セーフモードがオンになってるで"
pluginsAreDisabledBecauseSafeMode: "セーフモードがオンやから、プラグインは全部無効化されてるで。"
customCssIsDisabledBecauseSafeMode: "セーフモードがオンやから、カスタムCSSは適用されてへんで。"
themeIsDefaultBecauseSafeMode: "セーフモードがオンの間はデフォルトのテーマを使うで。セーフモードをオフにれば元に戻るで。"
widgets: "ウィジェット"
_chat:
noMessagesYet: "まだメッセージはあらへんで"
individualChat_description: "特定のユーザーと一対一でチャットができるで。"

View File

@ -1389,6 +1389,7 @@ scheduleToPostOnX: "{x}에 게시를 예약합니다."
scheduledToPostOnX: "{x}에 게시가 예약돼있습니다."
schedule: "예약"
scheduled: "예약"
widgets: "위젯"
_compression:
_quality:
high: "고품질"

View File

@ -1042,6 +1042,7 @@ postForm: "Formularz tworzenia wpisu"
information: "Informacje"
inMinutes: "minuta"
inDays: "dzień"
widgets: "Widżety"
_chat:
invitations: "Zaproś"
noHistory: "Brak historii"

View File

@ -1389,6 +1389,7 @@ scheduleToPostOnX: "Agendar nota para {x}"
scheduledToPostOnX: "A nota está agendada para {x}"
schedule: "Agendar"
scheduled: "Agendado"
widgets: "Widgets"
_compression:
_quality:
high: "Qualidade alta"

View File

@ -1277,6 +1277,7 @@ textCount: "Количество символов"
information: "Описание"
inMinutes: "мин"
inDays: "сут"
widgets: "Виджеты"
_chat:
invitations: "Пригласить"
noHistory: "История пока пуста"

View File

@ -915,6 +915,7 @@ postForm: "Napísať poznámku"
information: "Informácie"
inMinutes: "min"
inDays: "dní"
widgets: "Widgety"
_chat:
invitations: "Pozvať"
noHistory: "Žiadna história"

View File

@ -1389,6 +1389,7 @@ scheduleToPostOnX: "กำหนดเวลาให้โพสต์ไว้
scheduledToPostOnX: "มีการกำหนดเวลาให้โพสต์ไว้ที่ {x}"
schedule: "กำหนดเวลา"
scheduled: "กำหนดเวลา"
widgets: "วิดเจ็ต"
_compression:
_quality:
high: "คุณภาพสูง"

View File

@ -1378,6 +1378,7 @@ pluginsAreDisabledBecauseSafeMode: "Güvenli mod etkinleştirildiği için tüm
customCssIsDisabledBecauseSafeMode: "Güvenli mod etkin olduğu için özel CSS uygulanmıyor."
themeIsDefaultBecauseSafeMode: "Güvenli mod etkinken, varsayılan tema kullanılır. Güvenli modu devre dışı bırakmak bu değişiklikleri geri alır."
thankYouForTestingBeta: "Beta sürümünü test ettiğin için teşekkür ederiz!"
widgets: "Widget'lar"
_order:
newest: "Önce yeni"
oldest: "Önce eski"

View File

@ -921,6 +921,7 @@ postForm: "Створення нотатки"
information: "Інформація"
inMinutes: "х"
inDays: "д"
widgets: "Віджети"
_chat:
invitations: "Запросити"
noHistory: "Історія порожня"

View File

@ -1222,6 +1222,7 @@ migrateOldSettings: "Di chuyển cài đặt cũ"
migrateOldSettings_description: "Thông thường, quá trình này diễn ra tự động, nhưng nếu vì lý do nào đó mà quá trình di chuyển không thành công, bạn có thể kích hoạt thủ công quy trình di chuyển, quá trình này sẽ ghi đè lên thông tin cấu hình hiện tại của bạn."
inMinutes: "phút"
inDays: "ngày"
widgets: "Tiện ích"
_chat:
invitations: "Mời"
noHistory: "Không có dữ liệu"

View File

@ -56,7 +56,7 @@ deleteAndEdit: "删除并编辑"
deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。"
addToList: "添加至列表"
addToAntenna: "添加到天线"
sendMessage: "发送"
sendMessage: "发送消息"
copyRSS: "复制RSS"
copyUsername: "复制用户名"
copyUserId: "复制用户 ID"
@ -334,6 +334,7 @@ fileName: "文件名称"
selectFile: "选择文件"
selectFiles: "选择文件"
selectFolder: "选择文件夹"
unselectFolder: "取消全选文件夹"
selectFolders: "选择多个文件夹"
fileNotSelected: "未选择文件"
renameFile: "重命名文件"
@ -346,6 +347,7 @@ addFile: "添加文件"
showFile: "显示文件"
emptyDrive: "网盘中无文件"
emptyFolder: "此文件夹中无文件"
dropHereToUpload: "将文件拖动到这里来上传"
unableToDelete: "无法删除"
inputNewFileName: "请输入新文件名"
inputNewDescription: "请输入新标题"
@ -1389,6 +1391,9 @@ scheduleToPostOnX: "预定在 {x} 发出"
scheduledToPostOnX: "已预定在 {x} 发出"
schedule: "定时"
scheduled: "定时"
widgets: "小工具"
deviceInfo: "设备信息"
deviceInfoDescription: "咨询技术问题时,将以下信息一并发送有助于解决问题。"
_compression:
_quality:
high: "高质量"
@ -2431,6 +2436,7 @@ _auth:
scopeUser: "以下面的用户进行操作"
pleaseLogin: "在对应用进行授权许可之前,请先登录"
byClickingYouWillBeRedirectedToThisUrl: "允许访问后将会自动重定向到以下 URL"
alreadyAuthorized: "此应用已有访问许可。"
_antennaSources:
all: "所有帖子"
homeTimeline: "已关注用户的帖子"
@ -2697,6 +2703,8 @@ _notification:
quote: "引用"
reaction: "回应"
pollEnded: "问卷调查结束"
scheduledNotePosted: "定时发送成功"
scheduledNotePostFailed: "定时发送失败"
receiveFollowRequest: "收到关注请求"
followRequestAccepted: "关注请求已通过"
roleAssigned: "授予的角色"

View File

@ -1389,6 +1389,7 @@ scheduleToPostOnX: "排定在 {x} 發布"
scheduledToPostOnX: "已排定在 {x} 發布貼文"
schedule: "排定"
scheduled: "排定"
widgets: "小工具"
_compression:
_quality:
high: "高品質"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.10.0-beta.1",
"version": "2025.10.0-beta.2",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -40,6 +40,7 @@ type TimelineOptions = {
excludePureRenotes: boolean;
ignoreAuthorFromUserSuspension?: boolean;
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
preventEmptyTimelineDbFallback?: boolean;
};
@Injectable()
@ -73,12 +74,20 @@ export class FanoutTimelineEndpointService {
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
// オプション無効時、取得したredisResultのうち、2つ以上ソースがあり、1つでも空であればDBにフォールバックする
let shouldFallbackToDb = ps.useDbFallback &&
(ps.preventEmptyTimelineDbFallback !== true && redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
// 取得したresultの中で最古のIDのうち、最も新しいものを取得
const fttThresholdId = redisResult.map(ids => ids[0]).sort()[0];
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
const redisResultIds = shouldFallbackToDb ? [] : Array.from(new Set(redisResult.flat(1))).sort(idCompare);
let noteIds = redisResultIds.filter(id => id >= fttThresholdId).slice(0, ps.limit);
let noteIds = redisResultIds.slice(0, ps.limit);
const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
shouldFallbackToDb ||= ps.useDbFallback && (noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId);
if (!shouldFallbackToDb) {
let filter = ps.noteFilter ?? (_note => true) as NoteFilter;

View File

@ -192,7 +192,7 @@ export const paramDef = {
scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean', default: false },
},
required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'],
required: [],
} as const;
@Injectable()
@ -203,22 +203,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.create(me, {
fileIds: ps.fileIds,
fileIds: ps.fileIds ?? [],
pollChoices: ps.poll?.choices ?? [],
pollMultiple: ps.poll?.multiple ?? false,
pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
pollExpiredAfter: ps.poll?.expiredAfter ?? null,
hasPoll: ps.poll != null,
text: ps.text,
replyId: ps.replyId,
renoteId: ps.renoteId,
cw: ps.cw,
hashtag: ps.hashtag,
text: ps.text ?? null,
replyId: ps.replyId ?? null,
renoteId: ps.renoteId ?? null,
cw: ps.cw ?? null,
hashtag: ps.hashtag ?? null,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds,
channelId: ps.channelId,
visibleUserIds: ps.visibleUserIds ?? [],
channelId: ps.channelId ?? null,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled,
}).catch((err) => {

View File

@ -150,6 +150,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withFiles: ps.withFiles,
withRenotes: ps.withRenotes,
}, me),
preventEmptyTimelineDbFallback: true,
});
return timeline;

View File

@ -5,7 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- eslint-disable vue/no-v-html -->
<template>
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
<div
:class="[$style.codeBlockRoot, {
[$style.codeEditor]: codeEditor,
[$style.outerStyle]: !codeEditor && withOuterStyle,
[$style.dark]: darkMode,
[$style.light]: !darkMode,
}]" v-html="html"></div>
</template>
<script lang="ts" setup>
@ -15,11 +21,15 @@ import type { BundledLanguage } from 'shiki/langs';
import { getHighlighter, getTheme } from '@/utility/code-highlighter.js';
import { store } from '@/store.js';
const props = defineProps<{
const props = withDefaults(defineProps<{
code: string;
lang?: string;
codeEditor?: boolean;
}>();
withOuterStyle?: boolean;
}>(), {
codeEditor: false,
withOuterStyle: true,
});
const highlighter = await getHighlighter();
const darkMode = store.r.darkMode;
@ -73,17 +83,13 @@ watch(() => props.lang, (to) => {
<style module lang="scss">
.codeBlockRoot :global(.shiki) {
padding: 1em;
margin: 0;
overflow: auto;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
color: var(--shiki-fallback);
background-color: var(--shiki-fallback-bg);
& span {
color: var(--shiki-fallback);
background-color: var(--shiki-fallback-bg);
}
& pre,
@ -92,26 +98,40 @@ watch(() => props.lang, (to) => {
}
}
.outerStyle.codeBlockRoot :global(.shiki) {
padding: 1em;
margin: 0;
border-radius: 8px;
border: 1px solid var(--MI_THEME-divider);
background-color: var(--shiki-fallback-bg);
}
.light.codeBlockRoot :global(.shiki) {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
& span {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
}
}
.light.outerStyle.codeBlockRoot :global(.shiki),
.light.codeEditor.codeBlockRoot :global(.shiki) {
background-color: var(--shiki-light-bg);
}
.dark.codeBlockRoot :global(.shiki) {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
& span {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
}
}
.dark.outerStyle.codeBlockRoot :global(.shiki),
.dark.codeEditor.codeBlockRoot :global(.shiki) {
background-color: var(--shiki-dark-bg);
}
.codeBlockRoot.codeEditor {
min-width: 100%;
height: 100%;

View File

@ -5,15 +5,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.codeBlockRoot">
<button v-if="copyButton" :class="$style.codeBlockCopyButton" class="_button" @click="copy">
<button v-if="copyButton" :class="[$style.codeBlockCopyButton, { [$style.withOuterStyle]: withOuterStyle }]" class="_button" @click="copy">
<i class="ti ti-copy"></i>
</button>
<Suspense>
<template #fallback>
<MkLoading/>
<pre
class="_selectable"
:class="[$style.codeBlockFallbackRoot, {
[$style.outerStyle]: withOuterStyle,
}]"
><code :class="$style.codeBlockFallbackCode">Loading...</code></pre>
</template>
<XCode v-if="show && lang" class="_selectable" :code="code" :lang="lang"/>
<pre v-else-if="show" class="_selectable" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
<XCode
v-if="show && lang"
class="_selectable"
:code="code"
:lang="lang"
:withOuterStyle="withOuterStyle"
/>
<pre
v-else-if="show"
class="_selectable"
:class="[$style.codeBlockFallbackRoot, {
[$style.outerStyle]: withOuterStyle,
}]"
><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
<button v-else :class="$style.codePlaceholderRoot" @click="show = true">
<div :class="$style.codePlaceholderContainer">
<div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div>
@ -26,8 +43,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import * as os from '@/os.js';
import MkLoading from '@/components/global/MkLoading.vue';
import { i18n } from '@/i18n.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { prefer } from '@/preferences.js';
@ -36,10 +51,12 @@ const props = withDefaults(defineProps<{
code: string;
forceShow?: boolean;
copyButton?: boolean;
withOuterStyle?: boolean;
lang?: string;
}>(), {
copyButton: true,
forceShow: false,
withOuterStyle: true,
});
const show = ref(props.forceShow === true ? true : !prefer.s.dataSaver.code);
@ -58,10 +75,16 @@ function copy() {
.codeBlockCopyButton {
position: absolute;
top: 8px;
right: 8px;
opacity: 0.5;
top: 0;
right: 0;
&.withOuterStyle {
top: 8px;
right: 8px;
}
&:hover {
opacity: 0.8;
}
@ -70,11 +93,17 @@ function copy() {
.codeBlockFallbackRoot {
display: block;
overflow-wrap: anywhere;
padding: 1em;
margin: 0;
overflow: auto;
}
.outerStyle.codeBlockFallbackRoot {
background: var(--MI_THEME-bg);
padding: 1em;
margin: .5em 0;
border-radius: 8px;
border: 1px solid var(--MI_THEME-divider);
}
.codeBlockFallbackCode {
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
}

View File

@ -35,18 +35,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="select === 'folder'">
<template v-if="folder == null">
<MkButton v-if="!isRootSelected" @click="isRootSelected = true">
<i class="ti ti-square"></i> {{ i18n.ts.selectThisFolder }}
<i class="ti ti-square"></i> {{ i18n.ts.selectFolder }}
</MkButton>
<MkButton v-else @click="isRootSelected = false">
<i class="ti ti-checkbox"></i> {{ i18n.ts.unselectThisFolder }}
<i class="ti ti-checkbox"></i> {{ i18n.ts.unselectFolder }}
</MkButton>
</template>
<template v-else>
<MkButton v-if="!selectedFolders.some(f => f.id === folder!.id)" @click="selectedFolders.push(folder)">
<i class="ti ti-square"></i> {{ i18n.ts.selectThisFolder }}
<i class="ti ti-square"></i> {{ i18n.ts.selectFolder }}
</MkButton>
<MkButton v-else @click="selectedFolders = selectedFolders.filter(f => f.id !== folder!.id)">
<i class="ti ti-checkbox"></i> {{ i18n.ts.unselectThisFolder }}
<i class="ti ti-checkbox"></i> {{ i18n.ts.unselectFolder }}
</MkButton>
</template>
</div>
@ -112,7 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-show="filesPaginator.canFetchOlder.value" :class="$style.loadMore" primary rounded @click="filesPaginator.fetchOlder()">{{ i18n.ts.loadMore }}</MkButton>
<div v-if="filesPaginator.items.value.length == 0 && foldersPaginator.items.value.length == 0 && !fetching" :class="$style.empty">
<div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div>
<div v-if="draghover">{{ i18n.ts.dropHereToUpload }}</div>
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong></div>
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
</div>

View File

@ -96,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { nextTick, onMounted, ref, useTemplateRef } from 'vue';
import { nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
import { prefer } from '@/preferences.js';
import { getBgColor } from '@/utility/get-bg-color.js';
import { pageFolderTeleportCount, popup } from '@/os.js';
@ -119,6 +119,11 @@ const props = withDefaults(defineProps<{
canPage: true,
});
const emit = defineEmits<{
(ev: 'opened'): void;
(ev: 'closed'): void;
}>();
const rootEl = useTemplateRef('rootEl');
const asPage = props.canPage && deviceKind === 'smartphone' && prefer.s['experimental.enableFolderPageView'];
const bgSame = ref(false);
@ -164,7 +169,7 @@ function afterLeave(el: Element) {
let pageId = pageFolderTeleportCount.value;
pageFolderTeleportCount.value += 1000;
async function toggle() {
async function toggle(ev: MouseEvent) {
if (asPage && !opened.value) {
pageId++;
const { dispose } = await popup(MkFolderPage, {
@ -192,6 +197,14 @@ onMounted(() => {
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
bgSame.value = parentBg === myBg;
});
watch(opened, (isOpened) => {
if (isOpened) {
emit('opened');
} else {
emit('closed');
}
}, { flush: 'post' });
</script>
<style lang="scss" module>

View File

@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkKeyValue>
<template #key>{{ i18n.ts.id }}</template>
<template #key>{{ i18n.ts.name }}</template>
<template #value>{{ name }}</template>
</MkKeyValue>
<MkKeyValue>

View File

@ -68,7 +68,7 @@ export type GetMkSelectValueTypesFromDef<T extends MkSelectItem[]> = T[number] e
</script>
<script lang="ts" setup generic="const ITEMS extends MkSelectItem[], MODELT extends OptionValue">
import { onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import { onMounted, nextTick, ref, watch, computed, toRefs, useTemplateRef } from 'vue';
import { useInterval } from '@@/js/use-interval.js';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
@ -87,8 +87,8 @@ const props = defineProps<{
type ModelTChecked = MODELT & (
MODELT extends GetMkSelectValueTypesFromDef<ITEMS>
? unknown
: 'Error: The type of model does not match the type of items.'
? unknown
: 'Error: The type of model does not match the type of items.'
);
const model = defineModel<ModelTChecked>({ required: true });
@ -97,10 +97,10 @@ const { autofocus } = toRefs(props);
const focused = ref(false);
const opening = ref(false);
const currentValueText = ref<string | null>(null);
const inputEl = ref<HTMLObjectElement | null>(null);
const prefixEl = ref<HTMLElement | null>(null);
const suffixEl = ref<HTMLElement | null>(null);
const container = ref<HTMLElement | null>(null);
const inputEl = useTemplateRef('inputEl');
const prefixEl = useTemplateRef('prefixEl');
const suffixEl = useTemplateRef('suffixEl');
const container = useTemplateRef('container');
const height =
props.small ? 33 :
props.large ? 39 :

View File

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFoldableSection>
<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category ?? '___root___'">
<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category ?? '___root___'" :expanded="false">
<template #header>{{ category || i18n.ts.other }}</template>
<div :class="$style.emojis">
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/>

View File

@ -151,7 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else-if="tab === 'announcements'" class="_gaps">
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.createNew }}</MkButton>
<MkSelect v-model="announcementsStatus" :items="announcementsStatusDef">
<template #label>{{ i18n.ts.filter }}</template>

View File

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<h1>{{ i18n.ts._auth.denied }}</h1>
</div>
<div v-if="state == 'accepted' && session">
<h1>{{ session.app.isAuthorized ? i18n.ts['already-authorized'] : i18n.ts.allowed }}</h1>
<h1>{{ session.app.isAuthorized ? i18n.ts._auth.alreadyAuthorized : i18n.ts._auth.accepted }}</h1>
<p v-if="session.app.callbackUrl">
{{ i18n.ts._auth.callback }}
<MkEllipsis/>

View File

@ -28,17 +28,37 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
<MkFolder @opened="onOpened">
<template #icon><i class="ti ti-report-search"></i></template>
<template #label>{{ i18n.ts.deviceInfo }}</template>
<template #caption>{{ i18n.ts.deviceInfoDescription }}</template>
<MkLoading v-if="userEnv == null" />
<MkCode v-else lang="json" :code="JSON.stringify(userEnv, null, 2)" style="max-height: 300px; overflow: auto;"/>
</MkFolder>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { definePage } from '@/page.js';
import { getUserEnvironment } from '@/utility/get-user-environment.js';
import type { UserEnvironment } from '@/utility/get-user-environment.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkLink from '@/components/MkLink.vue';
import MkCode from '@/components/MkCode.vue';
const userEnv = ref<UserEnvironment | null>(null);
async function onOpened() {
if (userEnv.value == null) {
userEnv.value = await getUserEnvironment();
}
}
definePage(() => ({
title: i18n.ts.inquiry,

View File

@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
<FormSection v-if="keys">
<template #label>{{ i18n.ts.keys }}</template>
<template #label>{{ i18n.ts._registry.keys }}</template>
<div class="_gaps_s">
<FormLink v-for="key in keys" :to="`/registry/value/${props.domain}/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
</div>

View File

@ -159,7 +159,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { defineAsyncComponent, computed, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, watch, ref } from 'vue';
import { defineAsyncComponent, computed, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, watch, ref, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import { getScrollContainer } from '@@/js/scroll.js';
import MkNote from '@/components/MkNote.vue';
@ -222,9 +222,9 @@ const router = useRouter();
const user = ref(props.user);
const narrow = ref<null | boolean>(null);
const rootEl = ref<null | HTMLElement>(null);
const bannerEl = ref<null | HTMLElement>(null);
const memoTextareaEl = ref<null | HTMLElement>(null);
const rootEl = useTemplateRef('rootEl');
const bannerEl = useTemplateRef('bannerEl');
const memoTextareaEl = useTemplateRef('memoTextareaEl');
const memoDraft = ref(props.user.memo);
const isEditingMemo = ref(false);
const moderationNote = ref(props.user.moderationNote ?? '');

View File

@ -0,0 +1,66 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type UserEnvironment = {
os: string;
browser: string;
userAgent: string;
screenWidth: number;
screenHeight: number;
viaGetHighEntropyValues: true;
} | {
userAgent: string;
screenWidth: number;
screenHeight: number;
viaGetHighEntropyValues: false;
};
export async function getUserEnvironment(): Promise<UserEnvironment> {
if ('userAgentData' in navigator && navigator.userAgentData != null) {
try {
const uaData: any = await navigator.userAgentData.getHighEntropyValues([
'fullVersionList',
'platformVersion',
]);
let osVersion = 'v' + uaData.platformVersion;
if (uaData.platform === 'Windows' && uaData.platformVersion != null) {
// https://learn.microsoft.com/ja-jp/microsoft-edge/web-platform/how-to-detect-win11
const majorPlatformVersion = parseInt(uaData.platformVersion.split('.')[0]);
if (majorPlatformVersion >= 13) {
osVersion = '11 or later';
} else if (majorPlatformVersion > 0) {
osVersion = '10';
} else {
osVersion = '8.1 or earlier';
}
}
const browserData = uaData.fullVersionList.find((item) => !/^\s*not.+a.+brand\s*$/i.test(item.brand));
return {
os: `${uaData.platform} ${osVersion}`,
browser: browserData ? `${browserData.brand} v${browserData.version}` : 'Unknown',
userAgent: navigator.userAgent,
screenWidth: window.innerWidth,
screenHeight: window.innerHeight,
viaGetHighEntropyValues: true,
};
} catch {
return getViaUa();
}
} else {
return getViaUa();
}
}
function getViaUa(): UserEnvironment {
return {
userAgent: navigator.userAgent,
screenWidth: window.innerWidth,
screenHeight: window.innerHeight,
viaGetHighEntropyValues: false,
};
}

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<p v-if="widgetProps.folderId == null">
{{ i18n.ts.folder }}
</p>
<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.ts['no-image'] }}</p>
<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.ts.nothing }}</p>
<div ref="slideA" class="slide a"></div>
<div ref="slideB" class="slide b"></div>
</div>

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.10.0-beta.1",
"version": "2025.10.0-beta.2",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@ -29205,34 +29205,34 @@ export interface operations {
* @default public
* @enum {string}
*/
visibility: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds: string[];
cw: string | null;
hashtag: string | null;
visibility?: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds?: string[];
cw?: string | null;
hashtag?: string | null;
/** @default false */
localOnly: boolean;
localOnly?: boolean;
/**
* @default null
* @enum {string|null}
*/
reactionAcceptance: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
/** Format: misskey:id */
replyId: string | null;
replyId?: string | null;
/** Format: misskey:id */
renoteId: string | null;
renoteId?: string | null;
/** Format: misskey:id */
channelId: string | null;
text: string | null;
fileIds: string[];
poll: {
channelId?: string | null;
text?: string | null;
fileIds?: string[];
poll?: {
choices: string[];
multiple?: boolean;
expiresAt?: number | null;
expiredAfter?: number | null;
} | null;
scheduledAt: number | null;
scheduledAt?: number | null;
/** @default false */
isActuallyScheduled: boolean;
isActuallyScheduled?: boolean;
};
};
};

File diff suppressed because it is too large Load Diff