Compare commits

...

68 commits

Author SHA1 Message Date
df14b213b4 Merge tag 'tags/2024.3.3' 2024-05-14 16:48:25 -07:00
dakkar
3d6eb18e2a 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>
2024-05-09 20:28:58 +00:00
dakkar
c3cbde68d6 pin to node 20
node-gyp / re2 seems to fail on node 22
2024-05-09 19:20:49 +01:00
dakkar
23cbcbaa24 bump version to 2024.3.3 2024-05-09 14:14:15 +01:00
dakkar
f1d96b8ae8 merge: fix: incorrect type for quote property (!480)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/480

Closes #401

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Tess K <me@thvxl.se>
2024-05-09 09:05:16 +00:00
dakkar
f38e55d9de merge: rework pagination - probably fixes #491 (!495)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/495

Closes #491

Approved-by: Ember <acomputerdog@gmail.com>
Approved-by: Marie <marie@kaifa.ch>
2024-05-09 08:15:16 +00:00
dakkar
d40500d399 merge: fix: update reaction icons in tutorial to match sharkey icons (!499)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/499

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Tess K <me@thvxl.se>
Approved-by: Marie <marie@kaifa.ch>
2024-05-09 08:02:59 +00:00
dakkar
16c545aa1f merge: fix: update pl-PL translation for note from verb to noun (!498)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/498

Closes #503

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Tess K <me@thvxl.se>
2024-05-09 08:02:01 +00:00
dakkar
01256af028 merge: Rework cache clearing to be fault tolerant (!497)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/497

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
2024-05-09 08:01:18 +00:00
dakkar
84a98e41b6 merge: make eslint happy (!496)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/496

Approved-by: Tess K <me@thvxl.se>
Approved-by: Marie <marie@kaifa.ch>
2024-05-09 08:00:54 +00:00
dakkar
89f412c696 merge: Send default reactions as Like activities to Iceshrimp.NET instances (!505)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/505

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
2024-05-09 07:59:46 +00:00
dakkar
3ab7f3bd8a merge: fix #507: retain custom emoji aspect ratio in admin panel (!510)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/510

Closes #507

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
2024-05-09 07:59:35 +00:00
dakkar
37989ec0e9 merge: fix: #505 and #516 inconsistent event propagation causes issues in notes and emoji picker when clicking on emojis (!511)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/511

Closes #505 and #516

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
2024-05-09 07:59:23 +00:00
tess
ab074db0c1 fix: retain custom emoji aspect ratio in admin panel for remote emojis 2024-05-08 08:23:32 +02:00
Ember
ac9e4733fd merge: laxer HTML sanitisation for admin-controlled text - fixes #447 (!454)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/454

Closes #447

Approved-by: Ember <acomputerdog@gmail.com>
Approved-by: Marie <marie@kaifa.ch>
2024-05-07 20:19:53 +00:00
dakkar
2c40dd31f3 laxer HTML sanitisation for admin-controlled text - fixes #447 2024-05-07 20:19:52 +00:00
Ember
ed91663672 merge: don't count "system" local accounts in user chart - fixes #451 (!500)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/500

Closes #451

Approved-by: Ember <acomputerdog@gmail.com>
Approved-by: Marie <marie@kaifa.ch>
2024-05-07 20:17:54 +00:00
dakkar
89f4f0e5f4 don't count "system" local accounts in user chart - fixes #451 2024-05-07 20:17:53 +00:00
Ember
58ff225c4e merge: really edit notes in more cases - fixes #424 (!504)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/504

Closes #424

Approved-by: Leah <kevinlukej@gmail.com>
Approved-by: Ember <acomputerdog@gmail.com>
2024-05-07 20:16:39 +00:00
dakkar
eab690a5e3 really edit notes in more cases - fixes #424 2024-05-07 20:16:38 +00:00
tess
19cba1a12a fix: only stop event propagation in MkEmoji and MkCustomEmoji when menu prop is passed 2024-05-06 23:59:42 +02:00
Tess K
83df1c0128 merge: feat: check polls and media for muted keywords (!507)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/507

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Tess K <me@thvxl.se>
2024-05-04 18:51:36 +00:00
Tess K
5e20de45d7 merge: Compact LD-signed activities against well-known context (!503)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/503

Approved-by: Marie <marie@kaifa.ch>
Approved-by: Tess K <me@thvxl.se>
2024-05-04 17:19:42 +00:00
tess
598ebdd44a fix: retain custom emoji aspect ratio in admin panel 2024-05-04 17:49:20 +02:00
Hazel Koehler
cf317da244 revert refactor and cleanup 2024-05-03 22:26:35 -04:00
2241c67486 merge: fix: Add unicode flag to custom emoji regexes (!509)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/509

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Tess K <me@thvxl.se>
2024-05-03 21:22:50 +00:00
f843bf6c17
fix: Add unicode flag to custom emoji regexes 2024-05-03 11:48:00 -05:00
Hazel Koehler
26f8919432 feat: check polls and media for muted keywords 2024-05-02 20:31:34 -04:00
dakkar
d0a2708f91 merge: handle non-ASCII emoji names (!464)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/464

Approved-by: Leah <kevinlukej@gmail.com>
Approved-by: Ember <acomputerdog@gmail.com>
Approved-by: Marie <marie@kaifa.ch>
2024-05-02 21:06:10 +00:00
dakkar
12d92b174d merge: save and restore UI language together with other prefs - fixes #443 (!451)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/451

Closes #443

Approved-by: Marie <marie@kaifa.ch>
Approved-by: Ember <acomputerdog@gmail.com>
2024-05-02 21:05:38 +00:00
Ember
9de4e3c7aa merge: fix: MkPageWindow doesn't render custom emojis in the titlebar when displaying a user profile who has custom emojis in their display-name (!494)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/494

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
Approved-by: Ember <acomputerdog@gmail.com>
2024-05-02 17:53:05 +00:00
Tess K
b7ceb2063b fix: MkPageWindow doesn't render custom emojis in the titlebar when displaying a user profile who has custom emojis in their display-name 2024-05-02 17:53:05 +00:00
ShittyKopper
e333283905 Send default reactions as Like activities to Iceshrimp.NET instances 2024-05-02 02:37:59 +03:00
dakkar
45182c17e2 fix imports 2024-05-01 17:41:33 +01:00
dakkar
6ae01e28aa Compact LD-signed activities against well-known context
This should defend against some spoofing attacks, see also
https://nvd.nist.gov/vuln/detail/CVE-2022-24307 for Mastodon,
febb499fcb
from Iceshrimp and
e790d6be90
for Firefish

Thanks to @tesaguri@fedibird.com for reporting and providing the patch.
2024-04-30 10:16:57 +01:00
PrivateGER
493775ad7b
reformat expression 2024-04-24 16:05:30 +02:00
dakkar
0f3764ff71 teach ReactionService about non-ASCII emoji names 2024-04-23 14:42:02 +01:00
dakkar
504ff4c2af increase page size for most admin lists - #491
Since I can't quite figure out how to prevent `MkPagination` from
truncating the lists too eagerly, I'm going to hide the problem by
making it truncate _less_.
2024-04-21 11:17:17 +01:00
Sugar
9545b8209a fix: update reaction icons in tutorial to match sharkey icons 2024-04-20 21:09:28 +02:00
Sugar
0b705712ee fix: update pl-PL translation for note from verb to noun 2024-04-20 18:03:46 +02:00
Latte macchiato
dd3d562a1e Rework cache clearing to be fault tolerant 2024-04-19 21:58:37 +00:00
dakkar
fa60c1d4bf make eslint happy
also add some types to `chiptune2.ts`
2024-04-19 14:42:58 +01:00
dakkar
dbfafe25e3 rework pagination - fixes #491
previously, when adding items either at the beginnig (e.g. new notes
coming in while we're not looking at the top of the timeline) or a the
end (e.g. more items arriving from a background fetch) of a paginated
view, the resulting list got truncated to `displayLimit`, potentially
throwing data away and causing a new fetch.

This, coupled with the async nature of scrolling & fetching, could
cause weird results.

Also, `offset` was always incremented by the size of the fetched
results, even if not all of them were displayed, meant that it was
possible for offset-based pagination to drop items.

Finally, the "queue" of new items (usually, new notes) also got
truncated to `displayLimit`, which again could drop items (this effect
was usually masked by the first point: when scrolling to the top of
the timeline, if the queue's length was equal to `displayLimit`, those
notes displaced any existing ones, `unshiftItems` set
`more.value=true`, you got scrolled to the top, and notes were fetched
again, so you lost your position but at least all notes got shown,
eventually)
2024-04-19 13:35:18 +01:00
dakkar
e0afeff248 merge: hide images/videos in og cards, when under a CW - fixes #487 (!488)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/488

Closes #487

Approved-by: Marie <marie@kaifa.ch>
Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
2024-04-11 20:40:38 +00:00
Marie
cfc8081cec merge: bump tmp@0.2.3 - fixes #464 (!475)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/475

Closes #464

Approved-by: Marie <marie@kaifa.ch>
Approved-by: Luna <her@mint.lgbt>
Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
2024-04-11 18:00:40 +00:00
Marie
011ccd3a9a merge: bump devel version (!486)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/486

Approved-by: Marie <marie@kaifa.ch>
Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
2024-04-11 17:21:32 +00:00
Amelia Yukii
28065fc1d1 merge: handle ranged requests for proxied files - fixes #494 (!490)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/490

Closes #494

Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
Approved-by: Marie <marie@kaifa.ch>
2024-04-11 10:07:25 +00:00
dakkar
960f4fcff7 detect size of remote files - fixes #494
without this, remote files are assumed to have size 0 (even if we just
downloaded them!) and the range-related code won't run
2024-04-09 16:21:30 +01:00
dakkar
92eec2178f return 206 for every ranged response - fixes #494 2024-04-09 15:42:29 +01:00
dakkar
56dca6dbf5 hide images/videos in og cards, when under a CW - fixes #487 2024-04-07 16:58:13 +01:00
Marie
d6c736f1a5 fix conflicts 2024-04-07 15:42:28 +00:00
dakkar
b6f41a28ed pull in sfm-js that supports non-ascii in emoji names 2024-04-07 16:37:31 +01:00
Marie
bb7b4a8ea4 merge: fix: send null for empty edited_at in mastodon api (!487)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/487

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
2024-04-07 15:36:59 +00:00
dakkar
0690b9a429 merge: fix: load libopenmpt on demand (!469)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/469

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
2024-04-07 14:56:16 +00:00
Sugar🍬🍭🏳️‍⚧
e779c1e667 fix: send null for empty edited_at in mastodon api 2024-04-04 10:43:28 +02:00
Marie
8c955fcce5 merge: use correct note design in favorites page - fixes #483 (!481)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/481

Closes #483

Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
Approved-by: Marie <marie@kaifa.ch>
2024-04-03 09:28:01 +00:00
dakkar
2a634e0309 bump devel version 2024-03-30 12:48:03 +00:00
dakkar
e6970a0e7c Merge branch 'stable' into bump-devel-version 2024-03-30 12:44:31 +00:00
dakkar
bd7c4f66f3 use correct note design in favorites page - fixes #483 2024-03-27 16:03:35 +00:00
Marie
ece86f756d chore: add new type 2024-03-24 23:46:57 +00:00
Marie
56b19ab6bb fix: incorrect type for quote property 2024-03-24 23:41:23 +00:00
dakkar
f4e89f2e6b bump tmp@0.2.3 - fixes #464
see also https://github.com/raszi/node-tmp/issues/295
2024-03-19 17:13:43 +00:00
Alina Sireneva
ecfaf7ff7a chore: added license and patch info 2024-03-14 21:39:34 +03:00
Alina Sireneva
a69315a24b fix: added wasm in vite config 2024-03-14 14:41:24 +03:00
Alina Sireneva
d991eccd3f fix: Promise.resolve 2024-03-11 16:42:10 +03:00
Alina Sireneva
0085305579 fix: load libopenmpt on demand 2024-03-11 15:32:59 +03:00
dakkar
354cb2a675 handle non-ASCII emoji names
* use the more inclusive regexp for validating emoji names
* always normalize emoji names, aliases, categories

the latter point is necessary to allow matching, for example, `ä`
against `a`+combining diaeresis

this will also need to bump the version of `sfm-js` once we merge
https://activitypub.software/TransFem-org/sfm-js/-/merge_requests/2
2024-03-09 12:51:51 +00:00
dakkar
ebe8179c9e save and restore UI language together with other prefs - fixes #443 2024-03-03 11:23:49 +00:00
91 changed files with 432 additions and 214 deletions

View file

@ -4,7 +4,7 @@ stages:
testCommit:
stage: test
image: node:latest
image: node:iron
services:
- postgres:15
- redis

View file

@ -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"

View file

@ -1345,10 +1345,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)."

View file

@ -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"

View file

@ -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
View file

@ -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": {
/**

View file

@ -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"

View file

@ -1338,10 +1338,10 @@ _initialTutorial:
_reaction:
title: "リアクションって何?"
description: "ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。"
letsTryReacting: "リアクションは、ノートの「」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!"
letsTryReacting: "リアクションは、ノートの「{reaction}」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!"
reactToContinue: "リアクションをつけると先に進めるようになります。"
reactNotification: "あなたのノートが誰かにリアクションされると、リアルタイムで通知を受け取ります。"
reactDone: "「」ボタンを押すとリアクションを取り消すことができます。"
reactDone: "「{undo}」ボタンを押すとリアクションを取り消すことができます。"
_timeline:
title: "タイムラインのしくみ"
description1: "Sharkeyには、使い方に応じて複数のタイムラインが用意されていますサーバーによってはいずれかが無効になっていることがあります。"

View file

@ -1265,10 +1265,10 @@ _initialTutorial:
_reaction:
title: "ツッコミってなんや?"
description: "ノートには「ツッコミ」できんねん。「いいね」とか何言っとるかわからんし、簡単に表現できるのはええことやん?"
letsTryReacting: "ノートの「」ボタンでツッコめるわ。試しに下のノートにツッコんでみ。"
letsTryReacting: "ノートの「{reaction}」ボタンでツッコめるわ。試しに下のノートにツッコんでみ。"
reactToContinue: "ツッコんだら進めるようになるで。"
reactNotification: "あんたのノートが誰かにツッコまれたら、すぐ通知するで。"
reactDone: "「」ボタンでツッコミやめれるで。"
reactDone: "「{undo}」ボタンでツッコミやめれるで。"
_timeline:
title: "タイムラインのしくみ"
description1: "Sharkeyには、いろいろタイムラインがあんでただ、サーバーによっては無効化されてるところもあるな。"

View file

@ -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: "서버에 접속할 수 없습니다"

View file

@ -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"

View file

@ -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: "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้"

View file

@ -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: "无法连接到服务器"

View file

@ -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: "無法連接伺服器"

View file

@ -1,6 +1,6 @@
{
"name": "egirlskey",
"version": "2024.3.2+egirls.0",
"version": "2024.3.3+egirls.0",
"codename": "boobdog",
"repository": {
"type": "git",

View file

@ -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",

View file

@ -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({

View file

@ -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 {

View file

@ -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

View file

@ -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();

View file

@ -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,

View file

@ -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()) }),
]);

View file

@ -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}.`);
}
}

View file

@ -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;
}

View file

@ -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,

View file

@ -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);
@ -154,6 +155,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})`);

View file

@ -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,

View file

@ -35,7 +35,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')));
});
}
}

View file

@ -41,7 +41,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',
@ -74,18 +74,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,

View file

@ -83,15 +83,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,

View file

@ -99,7 +99,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');
}

View file

@ -93,17 +93,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 {

View file

@ -35,7 +35,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')));
});
}
}

View file

@ -35,7 +35,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')));
});
}
}

View file

@ -37,7 +37,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);
});
}
}

View file

@ -41,7 +41,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',
@ -73,6 +73,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 });
@ -84,22 +85,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,

View file

@ -83,7 +83,7 @@ export class MastoConverters {
}
return 'unknown';
}
public encodeFile(f: any): Entity.Attachment {
return {
id: f.id,
@ -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,
});
}
}

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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;

View file

@ -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;

View file

@ -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"

View file

@ -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;

View file

@ -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;

View file

@ -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][]);
}
/*

View file

@ -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';

View file

@ -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>

View file

@ -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"/>

View file

@ -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';

View file

@ -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}:`,

View file

@ -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,

View file

@ -43,7 +43,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"/>

View file

@ -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';

View file

@ -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>

View file

@ -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"/>

View file

@ -46,7 +46,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"/>

View file

@ -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/>

View file

@ -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>

View file

@ -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"/>

View file

@ -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"/>

View file

@ -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 {

View file

@ -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">

View file

@ -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,

View file

@ -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>

View file

@ -140,6 +140,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;
@ -198,6 +199,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'),
@ -313,6 +315,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);

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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 '';
};

View 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.

File diff suppressed because one or more lines are too long

View 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
```

View file

@ -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, '냥');

View 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']),
},
});
}

View file

@ -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,

View file

@ -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
}

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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
}
}

View file

@ -7,6 +7,7 @@ namespace MisskeyEntity {
export type Note = {
id: string
createdAt: string
updatedAt?: string | null
userId: string
user: User
text: string | null

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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,

22
pnpm-lock.yaml generated
View file

@ -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==}