mirror of
https://activitypub.software/TransFem-org/Sharkey
synced 2024-11-25 07:25:12 +00:00
merge: Release 2024.3.3 (!501)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/501 Approved-by: Amelia Yukii <amelia.yukii@shourai.de> Approved-by: Marie <marie@kaifa.ch>
This commit is contained in:
commit
3d6eb18e2a
91 changed files with 432 additions and 214 deletions
|
@ -4,7 +4,7 @@ stages:
|
|||
|
||||
testCommit:
|
||||
stage: test
|
||||
image: node:latest
|
||||
image: node:iron
|
||||
services:
|
||||
- postgres:15
|
||||
- redis
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)."
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
@ -5334,9 +5334,9 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"description": string;
|
||||
/**
|
||||
* リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!
|
||||
* リアクションは、ノートの「{reaction}」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!
|
||||
*/
|
||||
"letsTryReacting": string;
|
||||
"letsTryReacting": ParameterizedString<"reaction">;
|
||||
/**
|
||||
* リアクションをつけると先に進めるようになります。
|
||||
*/
|
||||
|
@ -5346,9 +5346,9 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"reactNotification": string;
|
||||
/**
|
||||
* 「ー」ボタンを押すとリアクションを取り消すことができます。
|
||||
* 「{undo}」ボタンを押すとリアクションを取り消すことができます。
|
||||
*/
|
||||
"reactDone": string;
|
||||
"reactDone": ParameterizedString<"undo">;
|
||||
};
|
||||
"_timeline": {
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -1338,10 +1338,10 @@ _initialTutorial:
|
|||
_reaction:
|
||||
title: "リアクションって何?"
|
||||
description: "ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。"
|
||||
letsTryReacting: "リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!"
|
||||
letsTryReacting: "リアクションは、ノートの「{reaction}」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!"
|
||||
reactToContinue: "リアクションをつけると先に進めるようになります。"
|
||||
reactNotification: "あなたのノートが誰かにリアクションされると、リアルタイムで通知を受け取ります。"
|
||||
reactDone: "「ー」ボタンを押すとリアクションを取り消すことができます。"
|
||||
reactDone: "「{undo}」ボタンを押すとリアクションを取り消すことができます。"
|
||||
_timeline:
|
||||
title: "タイムラインのしくみ"
|
||||
description1: "Sharkeyには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。"
|
||||
|
|
|
@ -1265,10 +1265,10 @@ _initialTutorial:
|
|||
_reaction:
|
||||
title: "ツッコミってなんや?"
|
||||
description: "ノートには「ツッコミ」できんねん。「いいね」とか何言っとるかわからんし、簡単に表現できるのはええことやん?"
|
||||
letsTryReacting: "ノートの「+」ボタンでツッコめるわ。試しに下のノートにツッコんでみ。"
|
||||
letsTryReacting: "ノートの「{reaction}」ボタンでツッコめるわ。試しに下のノートにツッコんでみ。"
|
||||
reactToContinue: "ツッコんだら進めるようになるで。"
|
||||
reactNotification: "あんたのノートが誰かにツッコまれたら、すぐ通知するで。"
|
||||
reactDone: "「ー」ボタンでツッコミやめれるで。"
|
||||
reactDone: "「{undo}」ボタンでツッコミやめれるで。"
|
||||
_timeline:
|
||||
title: "タイムラインのしくみ"
|
||||
description1: "Sharkeyには、いろいろタイムラインがあんで(ただ、サーバーによっては無効化されてるところもあるな)。"
|
||||
|
|
|
@ -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: "서버에 접속할 수 없습니다"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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: "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้"
|
||||
|
||||
|
|
|
@ -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: "无法连接到服务器"
|
||||
|
||||
|
|
|
@ -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: "無法連接伺服器"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sharkey",
|
||||
"version": "2024.3.2",
|
||||
"version": "2024.3.3",
|
||||
"codename": "shonk",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -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",
|
||||
|
@ -172,7 +172,7 @@
|
|||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.22.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.2",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.8",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.20",
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,6 +284,7 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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<JsonLdDocument> {
|
||||
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<string> {
|
||||
const customLoader = this.getLoader();
|
||||
|
|
|
@ -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<string, JsonLd> = {
|
||||
'https://w3id.org/identity/v1': id_v1,
|
||||
'https://w3id.org/security/v1': security_v1,
|
||||
|
|
|
@ -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<typeof schema> { // eslint-disable
|
|||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
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()) }),
|
||||
]);
|
||||
|
||||
|
|
|
@ -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(deletedCount / total);
|
||||
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}.`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<InboxJobData>): Promise<string> {
|
||||
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})`);
|
||||
|
|
|
@ -192,6 +192,7 @@ export class FileServerService {
|
|||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||
reply.header('Accept-Ranges', 'bytes');
|
||||
reply.header('Content-Length', chunksize);
|
||||
reply.code(206);
|
||||
} else {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
|
@ -261,7 +262,6 @@ export class FileServerService {
|
|||
const parts = range.replace(/bytes=/, '').split('-');
|
||||
const start = parseInt(parts[0], 10);
|
||||
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
||||
console.log(end);
|
||||
if (end > file.file.size) {
|
||||
end = file.file.size - 1;
|
||||
}
|
||||
|
@ -431,6 +431,7 @@ export class FileServerService {
|
|||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||
reply.header('Accept-Ranges', 'bytes');
|
||||
reply.header('Content-Length', chunksize);
|
||||
reply.code(206);
|
||||
} else {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
|
@ -527,6 +528,9 @@ export class FileServerService {
|
|||
if (!file.storedInternal) {
|
||||
if (!(file.isLink && file.uri)) return '204';
|
||||
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
||||
if (!file.size) {
|
||||
file.size = (await fs.promises.stat(result.path)).size;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
url: file.uri,
|
||||
|
|
|
@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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')));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<typeof meta, typeof paramDef> { // 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,
|
||||
|
|
|
@ -82,15 +82,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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,
|
||||
|
|
|
@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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');
|
||||
}
|
||||
|
||||
|
|
|
@ -92,17 +92,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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 {
|
||||
|
|
|
@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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')));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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')));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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,
|
||||
|
|
|
@ -278,8 +278,9 @@ export class MastoConverters {
|
|||
reactions: status.emoji_reactions,
|
||||
emoji_reactions: status.emoji_reactions,
|
||||
bookmarked: false,
|
||||
quote: isQuote ? await this.convertReblog(status.reblog) : false,
|
||||
edited_at: note.updatedAt?.toISOString(),
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ html
|
|||
link(rel='stylesheet' href='/assets/phosphor-icons/bold/style.css')
|
||||
link(rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css')
|
||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||
script(src='/client-assets/libopenmpt.js')
|
||||
|
||||
if !config.clientManifestExists
|
||||
script(type="module" src="/vite/@vite/client")
|
||||
|
@ -73,7 +72,6 @@ html
|
|||
script.
|
||||
var VERSION = "#{version}";
|
||||
var CLIENT_ENTRY = "#{clientEntry.file}";
|
||||
window.libopenmpt = window.Module;
|
||||
|
||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||
!= metaJson
|
||||
|
|
|
@ -5,8 +5,8 @@ block vars
|
|||
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
|
||||
- const url = `${config.url}/notes/${note.id}`;
|
||||
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
- const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
|
||||
- const videos = (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive)
|
||||
- const images = note.cw ? [] : (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
|
||||
- const videos = note.cw ? [] : (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive)
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -21,12 +21,12 @@
|
|||
"@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",
|
||||
"@syuilo/aiscript": "0.17.0",
|
||||
"@phosphor-icons/web": "^2.0.3",
|
||||
"@transfem-org/sfm-js": "0.24.5",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vue/compiler-sfc": "3.4.21",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" :displayLimit="50" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
||||
<MkA
|
||||
v-for="file in (items as Misskey.entities.DriveFile[])"
|
||||
:key="file.id"
|
||||
|
|
|
@ -55,8 +55,6 @@ import { i18n } from '@/i18n.js';
|
|||
import { infoImageUrl } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
console.log(defaultStore.state.noteDesign, defaultStore.state.noteDesign === 'sharkey');
|
||||
|
||||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
noGap?: boolean;
|
||||
|
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>
|
||||
<template v-if="pageMetadata">
|
||||
<i v-if="pageMetadata.icon" :class="pageMetadata.icon" style="margin-right: 0.5em;"></i>
|
||||
<span>{{ pageMetadata.title }}</span>
|
||||
<span><MkUserName v-if="pageMetadata.userName?.name" :user="pageMetadata.userName" />{{ pageMetadata.title }}</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -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][]);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -16,9 +16,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div v-else-if="phase === 'howToReact'" class="_gaps">
|
||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
|
||||
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
|
||||
<I18n :src="i18n.ts._initialTutorial._reaction.letsTryReacting" tag="div">
|
||||
<template #reaction>
|
||||
<i class="ph-smiley ph-bold ph-lg"></i>
|
||||
</template>
|
||||
</I18n>
|
||||
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
|
||||
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
|
||||
<div v-if="onceReacted">
|
||||
<b style="color: var(--accent);"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>
|
||||
<I18n :src="i18n.ts._initialTutorial._reaction.reactDone">
|
||||
<template #undo>
|
||||
<i class="ph-minus ph-bold ph-lg"></i>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkPagination :pagination="pagination">
|
||||
<MkPagination :pagination="pagination" :displayLimit="50">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
|
|
|
@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sanitizeHtml from '@/scripts/sanitize-html.js';
|
||||
import XSigninDialog from '@/components/MkSigninDialog.vue';
|
||||
import XSignupDialog from '@/components/MkSignupDialog.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
|
|
@ -85,6 +85,7 @@ const errored = ref(url.value == null);
|
|||
|
||||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
ev.stopPropagation();
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: `:${props.name}:`,
|
||||
|
|
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick" v-on:click.stop/>
|
||||
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick" v-on:click.stop>{{ colorizedNativeEmoji }}</span>
|
||||
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -39,6 +39,7 @@ function computeTitle(event: PointerEvent): void {
|
|||
|
||||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
ev.stopPropagation();
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: props.emoji,
|
||||
|
|
|
@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination" :displayLimit="50">
|
||||
<div :class="$style.items">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.item" :to="`/instance-info/${instance.host}`">
|
||||
<MkInstanceCardMini :instance="instance"/>
|
||||
|
|
|
@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sanitizeHtml from '@/scripts/sanitize-html.js';
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XEmojis from './about.emojis.vue';
|
||||
|
|
|
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
-->
|
||||
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);">
|
||||
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps_m">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<SkApprovalUser v-for="item in items" :key="item.id" :user="(item as any)" :onDeleted="deleted"/>
|
||||
|
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination" :displayLimit="50">
|
||||
<div :class="$style.instances">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.instance" :to="`/instance-info/${instance.host}`">
|
||||
<MkInstanceCardMini :instance="instance"/>
|
||||
|
|
|
@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/>
|
||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);">
|
||||
<div class="_gaps_s">
|
||||
<XModLog v-for="item in items" :key="item.id" :log="item"/>
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps">
|
||||
<MkButton primary rounded @click="assign"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.assign }}</MkButton>
|
||||
|
||||
<MkPagination :pagination="usersPagination">
|
||||
<MkPagination :pagination="usersPagination" :displayLimit="50">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination">
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<div :class="$style.users">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/admin/user/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
|
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton inline @click="setLicenseBulk">Set License</MkButton>
|
||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
|
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<MkPagination :pagination="remotePagination" :displayLimit="50">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
|
@ -352,6 +352,7 @@ definePageMetadata(() => ({
|
|||
> .img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
@ -398,6 +399,7 @@ definePageMetadata(() => ({
|
|||
> .img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
|
|
@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton>
|
||||
<MkInput v-model="name" pattern="[a-z0-9_]" autocapitalize="off">
|
||||
<MkInput v-model="name" autocapitalize="off">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="category" :datalist="customEmojiCategories">
|
||||
|
|
|
@ -16,9 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
|
||||
<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'"
|
||||
v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
|
||||
<MkNote :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</MkDateSeparatedList>
|
||||
<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'sharkey'"
|
||||
v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
|
||||
<SkNote :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
|
@ -28,10 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import SkNote from '@/components/SkNote.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'i/favorites' as const,
|
||||
|
|
|
@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_m">
|
||||
<MkSelect v-model="type">
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="following" v-if="hasSender">{{ i18n.ts.following }}</option>
|
||||
<option value="follower" v-if="hasSender">{{ i18n.ts.followers }}</option>
|
||||
<option value="mutualFollow" v-if="hasSender">{{ i18n.ts.mutualFollow }}</option>
|
||||
<option value="followingOrFollower" v-if="hasSender">{{ i18n.ts.followingOrFollower }}</option>
|
||||
<option value="list" v-if="hasSender">{{ i18n.ts.userList }}</option>
|
||||
<option v-if="hasSender" value="following">{{ i18n.ts.following }}</option>
|
||||
<option v-if="hasSender" value="follower">{{ i18n.ts.followers }}</option>
|
||||
<option v-if="hasSender" value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
|
||||
<option v-if="hasSender" value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option>
|
||||
<option v-if="hasSender" value="list">{{ i18n.ts.userList }}</option>
|
||||
<option value="never">{{ i18n.ts.none }}</option>
|
||||
</MkSelect>
|
||||
|
||||
|
|
|
@ -139,6 +139,7 @@ type Profile = {
|
|||
hot: Record<keyof typeof defaultStoreSaveKeys, unknown>;
|
||||
cold: Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
|
||||
fontSize: string | null;
|
||||
lang: string | null;
|
||||
cornerRadius: string | null;
|
||||
useSystemFont: 't' | null;
|
||||
wallpaper: string | null;
|
||||
|
@ -197,6 +198,7 @@ function getSettings(): Profile['settings'] {
|
|||
hot,
|
||||
cold,
|
||||
fontSize: miLocalStorage.getItem('fontSize'),
|
||||
lang: miLocalStorage.getItem('lang'),
|
||||
cornerRadius: miLocalStorage.getItem('cornerRadius'),
|
||||
useSystemFont: miLocalStorage.getItem('useSystemFont') as 't' | null,
|
||||
wallpaper: miLocalStorage.getItem('wallpaper'),
|
||||
|
@ -312,6 +314,13 @@ async function applyProfile(id: string): Promise<void> {
|
|||
miLocalStorage.removeItem('fontSize');
|
||||
}
|
||||
|
||||
// lang
|
||||
if (settings.lang) {
|
||||
miLocalStorage.setItem('lang', settings.lang);
|
||||
} else {
|
||||
miLocalStorage.removeItem('lang');
|
||||
}
|
||||
|
||||
// cornerRadius
|
||||
if (settings.cornerRadius) {
|
||||
miLocalStorage.setItem('cornerRadius', settings.cornerRadius);
|
||||
|
|
|
@ -130,7 +130,7 @@ definePageMetadata(() => ({
|
|||
title: i18n.ts.user,
|
||||
icon: 'ph-user ph-bold ph-lg',
|
||||
...user.value ? {
|
||||
title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
|
||||
title: user.value.name ? ` (@${user.value.username})` : `@${user.value.username}`,
|
||||
subtitle: `@${getAcct(user.value)}`,
|
||||
userName: user.value,
|
||||
avatar: user.value,
|
||||
|
|
|
@ -99,7 +99,7 @@ export class Autocomplete {
|
|||
const isHashtag = hashtagIndex !== -1;
|
||||
const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam?.includes(' ');
|
||||
const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
|
||||
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
|
||||
const isEmoji = emojiIndex !== -1 && text.split(/:[\p{Letter}\p{Number}\p{Mark}_+-]+:/u).pop()!.includes(':');
|
||||
|
||||
let opened = false;
|
||||
|
||||
|
@ -125,7 +125,7 @@ export class Autocomplete {
|
|||
if (isEmoji && !opened && this.onlyType.includes('emoji')) {
|
||||
const emoji = text.substring(emojiIndex + 1);
|
||||
if (!emoji.includes(' ')) {
|
||||
this.open('emoji', emoji);
|
||||
this.open('emoji', emoji.normalize('NFC'));
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||
import type { Note, MeDetailed } from "misskey-js/entities.js";
|
||||
|
||||
export function checkWordMute(note: Note, me: MeDetailed | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||
// 自分自身
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
|
||||
if (mutedWords.length > 0) {
|
||||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
const text = getNoteText(note);
|
||||
|
||||
if (text === '') return false;
|
||||
|
||||
|
@ -40,3 +42,25 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any>
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getNoteText(note: Note): string {
|
||||
const textParts: string[] = [];
|
||||
|
||||
if (note.cw)
|
||||
textParts.push(note.cw);
|
||||
|
||||
if (note.text)
|
||||
textParts.push(note.text);
|
||||
|
||||
if (note.files)
|
||||
for (const file of note.files)
|
||||
if (file.comment)
|
||||
textParts.push(file.comment);
|
||||
|
||||
if (note.poll)
|
||||
for (const choice of note.poll.choices)
|
||||
if (choice.text)
|
||||
textParts.push(choice.text);
|
||||
|
||||
return textParts.join('\n').trim();
|
||||
}
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
/* global libopenmpt UTF8ToString writeAsciiToMemory */
|
||||
/* eslint-disable */
|
||||
|
||||
const ChiptuneAudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
|
||||
export function ChiptuneJsConfig (repeatCount: number, context: AudioContext) {
|
||||
let libopenmpt
|
||||
let libopenmptLoadPromise
|
||||
|
||||
type ChiptuneJsConfig = {
|
||||
repeatCount: number | null;
|
||||
context: AudioContext | null;
|
||||
};
|
||||
|
||||
export function ChiptuneJsConfig (repeatCount?: number, context?: AudioContext) {
|
||||
this.repeatCount = repeatCount;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
ChiptuneJsConfig.prototype.constructor = ChiptuneJsConfig;
|
||||
|
||||
export function ChiptuneJsPlayer (config: object) {
|
||||
export function ChiptuneJsPlayer (config: ChiptuneJsConfig) {
|
||||
this.config = config;
|
||||
this.audioContext = config.context || new ChiptuneAudioContext();
|
||||
this.context = this.audioContext.createGain();
|
||||
|
@ -20,6 +27,28 @@ export function ChiptuneJsPlayer (config: object) {
|
|||
this.volume = 1;
|
||||
}
|
||||
|
||||
ChiptuneJsPlayer.prototype.initialize = function() {
|
||||
if (libopenmptLoadPromise) return libopenmptLoadPromise;
|
||||
if (libopenmpt) return Promise.resolve();
|
||||
|
||||
libopenmptLoadPromise = new Promise<void>(async (resolve, reject) => {
|
||||
try {
|
||||
const { Module } = await import('./libopenmpt/libopenmpt.js');
|
||||
await new Promise((resolve) => {
|
||||
Module['onRuntimeInitialized'] = resolve;
|
||||
})
|
||||
libopenmpt = Module;
|
||||
resolve()
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
} finally {
|
||||
libopenmptLoadPromise = undefined;
|
||||
}
|
||||
})
|
||||
|
||||
return libopenmptLoadPromise;
|
||||
}
|
||||
|
||||
ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer;
|
||||
|
||||
ChiptuneJsPlayer.prototype.fireEvent = function (eventName: string, response) {
|
||||
|
@ -61,12 +90,12 @@ ChiptuneJsPlayer.prototype.seek = function (position: number) {
|
|||
|
||||
ChiptuneJsPlayer.prototype.metadata = function () {
|
||||
const data = {};
|
||||
const keys = UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';');
|
||||
const keys = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';');
|
||||
let keyNameBuffer = 0;
|
||||
for (const key of keys) {
|
||||
keyNameBuffer = libopenmpt._malloc(key.length + 1);
|
||||
writeAsciiToMemory(key, keyNameBuffer);
|
||||
data[key] = UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer));
|
||||
libopenmpt.writeAsciiToMemory(key, keyNameBuffer);
|
||||
data[key] = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer));
|
||||
libopenmpt._free(keyNameBuffer);
|
||||
}
|
||||
return data;
|
||||
|
@ -84,7 +113,7 @@ ChiptuneJsPlayer.prototype.unlock = function () {
|
|||
};
|
||||
|
||||
ChiptuneJsPlayer.prototype.load = function (input) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return this.initialize().then(() => new Promise((resolve, reject) => {
|
||||
if(this.touchLocked) {
|
||||
this.unlock();
|
||||
}
|
||||
|
@ -106,7 +135,7 @@ ChiptuneJsPlayer.prototype.load = function (input) {
|
|||
reject(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
ChiptuneJsPlayer.prototype.play = function (buffer: ArrayBuffer) {
|
||||
|
@ -180,7 +209,7 @@ ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) {
|
|||
|
||||
ChiptuneJsPlayer.prototype.getPatternRowChannel = function (pattern: number, row: number, channel: number) {
|
||||
if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) {
|
||||
return UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true));
|
||||
return libopenmpt.UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true));
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
|
25
packages/frontend/src/scripts/libopenmpt/LICENSE
Normal file
25
packages/frontend/src/scripts/libopenmpt/LICENSE
Normal file
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2004-2024, OpenMPT Project Developers and Contributors
|
||||
Copyright (c) 1997-2003, Olivier Lapicque
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the OpenMPT project nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
8
packages/frontend/src/scripts/libopenmpt/libopenmpt.js
Normal file
8
packages/frontend/src/scripts/libopenmpt/libopenmpt.js
Normal file
File diff suppressed because one or more lines are too long
23
packages/frontend/src/scripts/libopenmpt/readme.md
Normal file
23
packages/frontend/src/scripts/libopenmpt/readme.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
modifications made to `libopenmpt.js` (can be taken from https://lib.openmpt.org/libopenmpt/download/):
|
||||
|
||||
at the beginning of the file:
|
||||
```js
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
```
|
||||
|
||||
at the end of the file:
|
||||
```js
|
||||
Module.UTF8ToString = UTF8ToString;
|
||||
Module.writeAsciiToMemory = writeAsciiToMemory;
|
||||
export { Module }
|
||||
```
|
||||
|
||||
replace
|
||||
```
|
||||
wasmBinaryFile="libopenmpt.wasm"
|
||||
```
|
||||
with
|
||||
```
|
||||
wasmBinaryFile=new URL("./libopenmpt.wasm", import.meta.url).href
|
||||
```
|
|
@ -9,9 +9,9 @@ const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm;
|
|||
|
||||
function ifAfter(prefix, fn) {
|
||||
const preLen = prefix.length;
|
||||
const regex = new RegExp(prefix,'i');
|
||||
return (x,pos,string) => {
|
||||
return pos > 0 && string.substring(pos-preLen,pos).match(regex) ? fn(x) : x;
|
||||
const regex = new RegExp(prefix, 'i');
|
||||
return (x, pos, string) => {
|
||||
return pos > 0 && string.substring(pos - preLen, pos).match(regex) ? fn(x) : x;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ export function nyaize(text: string): string {
|
|||
.replace(/one/gi, ifAfter('every', x => x === 'ONE' ? 'NYAN' : 'nyan'))
|
||||
// ko-KR
|
||||
.replace(koRegex1, match => String.fromCharCode(
|
||||
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
|
||||
match.charCodeAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
|
||||
))
|
||||
.replace(koRegex2, '다냥')
|
||||
.replace(koRegex3, '냥');
|
||||
|
|
18
packages/frontend/src/scripts/sanitize-html.ts
Normal file
18
packages/frontend/src/scripts/sanitize-html.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: dakkar and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import original from 'sanitize-html';
|
||||
|
||||
export default function sanitizeHtml(str: string | null): string | null {
|
||||
if (str == null) return str;
|
||||
return original(str, {
|
||||
allowedTags: original.defaults.allowedTags.concat(['img', 'audio', 'video', 'center', 'details', 'summary']),
|
||||
allowedAttributes: {
|
||||
...original.defaults.allowedAttributes,
|
||||
a: original.defaults.allowedAttributes.a.concat(['style']),
|
||||
img: original.defaults.allowedAttributes.img.concat(['style']),
|
||||
},
|
||||
});
|
||||
}
|
|
@ -8,7 +8,7 @@ import meta from '../../package.json';
|
|||
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
|
||||
import pluginJson5 from './vite.json5.js';
|
||||
|
||||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
|
||||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue', '.wasm'];
|
||||
|
||||
const hash = (str: string, seed = 0): number => {
|
||||
let h1 = 0xdeadbeef ^ seed,
|
||||
|
|
|
@ -19,6 +19,7 @@ namespace Entity {
|
|||
content: string
|
||||
plain_content?: string | null
|
||||
created_at: string
|
||||
edited_at: string | null
|
||||
emojis: Emoji[]
|
||||
replies_count: number
|
||||
reblogs_count: number
|
||||
|
@ -38,7 +39,7 @@ namespace Entity {
|
|||
language: string | null
|
||||
pinned: boolean | null
|
||||
emoji_reactions: Array<Reaction>
|
||||
quote: Status | boolean
|
||||
quote: Status | boolean | null
|
||||
bookmarked: boolean
|
||||
}
|
||||
|
||||
|
|
|
@ -725,6 +725,7 @@ namespace FriendicaAPI {
|
|||
content: s.content,
|
||||
plain_content: null,
|
||||
created_at: s.created_at,
|
||||
edited_at: s.edited_at || null,
|
||||
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
|
||||
replies_count: s.replies_count,
|
||||
reblogs_count: s.reblogs_count,
|
||||
|
|
|
@ -17,6 +17,7 @@ namespace FriendicaEntity {
|
|||
reblog: Status | null
|
||||
content: string
|
||||
created_at: string
|
||||
edited_at?: string | null
|
||||
emojis: Emoji[]
|
||||
replies_count: number
|
||||
reblogs_count: number
|
||||
|
|
|
@ -628,6 +628,7 @@ namespace MastodonAPI {
|
|||
content: s.content,
|
||||
plain_content: null,
|
||||
created_at: s.created_at,
|
||||
edited_at: s.edited_at || null,
|
||||
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
|
||||
replies_count: s.replies_count,
|
||||
reblogs_count: s.reblogs_count,
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace MastodonEntity {
|
|||
reblog: Status | null
|
||||
content: string
|
||||
created_at: string
|
||||
edited_at?: string | null
|
||||
emojis: Emoji[]
|
||||
replies_count: number
|
||||
reblogs_count: number
|
||||
|
|
|
@ -283,6 +283,7 @@ namespace MisskeyAPI {
|
|||
: '',
|
||||
plain_content: n.text ? n.text : null,
|
||||
created_at: n.createdAt,
|
||||
edited_at: n.updatedAt || null,
|
||||
emojis: mapEmojis(n.emojis).concat(mapReactionEmojis(n.reactionEmojis)),
|
||||
replies_count: n.repliesCount,
|
||||
reblogs_count: n.renoteCount,
|
||||
|
@ -303,7 +304,7 @@ namespace MisskeyAPI {
|
|||
pinned: null,
|
||||
emoji_reactions: typeof n.reactions === 'object' ? mapReactions(n.reactions, n.myReaction) : [],
|
||||
bookmarked: false,
|
||||
quote: n.renote && n.text ? note(n.renote, n.user.host ? n.user.host : host ? host : null) : false
|
||||
quote: n.renote && n.text ? note(n.renote, n.user.host ? n.user.host : host ? host : null) : null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace MisskeyEntity {
|
|||
export type Note = {
|
||||
id: string
|
||||
createdAt: string
|
||||
updatedAt?: string | null
|
||||
userId: string
|
||||
user: User
|
||||
text: string | null
|
||||
|
|
|
@ -357,6 +357,7 @@ namespace PleromaAPI {
|
|||
content: s.content,
|
||||
plain_content: s.pleroma.content?.['text/plain'] ? s.pleroma.content['text/plain'] : null,
|
||||
created_at: s.created_at,
|
||||
edited_at: s.edited_at || null,
|
||||
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
|
||||
replies_count: s.replies_count,
|
||||
reblogs_count: s.reblogs_count,
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace PleromaEntity {
|
|||
reblog: Status | null
|
||||
content: string
|
||||
created_at: string
|
||||
edited_at?: string | null
|
||||
emojis: Emoji[]
|
||||
replies_count: number
|
||||
reblogs_count: number
|
||||
|
|
|
@ -49,6 +49,7 @@ const status: Entity.Status = {
|
|||
content: 'hoge',
|
||||
plain_content: null,
|
||||
created_at: '2019-03-26T21:40:32',
|
||||
edited_at: null,
|
||||
emojis: [],
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
|
|
|
@ -38,6 +38,7 @@ const status: Entity.Status = {
|
|||
content: 'hoge',
|
||||
plain_content: 'hoge',
|
||||
created_at: '2019-03-26T21:40:32',
|
||||
edited_at: null,
|
||||
emojis: [],
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
|
|
|
@ -37,6 +37,7 @@ const status: Entity.Status = {
|
|||
content: 'hoge',
|
||||
plain_content: 'hoge',
|
||||
created_at: '2019-03-26T21:40:32',
|
||||
edited_at: null,
|
||||
emojis: [],
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
|
|
|
@ -140,8 +140,8 @@ importers:
|
|||
specifier: 1.3.107
|
||||
version: 1.3.107
|
||||
'@transfem-org/sfm-js':
|
||||
specifier: 0.24.4
|
||||
version: 0.24.4
|
||||
specifier: 0.24.5
|
||||
version: 0.24.5
|
||||
'@twemoji/parser':
|
||||
specifier: 15.0.0
|
||||
version: 15.0.0
|
||||
|
@ -392,8 +392,8 @@ importers:
|
|||
specifier: 1.6.0
|
||||
version: 1.6.0
|
||||
tmp:
|
||||
specifier: 0.2.2
|
||||
version: 0.2.2
|
||||
specifier: 0.2.3
|
||||
version: 0.2.3
|
||||
tsc-alias:
|
||||
specifier: 1.8.8
|
||||
version: 1.8.8
|
||||
|
@ -709,8 +709,8 @@ importers:
|
|||
specifier: 0.17.0
|
||||
version: 0.17.0
|
||||
'@transfem-org/sfm-js':
|
||||
specifier: 0.24.4
|
||||
version: 0.24.4
|
||||
specifier: 0.24.5
|
||||
version: 0.24.5
|
||||
'@twemoji/parser':
|
||||
specifier: 15.0.0
|
||||
version: 15.0.0
|
||||
|
@ -7436,8 +7436,8 @@ packages:
|
|||
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
||||
dev: false
|
||||
|
||||
/@transfem-org/sfm-js@0.24.4:
|
||||
resolution: {integrity: sha1-0wEXqL5UJseGFO4GGFRrES6NCDk=, tarball: https://activitypub.software/api/v4/projects/2/packages/npm/@transfem-org/sfm-js/-/@transfem-org/sfm-js-0.24.4.tgz}
|
||||
/@transfem-org/sfm-js@0.24.5:
|
||||
resolution: {integrity: sha1-c9qJO12lIG+kovDGKjZmK2qPqcw=, tarball: https://activitypub.software/api/v4/projects/2/packages/npm/@transfem-org/sfm-js/-/@transfem-org/sfm-js-0.24.5.tgz}
|
||||
dependencies:
|
||||
'@twemoji/parser': 15.0.0
|
||||
dev: false
|
||||
|
@ -18813,6 +18813,12 @@ packages:
|
|||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
rimraf: 5.0.5
|
||||
dev: true
|
||||
|
||||
/tmp@0.2.3:
|
||||
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
|
||||
engines: {node: '>=14.14'}
|
||||
dev: false
|
||||
|
||||
/tmpl@1.0.5:
|
||||
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
|
||||
|
|
Loading…
Reference in a new issue