diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index be00aa7097..1da34db4e6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,7 @@ stages: testCommit: stage: test - image: node:latest + image: node:iron services: - postgres:15 - redis diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index ffdcc9787f..985f658999 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1264,10 +1264,10 @@ _initialTutorial: _reaction: title: "Què són les Reaccions?" description: "Es poden reaccionar a les Notes amb diferents emoticones. Les reaccions et permeten expressar matisos que hi són més enllà d'un simple m'agrada." - letsTryReacting: "Es poden afegir reaccions fent clic al botó '+'. Prova reaccionant a aquesta nota!" + letsTryReacting: "Es poden afegir reaccions fent clic al botó '{reaction}'. Prova reaccionant a aquesta nota!" reactToContinue: "Afegeix una reacció per continuar." reactNotification: "Rebràs notificacions en temps real quan un usuari reaccioni a les teves notes." - reactDone: "Pots desfer una reacció fent clic al botó '-'." + reactDone: "Pots desfer una reacció fent clic al botó '{undo}'." _timeline: title: "El concepte de les línies de temps" description1: "Misskey mostra diferents línies de temps basades en l'ús (algunes poden no estar disponibles depenent de la política del servidor)" @@ -2255,4 +2255,3 @@ _externalResourceInstaller: title: "Paràmetres no vàlids " _reversi: total: "Total" - diff --git a/locales/en-US.yml b/locales/en-US.yml index 80e45c42d8..fe27a131ba 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1335,10 +1335,10 @@ _initialTutorial: _reaction: title: "What are Reactions?" description: "Notes can be reacted to with various emojis. Reactions allow you to express nuances that may not be conveyed with just a 'like.'" - letsTryReacting: "Reactions can be added by clicking the '+' button on the note. Try reacting to this sample note!" + letsTryReacting: "Reactions can be added by clicking the '{reaction}' button on the note. Try reacting to this sample note!" reactToContinue: "Add a reaction to proceed." reactNotification: "You'll receive real-time notifications when someone reacts to your note." - reactDone: "You can undo a reaction by pressing the '-' button." + reactDone: "You can undo a reaction by pressing the '{undo}' button." _timeline: title: "The Concept of Timelines" description1: "Sharkey provides multiple timelines based on usage (some may not be available depending on the server's policies)." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 2288038e64..08fed63f5d 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1263,10 +1263,10 @@ _initialTutorial: _reaction: title: "¿Qué son las reacciones?" description: "Se puede reaccionar a las Notas con diferentes emojis. Las reacciones te permiten expresar matices que no se pueden transmitir con un simple 'me gusta'." - letsTryReacting: "Puedes añadir reacciones pulsando en el botón '+' de la nota. ¡Intenta reaccionar a esta nota de ejemplo!" + letsTryReacting: "Puedes añadir reacciones pulsando en el botón '{reaction}' de la nota. ¡Intenta reaccionar a esta nota de ejemplo!" reactToContinue: "Añade una reacción para continuar." reactNotification: "Recibirás notificaciones en tiempo real cuando alguien reaccione a tu nota." - reactDone: "Puedes deshacer una reacción pulsando en el botón '-'." + reactDone: "Puedes deshacer una reacción pulsando en el botón '{undo}'." _timeline: title: "El concepto de Línea de tiempo" description1: "Misskey proporciona múltiples líneas de tiempo basadas en su uso (algunas pueden no estar disponibles dependiendo de las políticas de la instancia)." @@ -2449,4 +2449,3 @@ _reversi: reversi: "Reversi" won: "{name} ha ganado" total: "Total" - diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 24243c1f18..6a32f75231 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1245,10 +1245,10 @@ _initialTutorial: _reaction: title: "Qu'est-ce que les réactions ?" description: "Vous pouvez ajouter des « réactions » aux notes. Les réactions vous permettent d'exprimer à l'aise des nuances qui ne peuvent pas être exprimées par des mentions j'aime." - letsTryReacting: "Des réactions peuvent être ajoutées en cliquant sur le bouton « + » de la note. Essayez d'ajouter une réaction à cet exemple de note !" + letsTryReacting: "Des réactions peuvent être ajoutées en cliquant sur le bouton « {reaction} » de la note. Essayez d'ajouter une réaction à cet exemple de note !" reactToContinue: "Ajoutez une réaction pour procéder." reactNotification: "Vous recevez des notifications en temps réel lorsque quelqu'un réagit à votre note." - reactDone: "Vous pouvez annuler la réaction en cliquant sur le bouton « - » ." + reactDone: "Vous pouvez annuler la réaction en cliquant sur le bouton « {undo} » ." _timeline: title: "Fonctionnement des fils" description1: "Misskey offre plusieurs fils selon l'usage (certains peuvent être désactivés par le serveur)." @@ -2140,4 +2140,3 @@ _dataSaver: description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données." _reversi: total: "Total" - diff --git a/locales/index.d.ts b/locales/index.d.ts index d3a4424c2a..fe24a2c44c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5378,9 +5378,9 @@ export interface Locale extends ILocale { */ "description": string; /** - * リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください! + * リアクションは、ノートの「{reaction}」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください! */ - "letsTryReacting": string; + "letsTryReacting": ParameterizedString<"reaction">; /** * リアクションをつけると先に進めるようになります。 */ @@ -5390,9 +5390,9 @@ export interface Locale extends ILocale { */ "reactNotification": string; /** - * 「ー」ボタンを押すとリアクションを取り消すことができます。 + * 「{undo}」ボタンを押すとリアクションを取り消すことができます。 */ - "reactDone": string; + "reactDone": ParameterizedString<"undo">; }; "_timeline": { /** diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 7414e083d0..3868eea9e3 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1275,10 +1275,10 @@ _initialTutorial: _reaction: title: "Cosa sono le Reazioni?" description: "Puoi reagire alle Note. Le sensazioni che non si riescono a trasmettere con i \"Mi piace\" si possono esprimere facilmente inviando una reazione." - letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"+\" (più) della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!" + letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"{reaction}\" della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!" reactToContinue: "Aggiungere la Reazione ti consentirà di procedere col tutorial." reactNotification: "Quando qualcuno reagisce alle tue Note, ricevi una notifica in tempo reale." - reactDone: "Puoi annullare la tua Reazione premendo il bottone \"ー\" (meno)" + reactDone: "Puoi annullare la tua Reazione premendo il bottone \"{undo}\"" _timeline: title: "Come funziona la Timeline" description1: "Misskey fornisce alcune Timeline (sequenze cronologiche di Note). Una di queste potrebbe essere stata disattivata dagli amministratori." @@ -2509,4 +2509,3 @@ _reversi: _offlineScreen: title: "Scollegato. Impossibile connettersi al server" header: "Impossibile connettersi al server" - diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 597b420c39..28fac61731 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1349,10 +1349,10 @@ _initialTutorial: _reaction: title: "リアクションって何?" description: "ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。" - letsTryReacting: "リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!" + letsTryReacting: "リアクションは、ノートの「{reaction}」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!" reactToContinue: "リアクションをつけると先に進めるようになります。" reactNotification: "あなたのノートが誰かにリアクションされると、リアルタイムで通知を受け取ります。" - reactDone: "「ー」ボタンを押すとリアクションを取り消すことができます。" + reactDone: "「{undo}」ボタンを押すとリアクションを取り消すことができます。" _timeline: title: "タイムラインのしくみ" description1: "Sharkeyには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index d4c7eb0918..edae188bb6 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1265,10 +1265,10 @@ _initialTutorial: _reaction: title: "ツッコミってなんや?" description: "ノートには「ツッコミ」できんねん。「いいね」とか何言っとるかわからんし、簡単に表現できるのはええことやん?" - letsTryReacting: "ノートの「+」ボタンでツッコめるわ。試しに下のノートにツッコんでみ。" + letsTryReacting: "ノートの「{reaction}」ボタンでツッコめるわ。試しに下のノートにツッコんでみ。" reactToContinue: "ツッコんだら進めるようになるで。" reactNotification: "あんたのノートが誰かにツッコまれたら、すぐ通知するで。" - reactDone: "「ー」ボタンでツッコミやめれるで。" + reactDone: "「{undo}」ボタンでツッコミやめれるで。" _timeline: title: "タイムラインのしくみ" description1: "Sharkeyには、いろいろタイムラインがあんで(ただ、サーバーによっては無効化されてるところもあるな)。" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 877ae6b217..39a186a7f8 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1271,10 +1271,10 @@ _initialTutorial: _reaction: title: "'리액션'이 무엇인가요?" description: "노트에 '리액션'을 보낼 수 있습니다. '좋아요'만으로는 충분히 전해지지 않는 감정을, 이모지에 실어서 가볍게 보낼 수 있습니다." - letsTryReacting: "리액션은 노트의 '+' 버튼을 클릭하여 붙일 수 있습니다. 지금 표시되는 샘플 노트에 리액션을 달아 보세요!" + letsTryReacting: "리액션은 노트의 '{reaction}' 버튼을 클릭하여 붙일 수 있습니다. 지금 표시되는 샘플 노트에 리액션을 달아 보세요!" reactToContinue: "다음으로 진행하려면 리액션을 보내세요." reactNotification: "누군가가 나의 노트에 리액션을 보내면 실시간으로 알림을 받게 됩니다." - reactDone: "'-' 버튼을 눌러서 리액션을 취소할 수 있습니다." + reactDone: "'{undo}' 버튼을 눌러서 리액션을 취소할 수 있습니다." _timeline: title: "타임라인에 대하여" description1: "Misskey에는 종류에 따라 여러 가지의 타임라인으로 구성되어 있습니다.(서버에 따라서는 일부 타임라인을 사용할 수 없는 경우가 있습니다)" @@ -2505,4 +2505,3 @@ _reversi: _offlineScreen: title: "오프라인 - 서버에 접속할 수 없습니다" header: "서버에 접속할 수 없습니다" - diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 3f9fa5f5f1..b7566aaa46 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -73,7 +73,7 @@ exportRequested: "Zażądałeś eksportu. Może to zająć trochę czasu. Po zak importRequested: "Zażądano importu. Może to zająć chwilę." lists: "Listy" noLists: "Nie masz żadnych list" -note: "Utwórz wpis" +note: "Wpis" notes: "Wpisy" following: "Obserwowani" followers: "Obserwujący" @@ -1400,4 +1400,3 @@ _moderationLogTypes: resetPassword: "Zresetuj hasło" _reversi: total: "Łącznie" - diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 60c34d84b9..f0ddeab822 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1285,10 +1285,10 @@ _initialTutorial: _reaction: title: "รีแอคชั่นคืออะไร?" description: "โน้ตสามารถ“รีแอคชั่น”ด้วยเอโมจิต่างๆ ซึ่งทำให้สามารถแสดงความแตกต่างเล็กๆ น้อยๆ ที่อาจไม่สามารถสื่อออกมาได้ด้วยการแค่การกด “ถูกใจ”" - letsTryReacting: "คุณสามารถเพิ่มรีแอคชั่นได้ด้วยการคลิกปุ่ม “+” บนโน้ต ลองรีแอคชั่นโน้ตตัวอย่างนี้ดูสิ!" + letsTryReacting: "คุณสามารถเพิ่มรีแอคชั่นได้ด้วยการคลิกปุ่ม “{reaction}” บนโน้ต ลองรีแอคชั่นโน้ตตัวอย่างนี้ดูสิ!" reactToContinue: "เพิ่มรีแอคชั่นเพื่อดำเนินการต่อ" reactNotification: "คุณจะได้รับการแจ้งเตือนแบบเรียลไทม์เมื่อมีคนตอบรีแอคชั่นโน้ตของคุณ" - reactDone: "คุณสามารถยกเลิกรีแอคชั่นได้โดยการกดปุ่ม “-”" + reactDone: "คุณสามารถยกเลิกรีแอคชั่นได้โดยการกดปุ่ม “{undo}”" _timeline: title: "แนวคิดเรื่องของไทม์ไลน์" description1: "Misskey มีหลายไทม์ไลน์ขึ้นอยู่กับวิธีการใช้งานของคุณ (บางไทม์ไลน์อาจไม่สามารถใช้ได้ขึ้นอยู่กับนโยบายของเซิร์ฟเวอร์)" @@ -2524,4 +2524,3 @@ _reversi: _offlineScreen: title: "ออฟไลน์ - ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" header: "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" - diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 17ad6e7150..ddd8914146 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1284,10 +1284,10 @@ _initialTutorial: _reaction: title: "什么是回应?" description: "您可以在帖子中添加“回应”。 您可以使用反应轻松地表达点“赞”所无法传达的细微差别。" - letsTryReacting: "回应可以通过点击帖子中的「+」按钮来添加。试着给这个示例帖子添加一个回应!" + letsTryReacting: "回应可以通过点击帖子中的「{reaction}」按钮来添加。试着给这个示例帖子添加一个回应!" reactToContinue: "添加一个回应来继续" reactNotification: "当您的帖子被某人添加了回应时,将实时收到通知。" - reactDone: "通过按下「ー」按钮,可以取消已经添加的回应" + reactDone: "通过按下「{undo}」按钮,可以取消已经添加的回应" _timeline: title: "时间线的运作方式" description1: "Misskey 根据使用方式提供了多个时间线(根据服务器的设定,可能有一些被禁用)。" @@ -2519,4 +2519,3 @@ _reversi: _offlineScreen: title: "离线——无法连接到服务器" header: "无法连接到服务器" - diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 5cdecc10ac..f8bdd002b4 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1285,10 +1285,10 @@ _initialTutorial: _reaction: title: "什麼是反應?" description: "您可以在貼文中添加「反應」。您可以使用反應輕鬆隨意地表達「最愛/大心」所無法傳達的細微差別。" - letsTryReacting: "可以透過點擊貼文上的「+」按鈕來添加反應。請嘗試在此範例貼文添加反應!" + letsTryReacting: "可以透過點擊貼文上的「{reaction}」按鈕來添加反應。請嘗試在此範例貼文添加反應!" reactToContinue: "添加反應以繼續教學課程。" reactNotification: "當有人對您的貼文做出反應時會即時接收到通知。" - reactDone: "按下「-」按鈕可以取消反應。" + reactDone: "按下「{undo}」按鈕可以取消反應。" _timeline: title: "時間軸如何運作" description1: "Misskey根據使用方式提供了多個時間軸(伺服器可能會將部份時間軸停用)。" @@ -2524,4 +2524,3 @@ _reversi: _offlineScreen: title: "離線-無法連接伺服器" header: "無法連接伺服器" - diff --git a/package.json b/package.json index 4d823d3123..7791dfb949 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.3.2-devel", + "version": "2024.3.3-devel", "codename": "shonk", "repository": { "type": "git", diff --git a/packages/backend/package.json b/packages/backend/package.json index 19d9bab1e9..4393b06d1d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -88,7 +88,7 @@ "@smithy/node-http-handler": "2.1.10", "@swc/cli": "0.1.63", "@swc/core": "1.3.107", - "@transfem-org/sfm-js": "0.24.4", + "@transfem-org/sfm-js": "0.24.5", "@twemoji/parser": "15.0.0", "accepts": "1.3.8", "ajv": "8.12.0", diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index dbd5db21a8..34017f015a 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -430,11 +430,16 @@ export class NoteEditService implements OnApplicationShutdown { update.hasPoll = !!data.poll; } + // technically we should check if the two sets of files are + // different, or if their descriptions have changed. In practice + // this is good enough. + const filesChanged = oldnote.fileIds?.length || data.files?.length; + const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id }); const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null; - if (Object.keys(update).length > 0) { + if (Object.keys(update).length > 0 || filesChanged) { const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id }); await this.noteEditRepository.insert({ diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 90586b500e..c0b59e635d 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -64,8 +64,8 @@ type DecodedReaction = { host?: string | null; }; -const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/; -const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/; +const isCustomEmojiRegexp = /^:([\p{Letter}\p{Number}\p{Mark}_+-]+)(?:@\.)?:$/u; +const decodeCustomEmojiRegexp = /^:([\p{Letter}\p{Number}\p{Mark}_+-]+)(?:@([\w.-]+))?:$/u; @Injectable() export class ReactionService { diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index a84feffac6..38175b2059 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -31,6 +31,7 @@ import { IdService } from '@/core/IdService.js'; import { MetaService } from '../MetaService.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; +import { CONTEXT } from './misc/contexts.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; @Injectable() @@ -283,9 +284,10 @@ export class ApRendererService { if (instance && instance.softwareName === 'mastodon') isMastodon = true; if (instance && instance.softwareName === 'akkoma') isMastodon = true; if (instance && instance.softwareName === 'pleroma') isMastodon = true; + if (instance && instance.softwareName === 'iceshrimp.net') isMastodon = true; } } - + const object: ILike = { type: 'Like', id: `${this.config.url}/likes/${noteReaction.id}`, @@ -785,48 +787,7 @@ export class ApRendererService { x.id = `${this.config.url}/${randomUUID()}`; } - return Object.assign({ - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - { - Key: 'sec:Key', - // as non-standards - manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', - sensitive: 'as:sensitive', - Hashtag: 'as:Hashtag', - quoteUrl: 'as:quoteUrl', - fedibird: 'http://fedibird.com/ns#', - quoteUri: 'fedibird:quoteUri', - // Mastodon - toot: 'http://joinmastodon.org/ns#', - Emoji: 'toot:Emoji', - featured: 'toot:featured', - discoverable: 'toot:discoverable', - // schema - schema: 'http://schema.org#', - PropertyValue: 'schema:PropertyValue', - value: 'schema:value', - // Misskey - misskey: 'https://misskey-hub.net/ns#', - '_misskey_content': 'misskey:_misskey_content', - '_misskey_quote': 'misskey:_misskey_quote', - '_misskey_reaction': 'misskey:_misskey_reaction', - '_misskey_votes': 'misskey:_misskey_votes', - '_misskey_summary': 'misskey:_misskey_summary', - 'isCat': 'misskey:isCat', - // Firefish - firefish: 'https://joinfirefish.org/ns#', - speakAsCat: 'firefish:speakAsCat', - // Sharkey - sharkey: 'https://joinsharkey.org/ns#', - backgroundUrl: 'sharkey:backgroundUrl', - listenbrainz: 'sharkey:listenbrainz', - // vcard - vcard: 'http://www.w3.org/2006/vcard/ns#', - }, - ], - }, x as T & { id: string }); + return Object.assign({ '@context': CONTEXT }, x as T & { id: string }); } @bindThis diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index 9de184336f..2ec6dc4585 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -7,7 +7,7 @@ import * as crypto from 'node:crypto'; import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; -import { CONTEXTS } from './misc/contexts.js'; +import { CONTEXT, CONTEXTS } from './misc/contexts.js'; import { validateContentTypeSetAsJsonLD } from './misc/validator.js'; import type { JsonLdDocument } from 'jsonld'; import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js'; @@ -88,6 +88,16 @@ class LdSignature { return verifyData; } + @bindThis + public async compact(data: any, context: any = CONTEXT): Promise { + const customLoader = this.getLoader(); + // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically + // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 + return (await import('jsonld')).default.compact(data, context, { + documentLoader: customLoader, + }); + } + @bindThis public async normalize(data: JsonLdDocument): Promise { const customLoader = this.getLoader(); diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index 88afdefcd3..4ff114bbf5 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { JsonLd } from 'jsonld/jsonld-spec.js'; +import type { Context, JsonLd } from 'jsonld/jsonld-spec.js'; /* eslint:disable:quotemark indent */ const id_v1 = { @@ -526,6 +526,50 @@ const activitystreams = { }, } satisfies JsonLd; +const context_iris = [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', +]; + +const extension_context_definition = { + Key: 'sec:Key', + // as non-standards + manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', + sensitive: 'as:sensitive', + Hashtag: 'as:Hashtag', + quoteUrl: 'as:quoteUrl', + fedibird: 'http://fedibird.com/ns#', + quoteUri: 'fedibird:quoteUri', + // Mastodon + toot: 'http://joinmastodon.org/ns#', + Emoji: 'toot:Emoji', + featured: 'toot:featured', + discoverable: 'toot:discoverable', + // schema + schema: 'http://schema.org#', + PropertyValue: 'schema:PropertyValue', + value: 'schema:value', + // Misskey + misskey: 'https://misskey-hub.net/ns#', + '_misskey_content': 'misskey:_misskey_content', + '_misskey_quote': 'misskey:_misskey_quote', + '_misskey_reaction': 'misskey:_misskey_reaction', + '_misskey_votes': 'misskey:_misskey_votes', + '_misskey_summary': 'misskey:_misskey_summary', + 'isCat': 'misskey:isCat', + // Firefish + firefish: 'https://joinfirefish.org/ns#', + speakAsCat: 'firefish:speakAsCat', + // Sharkey + sharkey: 'https://joinsharkey.org/ns#', + backgroundUrl: 'sharkey:backgroundUrl', + listenbrainz: 'sharkey:listenbrainz', + // vcard + vcard: 'http://www.w3.org/2006/vcard/ns#', +} satisfies Context; + +export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition]; + export const CONTEXTS: Record = { 'https://w3id.org/identity/v1': id_v1, 'https://w3id.org/security/v1': security_v1, diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts index d148fc629b..840522ae9b 100644 --- a/packages/backend/src/core/chart/charts/users.ts +++ b/packages/backend/src/core/chart/charts/users.ts @@ -4,7 +4,7 @@ */ import { Injectable, Inject } from '@nestjs/common'; -import { Not, IsNull, DataSource } from 'typeorm'; +import { Not, IsNull, Like, DataSource } from 'typeorm'; import type { MiUser } from '@/models/User.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; @@ -37,7 +37,10 @@ export default class UsersChart extends Chart { // eslint-disable protected async tickMajor(): Promise>> { const [localCount, remoteCount] = await Promise.all([ - this.usersRepository.countBy({ host: IsNull() }), + // that Not(Like()) is ugly, but it matches the logic in + // packages/backend/src/models/User.ts to not count "system" + // accounts + this.usersRepository.countBy({ host: IsNull(), username: Not(Like('%.%')) }), this.usersRepository.countBy({ host: Not(IsNull()) }), ]); diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index 728fc9e72b..4fa414b0b5 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -33,6 +33,12 @@ export class CleanRemoteFilesProcessorService { let deletedCount = 0; let cursor: MiDriveFile['id'] | null = null; + let errorCount = 0; + + const total = await this.driveFilesRepository.countBy({ + userHost: Not(IsNull()), + isLink: false, + }); while (true) { const files = await this.driveFilesRepository.find({ @@ -41,7 +47,7 @@ export class CleanRemoteFilesProcessorService { isLink: false, ...(cursor ? { id: MoreThan(cursor) } : {}), }, - take: 8, + take: 256, order: { id: 1, }, @@ -54,18 +60,22 @@ export class CleanRemoteFilesProcessorService { cursor = files.at(-1)?.id ?? null; - await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true))); + // Handle deletion in a batch + const results = await Promise.allSettled(files.map(file => this.driveService.deleteFileSync(file, true))); - deletedCount += 8; - - const total = await this.driveFilesRepository.countBy({ - userHost: Not(IsNull()), - isLink: false, + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + deletedCount++; + } else { + this.logger.error(`Failed to delete file ID ${files[index].id}: ${result.reason}`); + errorCount++; + } }); - job.updateProgress(100 / total * deletedCount); + await job.updateProgress(100 / total * deletedCount); + } - this.logger.succ('All cached remote files has been deleted.'); + this.logger.succ(`All cached remote files processed. Total deleted: ${deletedCount}, Failed: ${errorCount}.`); } } diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index e4eb4791bd..45087927a5 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -85,7 +85,7 @@ export class ExportCustomEmojisProcessorService { }); for (const emoji of customEmojis) { - if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) { + if (!/^[\p{Letter}\p{Number}\p{Mark}_+-]+$/u.test(emoji.name)) { this.logger.error(`invalid emoji name: ${emoji.name}`); continue; } diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 171809d25c..04ad74ee01 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -79,13 +79,14 @@ export class ImportCustomEmojisProcessorService { continue; } const emojiInfo = record.emoji; - if (!/^[a-zA-Z0-9_]+$/.test(emojiInfo.name)) { - this.logger.error(`invalid emojiname: ${emojiInfo.name}`); + const nameNfc = emojiInfo.name.normalize('NFC'); + if (!/^[\p{Letter}\p{Number}\p{Mark}_+-]+$/u.test(nameNfc)) { + this.logger.error(`invalid emojiname: ${nameNfc}`); continue; } const emojiPath = outputPath + '/' + record.fileName; await this.emojisRepository.delete({ - name: emojiInfo.name, + name: nameNfc, }); const driveFile = await this.driveService.addFile({ user: null, @@ -94,10 +95,10 @@ export class ImportCustomEmojisProcessorService { force: true, }); await this.customEmojiService.add({ - name: emojiInfo.name, - category: emojiInfo.category, + name: nameNfc, + category: emojiInfo.category?.normalize('NFC'), host: null, - aliases: emojiInfo.aliases, + aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')), driveFile, license: emojiInfo.license, isSensitive: emojiInfo.isSensitive, diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index ad1d9799a7..2b5b7c5619 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -15,6 +15,7 @@ import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import FederationChart from '@/core/chart/charts/federation.js'; import { getApId } from '@/core/activitypub/type.js'; +import type { IActivity } from '@/core/activitypub/type.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; @@ -52,7 +53,7 @@ export class InboxProcessorService { @bindThis public async process(job: Bull.Job): Promise { const signature = job.data.signature; // HTTP-signature - const activity = job.data.activity; + let activity = job.data.activity; //#region Log const info = Object.assign({}, activity); @@ -150,6 +151,17 @@ export class InboxProcessorService { throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); } + // アクティビティを正規化 + delete activity.signature; + try { + activity = await ldSignature.compact(activity) as IActivity; + } catch (e) { + throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`); + } + // TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする + // https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29 + activity.signature = ldSignature; + // もう一度actorチェック if (authUser.user.uri !== activity.actor) { throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index a30a080e59..f4fc79bdb3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -34,7 +34,7 @@ export default class extends Endpoint { // eslint- private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases); + await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC'))); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 767e517b80..b45a3c7156 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -40,7 +40,7 @@ export const meta = { export const paramDef = { type: 'object', properties: { - name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + name: { type: 'string', pattern: '^[\\p{Letter}\\p{Number}\\p{Mark}_+-]+$' }, fileId: { type: 'string', format: 'misskey:id' }, category: { type: 'string', @@ -73,18 +73,19 @@ export default class extends Endpoint { // eslint- private emojiEntityService: EmojiEntityService, ) { super(meta, paramDef, async (ps, me) => { + const nameNfc = ps.name.normalize('NFC'); const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); - const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc); if (isDuplicate) throw new ApiError(meta.errors.duplicateName); if (driveFile.user !== null) await this.driveFilesRepository.update(driveFile.id, { user: null }); const emoji = await this.customEmojiService.add({ driveFile, - name: ps.name, - category: ps.category ?? null, - aliases: ps.aliases ?? [], + name: nameNfc, + category: ps.category?.normalize('NFC') ?? null, + aliases: ps.aliases?.map(a => a.normalize('NFC')) ?? [], host: null, license: ps.license ?? null, isSensitive: ps.isSensitive ?? false, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 29af7598ed..f968813197 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -82,15 +82,16 @@ export default class extends Endpoint { // eslint- throw new ApiError(); } + const nameNfc = emoji.name.normalize('NFC'); // Duplication Check - const isDuplicate = await this.customEmojiService.checkDuplicate(emoji.name); + const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc); if (isDuplicate) throw new ApiError(meta.errors.duplicateName); const addedEmoji = await this.customEmojiService.add({ driveFile, - name: emoji.name, - category: emoji.category, - aliases: emoji.aliases, + name: nameNfc, + category: emoji.category?.normalize('NFC'), + aliases: emoji.aliases?.map(a => a.normalize('NFC')), host: null, license: emoji.license, isSensitive: emoji.isSensitive, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index e423f440d0..1182918ea2 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -98,7 +98,7 @@ export default class extends Endpoint { // eslint- } if (ps.query) { - q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }) + q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query.normalize('NFC')) + '%' }) .orderBy('length(emoji.name)', 'ASC'); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 53810d1d16..5e21111f9f 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -92,17 +92,18 @@ export default class extends Endpoint { // eslint- //const emojis = await q.limit(ps.limit).getMany(); emojis = await q.orderBy('length(emoji.name)', 'ASC').getMany(); - const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g); + const queryarry = ps.query.match(/:([\p{Letter}\p{Number}\p{Mark}_+-]*):/ug); if (queryarry) { emojis = emojis.filter(emoji => - queryarry.includes(`:${emoji.name}:`), + queryarry.includes(`:${emoji.name.normalize('NFC')}:`), ); } else { + const queryNfc = ps.query!.normalize('NFC'); emojis = emojis.filter(emoji => - emoji.name.includes(ps.query!) || - emoji.aliases.some(a => a.includes(ps.query!)) || - emoji.category?.includes(ps.query!)); + emoji.name.includes(queryNfc) || + emoji.aliases.some(a => a.includes(queryNfc)) || + emoji.category?.includes(queryNfc)); } emojis.splice(ps.limit + 1); } else { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index 0fa119eabe..e78620eac1 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -34,7 +34,7 @@ export default class extends Endpoint { // eslint- private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases); + await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC'))); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index d9ee18699c..69fc8e0cb5 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -34,7 +34,7 @@ export default class extends Endpoint { // eslint- private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases); + await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC'))); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index dc25df2767..679a9f9c42 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -36,7 +36,7 @@ export default class extends Endpoint { // eslint- private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.customEmojiService.setCategoryBulk(ps.ids, ps.category ?? null); + await this.customEmojiService.setCategoryBulk(ps.ids, ps.category?.normalize('NFC') ?? null); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 22609a16a3..3caa0f84a3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -40,7 +40,7 @@ export const paramDef = { type: 'object', properties: { id: { type: 'string', format: 'misskey:id' }, - name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + name: { type: 'string', pattern: '^[\\p{Letter}\\p{Number}\\p{Mark}_+-]+$' }, fileId: { type: 'string', format: 'misskey:id' }, category: { type: 'string', @@ -72,6 +72,7 @@ export default class extends Endpoint { // eslint- private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { + const nameNfc = ps.name?.normalize('NFC'); let driveFile; if (ps.fileId) { driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); @@ -83,22 +84,22 @@ export default class extends Endpoint { // eslint- emojiId = ps.id; const emoji = await this.customEmojiService.getEmojiById(ps.id); if (!emoji) throw new ApiError(meta.errors.noSuchEmoji); - if (ps.name && (ps.name !== emoji.name)) { - const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + if (nameNfc && (nameNfc !== emoji.name)) { + const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc); if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); } } else { - if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.'); - const emoji = await this.customEmojiService.getEmojiByName(ps.name); + if (!nameNfc) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.'); + const emoji = await this.customEmojiService.getEmojiByName(nameNfc); if (!emoji) throw new ApiError(meta.errors.noSuchEmoji); emojiId = emoji.id; } await this.customEmojiService.update(emojiId, { driveFile, - name: ps.name, - category: ps.category, - aliases: ps.aliases, + name: nameNfc, + category: ps.category?.normalize('NFC'), + aliases: ps.aliases?.map(a => a.normalize('NFC')), license: ps.license, isSensitive: ps.isSensitive, localOnly: ps.localOnly, diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 60e25c0dfd..326d3a1d5c 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -278,7 +278,7 @@ export class MastoConverters { reactions: status.emoji_reactions, emoji_reactions: status.emoji_reactions, bookmarked: false, - quote: isQuote ? await this.convertReblog(status.reblog) : false, + quote: isQuote ? await this.convertReblog(status.reblog) : null, // optional chaining cannot be used, as it evaluates to undefined, not null edited_at: note.updatedAt ? note.updatedAt.toISOString() : null, }); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 81f3388f41..2e7ae4fa70 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -21,10 +21,11 @@ "@github/webauthn-json": "2.1.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@misskey-dev/browser-image-resizer": "2024.1.0", + "@phosphor-icons/web": "^2.0.3", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.5", "@rollup/pluginutils": "5.1.0", - "@transfem-org/sfm-js": "0.24.4", + "@transfem-org/sfm-js": "0.24.5", "@syuilo/aiscript": "0.18.0", "@phosphor-icons/web": "^2.0.3", "@twemoji/parser": "15.0.0", diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 8b665bfacd..b04a1c92e7 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -238,7 +238,7 @@ function exec() { return; } - emojis.value = searchEmoji(props.q.toLowerCase(), emojiDb.value); + emojis.value = searchEmoji(props.q.normalize('NFC').toLowerCase(), emojiDb.value); } else if (props.type === 'mfmTag') { if (!props.q || props.q === '') { mfmTags.value = MFM_TAGS; diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 1219a29d85..53fca97fc0 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -205,7 +205,7 @@ watch(q, () => { return; } - const newQ = q.value.replace(/:/g, '').toLowerCase(); + const newQ = q.value.replace(/:/g, '').normalize('NFC').toLowerCase(); const searchCustom = () => { const max = 100; diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue index f3305e9f54..bab844c27f 100644 --- a/packages/frontend/src/components/MkFileListForAdmin.vue +++ b/packages/frontend/src/components/MkFileListForAdmin.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -43,6 +43,7 @@ import { claimAchievement } from '@/scripts/achievements.js'; import { getScrollContainer } from '@/scripts/scroll.js'; import { useRouterFactory } from '@/router/supplier.js'; import { mainRouter } from '@/router/main.js'; +import MkUserName from './global/MkUserName.vue'; const props = defineProps<{ initialPath: string; diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 62a85389ad..6f6007d432 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -395,10 +395,10 @@ const prepend = (item: MisskeyEntity): void => { * @param newItems 新しいアイテムの配列 */ function unshiftItems(newItems: MisskeyEntity[]) { - const length = newItems.length + items.value.size; - items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, props.displayLimit)); - - if (length >= props.displayLimit) more.value = true; + const prevLength = items.value.size; + items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, newItems.length + props.displayLimit)); + // if we truncated, mark that there are more values to fetch + if (items.value.size < prevLength) more.value = true; } /** @@ -406,10 +406,10 @@ function unshiftItems(newItems: MisskeyEntity[]) { * @param oldItems 古いアイテムの配列 */ function concatItems(oldItems: MisskeyEntity[]) { - const length = oldItems.length + items.value.size; - items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, props.displayLimit)); - - if (length >= props.displayLimit) more.value = true; + const prevLength = items.value.size; + items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, oldItems.length + props.displayLimit)); + // if we truncated, mark that there are more values to fetch + if (items.value.size < prevLength) more.value = true; } function executeQueue() { @@ -418,7 +418,7 @@ function executeQueue() { } function prependQueue(newItem: MisskeyEntity) { - queue.value = new Map([[newItem.id, newItem], ...queue.value].slice(0, props.displayLimit) as [string, MisskeyEntity][]); + queue.value = new Map([[newItem.id, newItem], ...queue.value] as [string, MisskeyEntity][]); } /* diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index 18a9eeda23..c2435b308f 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, ref } from 'vue'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; -import sanitizeHtml from 'sanitize-html'; +import sanitizeHtml from '@/scripts/sanitize-html.js'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue index 57216058fd..725cfcdc33 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Note.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -16,9 +16,20 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._initialTutorial._reaction.description }}
-
{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}
+ + + -
{{ i18n.ts._initialTutorial.wellDone }} {{ i18n.ts._initialTutorial._reaction.reactNotification }}
{{ i18n.ts._initialTutorial._reaction.reactDone }}
+
+ {{ i18n.ts._initialTutorial.wellDone }} {{ i18n.ts._initialTutorial._reaction.reactNotification }}
+ + + +
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue index 17a9254d01..ac82ecc3d6 100644 --- a/packages/frontend/src/components/MkUserList.vue +++ b/packages/frontend/src/components/MkUserList.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only -->