Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
7f643f7961
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -49,6 +49,12 @@
|
||||||
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
||||||
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
||||||
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
||||||
|
- Enhance: リモートへの引用リノートと同一のリンクにはリンクプレビューを表示しないように
|
||||||
|
- Enhance: コードのシンタックスハイライトにテーマを適用できるように
|
||||||
|
- Enhance: リアクション権限がない場合、ハートにフォールバックするのではなくリアクションピッカーなどから打てないように
|
||||||
|
- リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合
|
||||||
|
- センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合
|
||||||
|
- ロールが必要な絵文字をリアクションしようとした場合
|
||||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||||
|
@ -65,6 +71,10 @@
|
||||||
- Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正
|
- Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正
|
||||||
- Fix: 画像をクロップ時、正常に完了できない問題の修正
|
- Fix: 画像をクロップ時、正常に完了できない問題の修正
|
||||||
- Fix: キャプションが空の画像をクロップするとキャプションにnullという文字列が入ってしまう問題の修正
|
- Fix: キャプションが空の画像をクロップするとキャプションにnullという文字列が入ってしまう問題の修正
|
||||||
|
- Fix: プロフィールを編集してもリロードするまで反映されない問題を修正
|
||||||
|
- Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正
|
||||||
|
- Fix: MkCodeEditorで行がずれていってしまう問題の修正
|
||||||
|
- Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||||
|
@ -79,6 +89,7 @@
|
||||||
- Fix: properly handle cc followers
|
- Fix: properly handle cc followers
|
||||||
- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec
|
- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec
|
||||||
- Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122
|
- Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122
|
||||||
|
- Enhance: 連合向けのノート配信を軽量化 #13192
|
||||||
|
|
||||||
### Service Worker
|
### Service Worker
|
||||||
- Enhance: オフライン表示のデザインを改善・多言語対応
|
- Enhance: オフライン表示のデザインを改善・多言語対応
|
||||||
|
|
|
@ -281,24 +281,22 @@ You can override the component meta by creating a meta story file (`MyComponent.
|
||||||
```ts
|
```ts
|
||||||
export const argTypes = {
|
export const argTypes = {
|
||||||
scale: {
|
scale: {
|
||||||
control: {
|
control: {
|
||||||
type: 'range',
|
type: 'range',
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 4,
|
max: 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers.
|
Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
export const handlers = [
|
export const handlers = [
|
||||||
rest.post('/api/notes/timeline', (req, res, ctx) => {
|
http.post('/api/notes/timeline', ({ request }) => {
|
||||||
return res(
|
return HttpResponse.json([]);
|
||||||
ctx.json([]),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
|
@ -1237,7 +1237,7 @@ _initialAccountSetting:
|
||||||
pushNotificationDescription: "Activant les notificacions emergents et permetrà rebre notificacions de {name} directament al teu dispositiu."
|
pushNotificationDescription: "Activant les notificacions emergents et permetrà rebre notificacions de {name} directament al teu dispositiu."
|
||||||
initialAccountSettingCompleted: "Configuració del perfil completada!"
|
initialAccountSettingCompleted: "Configuració del perfil completada!"
|
||||||
haveFun: "Disfruta {name}!"
|
haveFun: "Disfruta {name}!"
|
||||||
youCanContinueTutorial: "Pots continuar amb un tutorial per aprendre a Fer servir {name} (MissKey) o tu pots estalviar i començar a fer-lo servir ja."
|
youCanContinueTutorial: "Pots continuar amb un tutorial per aprendre a Fer servir {name} (CherryPick) o tu pots estalviar i començar a fer-lo servir ja."
|
||||||
startTutorial: "Començar el tutorial"
|
startTutorial: "Començar el tutorial"
|
||||||
skipAreYouSure: "Et vols saltar la configuració del perfil?"
|
skipAreYouSure: "Et vols saltar la configuració del perfil?"
|
||||||
laterAreYouSure: "Vols continuar la configuració del perfil més tard?"
|
laterAreYouSure: "Vols continuar la configuració del perfil més tard?"
|
||||||
|
@ -1248,10 +1248,10 @@ _initialTutorial:
|
||||||
skipAreYouSure: "Sortir del tutorial?"
|
skipAreYouSure: "Sortir del tutorial?"
|
||||||
_landing:
|
_landing:
|
||||||
title: "Benvingut al tutorial"
|
title: "Benvingut al tutorial"
|
||||||
description: "Aquí aprendràs el bàsic per poder fer servir Misskey i les seves característiques."
|
description: "Aquí aprendràs el bàsic per poder fer servir CherryPick i les seves característiques."
|
||||||
_note:
|
_note:
|
||||||
title: "Què és una Nota?"
|
title: "Què és una Nota?"
|
||||||
description: "Les publicacions a Misskey es diuen 'Notes'. Les Notes s'ordenen cronològicament a la línia de temps i s'actualitzen de forma automàtica."
|
description: "Les publicacions a CherryPick es diuen 'Notes'. Les Notes s'ordenen cronològicament a la línia de temps i s'actualitzen de forma automàtica."
|
||||||
reply: "Fes clic en aquest botó per contestar a un missatge. També és possible contestar a una contestació, continuant la conversació en forma de fil."
|
reply: "Fes clic en aquest botó per contestar a un missatge. També és possible contestar a una contestació, continuant la conversació en forma de fil."
|
||||||
renote: "Pots compartir una Nota a la teva pròpia línia de temps. Inclús pots citar-les amb els teus comentaris."
|
renote: "Pots compartir una Nota a la teva pròpia línia de temps. Inclús pots citar-les amb els teus comentaris."
|
||||||
reaction: "Pots afegir reaccions a les Notes. Entrarem més en detall a la pròxima pàgina."
|
reaction: "Pots afegir reaccions a les Notes. Entrarem més en detall a la pròxima pàgina."
|
||||||
|
@ -1265,7 +1265,7 @@ _initialTutorial:
|
||||||
reactDone: "Pots desfer una reacció fent clic al botó '-'."
|
reactDone: "Pots desfer una reacció fent clic al botó '-'."
|
||||||
_timeline:
|
_timeline:
|
||||||
title: "El concepte de les línies de temps"
|
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)"
|
description1: "CherryPick mostra diferents línies de temps basades en l'ús (algunes poden no estar disponibles depenent de la política del servidor)"
|
||||||
home: "Pots veure notes dels comptes que segueixes"
|
home: "Pots veure notes dels comptes que segueixes"
|
||||||
local: "Pots veure les notes dels usuaris del servidor."
|
local: "Pots veure les notes dels usuaris del servidor."
|
||||||
social: "Es mostren les notes de les línies de temps d'Inici i Local."
|
social: "Es mostren les notes de les línies de temps d'Inici i Local."
|
||||||
|
@ -1274,7 +1274,7 @@ _initialTutorial:
|
||||||
description3: "A més hi ha línies de temps per llistes i per canals. Si vols saber més {link}."
|
description3: "A més hi ha línies de temps per llistes i per canals. Si vols saber més {link}."
|
||||||
_postNote:
|
_postNote:
|
||||||
title: "Configuració de la publicació de les notes"
|
title: "Configuració de la publicació de les notes"
|
||||||
description1: "Quan públiques una nota a Misskey hi ha diferents opcions disponibles. El formulari de publicació es veu així"
|
description1: "Quan públiques una nota a CherryPick hi ha diferents opcions disponibles. El formulari de publicació es veu així"
|
||||||
_visibility:
|
_visibility:
|
||||||
description: "Pots limitar qui pot veure les teves notes."
|
description: "Pots limitar qui pot veure les teves notes."
|
||||||
public: "La teva nota serà visible per a tots els usuaris."
|
public: "La teva nota serà visible per a tots els usuaris."
|
||||||
|
@ -1302,7 +1302,7 @@ _initialTutorial:
|
||||||
doItToContinue: "Marca el fitxer adjunt com a sensible per poder continuar."
|
doItToContinue: "Marca el fitxer adjunt com a sensible per poder continuar."
|
||||||
_done:
|
_done:
|
||||||
title: "Has completat el tutorial 🎉"
|
title: "Has completat el tutorial 🎉"
|
||||||
description: "Les funcions explicades aquí és una petita mostra. Per una explicació més detallada de com fer servir MissKey consulta {link}."
|
description: "Les funcions explicades aquí és una petita mostra. Per una explicació més detallada de com fer servir CherryPick consulta {link}."
|
||||||
_timelineDescription:
|
_timelineDescription:
|
||||||
home: "A la línia de temps d'Inici pots veure les notes dels usuaris que segueixes."
|
home: "A la línia de temps d'Inici pots veure les notes dels usuaris que segueixes."
|
||||||
local: "A la línia de temps Local pots veure les notes de tots els usuaris d'aquest servidor."
|
local: "A la línia de temps Local pots veure les notes de tots els usuaris d'aquest servidor."
|
||||||
|
@ -1330,7 +1330,7 @@ _accountMigration:
|
||||||
moveTo: "Migrar aquest compte a un altre"
|
moveTo: "Migrar aquest compte a un altre"
|
||||||
moveToLabel: "Compte al qual es vol migrar:"
|
moveToLabel: "Compte al qual es vol migrar:"
|
||||||
moveCannotBeUndone: "Les migracions dels comptes no es poden desfer."
|
moveCannotBeUndone: "Les migracions dels comptes no es poden desfer."
|
||||||
moveAccountDescription: "Això migrarà la teva compte a un altre diferent.\n ・Els seguidors d'aquest compte és passaran al compte nou de forma automàtica\n ・Es deixaran de seguir a tots els usuaris que es segueixen actualment en aquest compte\n ・No es poden crear notes noves, etc. en aquest compte\n\nSi bé la migració de seguidors es automàtica, has de preparar alguns pasos manualment per migrar la llista d'usuaris que segueixes. Per fer això has d'exportar els seguidors que després importaraes al compte nou mitjançant el menú de configuració. El mateix procediment s'ha de seguir per less teves llistes i els teus usuaris silenciats i bloquejats.\n\n(Aquesta explicació s'aplica a Misskey v13.12.0 i posteriors. Altres aplicacions, com Mastodon, poden funcionar diferent.)"
|
moveAccountDescription: "Això migrarà la teva compte a un altre diferent.\n ・Els seguidors d'aquest compte és passaran al compte nou de forma automàtica\n ・Es deixaran de seguir a tots els usuaris que es segueixen actualment en aquest compte\n ・No es poden crear notes noves, etc. en aquest compte\n\nSi bé la migració de seguidors es automàtica, has de preparar alguns pasos manualment per migrar la llista d'usuaris que segueixes. Per fer això has d'exportar els seguidors que després importaraes al compte nou mitjançant el menú de configuració. El mateix procediment s'ha de seguir per less teves llistes i els teus usuaris silenciats i bloquejats.\n\n(Aquesta explicació s'aplica a CherryPick v13.12.0 i posteriors. Altres aplicacions, com Mastodon, poden funcionar diferent.)"
|
||||||
moveAccountHowTo: "Per fer la migració, primer has de crear un àlies per aquest compte al compte al qual vols migrar.\nDesprés de crear l'àlies, introdueix el compte al qual vols migrar amb el format següent: @nomusuari@servidor.exemple.com"
|
moveAccountHowTo: "Per fer la migració, primer has de crear un àlies per aquest compte al compte al qual vols migrar.\nDesprés de crear l'àlies, introdueix el compte al qual vols migrar amb el format següent: @nomusuari@servidor.exemple.com"
|
||||||
startMigration: "Migrar"
|
startMigration: "Migrar"
|
||||||
migrationConfirm: "Vols migrar aquest compte a {account}? Una vegada comenci la migració no es podrà parar O fer marxa enrere i no podràs tornar a fer servir aquest compte mai més."
|
migrationConfirm: "Vols migrar aquest compte a {account}? Una vegada comenci la migració no es podrà parar O fer marxa enrere i no podràs tornar a fer servir aquest compte mai més."
|
||||||
|
@ -1439,7 +1439,7 @@ _achievements:
|
||||||
_login1000:
|
_login1000:
|
||||||
title: "Mestre de les Notes III"
|
title: "Mestre de les Notes III"
|
||||||
description: "Vas iniciar sessió fa mil dies"
|
description: "Vas iniciar sessió fa mil dies"
|
||||||
flavor: "Gràcies per fer servir MissKey!"
|
flavor: "Gràcies per fer servir CherryPick!"
|
||||||
_noteClipped1:
|
_noteClipped1:
|
||||||
title: "He de retallar-te!"
|
title: "He de retallar-te!"
|
||||||
description: "Retalla la teva primera nota"
|
description: "Retalla la teva primera nota"
|
||||||
|
@ -1499,18 +1499,18 @@ _achievements:
|
||||||
title: "M'agraden els èxits "
|
title: "M'agraden els èxits "
|
||||||
description: "Mira la teva llista d'assoliments durant més de 3 minuts"
|
description: "Mira la teva llista d'assoliments durant més de 3 minuts"
|
||||||
_iLoveMisskey:
|
_iLoveMisskey:
|
||||||
title: "Estimo Misskey"
|
title: "Estimo CherryPick"
|
||||||
description: "Publica \"I ❤ #Misskey\""
|
description: "Publica \"I ❤ #CherryPick\""
|
||||||
flavor: "L'equip de desenvolupament de Misskey agraeix el vostre suport!"
|
flavor: "L'equip de desenvolupament de CherryPick agraeix el vostre suport!"
|
||||||
_foundTreasure:
|
_foundTreasure:
|
||||||
title: "A la Recerca del Tresor"
|
title: "A la Recerca del Tresor"
|
||||||
description: "Has trobat el tresor amagat"
|
description: "Has trobat el tresor amagat"
|
||||||
_client30min:
|
_client30min:
|
||||||
title: "Parem una estona"
|
title: "Parem una estona"
|
||||||
description: "Mantingues obert Misskey per 30 minuts"
|
description: "Mantingues obert CherryPick per 30 minuts"
|
||||||
_client60min:
|
_client60min:
|
||||||
title: "A totes amb Misskey"
|
title: "A totes amb CherryPick"
|
||||||
description: "Mantingues Misskey obert per 60 minuts"
|
description: "Mantingues CherryPick obert per 60 minuts"
|
||||||
_noteDeletedWithin1min:
|
_noteDeletedWithin1min:
|
||||||
title: "No et preocupis"
|
title: "No et preocupis"
|
||||||
description: "Esborra una nota al minut de publicar-la"
|
description: "Esborra una nota al minut de publicar-la"
|
||||||
|
|
|
@ -1235,7 +1235,7 @@ _initialTutorial:
|
||||||
description: "Hier kannst du sehen, wie CherryPick funktioniert"
|
description: "Hier kannst du sehen, wie CherryPick funktioniert"
|
||||||
_note:
|
_note:
|
||||||
title: "Was sind Notizen?"
|
title: "Was sind Notizen?"
|
||||||
description: "Beiträge auf Misskey heißen \"Notizen\". Notizen werden chronologisch in der Chronik angeordnet und in Echtzeit aktualisiert."
|
description: "Beiträge auf CherryPick heißen \"Notizen\". Notizen werden chronologisch in der Chronik angeordnet und in Echtzeit aktualisiert."
|
||||||
reply: "Klicke auf diesen Button, um auf eine Nachricht zu antworten. Es ist auch möglich, auf Antworten zu antworten und die Unterhaltung wie einen Thread fortzusetzen."
|
reply: "Klicke auf diesen Button, um auf eine Nachricht zu antworten. Es ist auch möglich, auf Antworten zu antworten und die Unterhaltung wie einen Thread fortzusetzen."
|
||||||
_reaction:
|
_reaction:
|
||||||
title: "Was sind Reaktionen?"
|
title: "Was sind Reaktionen?"
|
||||||
|
|
|
@ -541,7 +541,7 @@ volume: "음량"
|
||||||
masterVolume: "대빵 음량"
|
masterVolume: "대빵 음량"
|
||||||
notUseSound: "음소거하기"
|
notUseSound: "음소거하기"
|
||||||
useSoundOnlyWhenActive: "CherryPick이 활성화되어 있을 때만 소리 내기"
|
useSoundOnlyWhenActive: "CherryPick이 활성화되어 있을 때만 소리 내기"
|
||||||
details: "좀 더"
|
details: "자세히"
|
||||||
chooseEmoji: "이모지 선택"
|
chooseEmoji: "이모지 선택"
|
||||||
unableToProcess: "작업 다 몬 했십니다"
|
unableToProcess: "작업 다 몬 했십니다"
|
||||||
recentUsed: "최근 쓴 놈"
|
recentUsed: "최근 쓴 놈"
|
||||||
|
@ -691,7 +691,7 @@ _achievements:
|
||||||
_myNoteFavorited1:
|
_myNoteFavorited1:
|
||||||
description: "다런 사람이 내 노트럴 질겨찾기에 담앗십니다"
|
description: "다런 사람이 내 노트럴 질겨찾기에 담앗십니다"
|
||||||
_iLoveMisskey:
|
_iLoveMisskey:
|
||||||
description: "“I ❤ #Misskey”럴 섰어예"
|
description: "“I ❤ #CherryPick”럴 섰어예"
|
||||||
_postedAt0min0sec:
|
_postedAt0min0sec:
|
||||||
description: "0분 0초에 노트를 섰어예"
|
description: "0분 0초에 노트를 섰어예"
|
||||||
_tutorialCompleted:
|
_tutorialCompleted:
|
||||||
|
|
|
@ -2369,9 +2369,9 @@ _permissions:
|
||||||
"write:report-abuse": "위반 내용 신고하기"
|
"write:report-abuse": "위반 내용 신고하기"
|
||||||
_auth:
|
_auth:
|
||||||
shareAccessTitle: "애플리케이션 접근 허가"
|
shareAccessTitle: "애플리케이션 접근 허가"
|
||||||
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용할까요?"
|
shareAccess: "‘{name}’에서 계정에 접근하는 것을 허용할까요?"
|
||||||
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용할까요?"
|
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용할까요?"
|
||||||
permission: "{name}에서 다음 권한을 요청했어요"
|
permission: "‘{name}’에서 다음 권한을 요청했어요"
|
||||||
permissionAsk: "이 앱은 다음 권한을 요청하고 있습니다"
|
permissionAsk: "이 앱은 다음 권한을 요청하고 있습니다"
|
||||||
pleaseGoBack: "앱으로 돌아가서 계속 진행해 주세요"
|
pleaseGoBack: "앱으로 돌아가서 계속 진행해 주세요"
|
||||||
callback: "앱으로 돌아갈게요!"
|
callback: "앱으로 돌아갈게요!"
|
||||||
|
@ -2837,7 +2837,7 @@ _reversi:
|
||||||
freeMatch: "프리 매치"
|
freeMatch: "프리 매치"
|
||||||
lookingForPlayer: "대전 상대를 찾고 있어요"
|
lookingForPlayer: "대전 상대를 찾고 있어요"
|
||||||
gameCanceled: "대국이 취소되었어요"
|
gameCanceled: "대국이 취소되었어요"
|
||||||
shareToTlTheGameWhenStart: "시작 시 대국을 타임라인에 게시"
|
shareToTlTheGameWhenStart: "대국 시작 시 타임라인에 대국 게시하기"
|
||||||
iStartedAGame: "대국이 시작되었어요! #MisskeyReversi"
|
iStartedAGame: "대국이 시작되었어요! #MisskeyReversi"
|
||||||
opponentHasSettingsChanged: "상대방이 게임 설정을 변경했어요"
|
opponentHasSettingsChanged: "상대방이 게임 설정을 변경했어요"
|
||||||
allowIrregularRules: "변칙 허용(완전 자유)"
|
allowIrregularRules: "변칙 허용(완전 자유)"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "lycheebridge",
|
"name": "lycheebridge",
|
||||||
"version": "2024.2.0-alpha.3",
|
"version": "2024.2.0-alpha.3",
|
||||||
"basedCherryPickVersion": "4.7.0-beta.1",
|
"basedCherryPickVersion": "4.7.0-beta.2",
|
||||||
"basedMisskeyVersion": "2024.2.0-beta.10",
|
"basedMisskeyVersion": "2024.2.0-beta.10",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -388,7 +388,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public checkDuplicate(name: string): Promise<boolean> {
|
public checkDuplicate(name: string): Promise<boolean> {
|
||||||
return this.emojisRepository.exist({ where: { name, host: IsNull() } });
|
return this.emojisRepository.exists({ where: { name, host: IsNull() } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -419,6 +419,10 @@ export class MfmService {
|
||||||
},
|
},
|
||||||
|
|
||||||
text: (node) => {
|
text: (node) => {
|
||||||
|
if (!node.props.text.match(/[\r\n]/)) {
|
||||||
|
return doc.createTextNode(node.props.text);
|
||||||
|
}
|
||||||
|
|
||||||
const el = doc.createElement('span');
|
const el = doc.createElement('span');
|
||||||
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
||||||
|
|
||||||
|
|
|
@ -628,7 +628,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
// 通知
|
// 通知
|
||||||
if (data.reply.userHost === null) {
|
if (data.reply.userHost === null) {
|
||||||
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
userId: data.reply.userId,
|
userId: data.reply.userId,
|
||||||
threadId: data.reply.threadId ?? data.reply.id,
|
threadId: data.reply.threadId ?? data.reply.id,
|
||||||
|
@ -766,7 +766,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
@bindThis
|
@bindThis
|
||||||
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) {
|
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) {
|
||||||
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
|
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
|
||||||
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
userId: u.id,
|
userId: u.id,
|
||||||
threadId: note.threadId ?? note.id,
|
threadId: note.threadId ?? note.id,
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// スレッドミュート
|
// スレッドミュート
|
||||||
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
threadId: note.threadId ?? note.id,
|
threadId: note.threadId ?? note.id,
|
||||||
|
@ -70,7 +70,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
|
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
|
||||||
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
||||||
const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } });
|
const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } });
|
||||||
|
|
||||||
if (!exist) return;
|
if (!exist) return;
|
||||||
|
|
||||||
|
|
|
@ -74,12 +74,12 @@ export class SignupService {
|
||||||
const secret = generateUserToken();
|
const secret = generateUserToken();
|
||||||
|
|
||||||
// Check username duplication
|
// Check username duplication
|
||||||
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
||||||
throw new Error('DUPLICATED_USERNAME');
|
throw new Error('DUPLICATED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check deleted username duplication
|
// Check deleted username duplication
|
||||||
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
|
if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) {
|
||||||
throw new Error('USED_USERNAME');
|
throw new Error('USED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
let autoAccept = false;
|
let autoAccept = false;
|
||||||
|
|
||||||
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
||||||
const isFollowing = await this.followingsRepository.exist({
|
const isFollowing = await this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
|
@ -156,7 +156,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
// フォローしているユーザーは自動承認オプション
|
// フォローしているユーザーは自動承認オプション
|
||||||
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
|
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
|
||||||
const isFollowed = await this.followingsRepository.exist({
|
const isFollowed = await this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: followee.id,
|
followerId: followee.id,
|
||||||
followeeId: follower.id,
|
followeeId: follower.id,
|
||||||
|
@ -170,7 +170,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
if (followee.isLocked && !autoAccept) {
|
if (followee.isLocked && !autoAccept) {
|
||||||
autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs(
|
autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs(
|
||||||
follower,
|
follower,
|
||||||
(oldSrc, newSrc) => this.followingsRepository.exist({
|
(oldSrc, newSrc) => this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: newSrc.id,
|
followerId: newSrc.id,
|
||||||
|
@ -233,7 +233,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||||
|
|
||||||
const requestExist = await this.followRequestsRepository.exist({
|
const requestExist = await this.followRequestsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
|
@ -531,7 +531,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestExist = await this.followRequestsRepository.exist({
|
const requestExist = await this.followRequestsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
|
|
|
@ -683,7 +683,7 @@ export class ApInboxService {
|
||||||
return 'skip: follower not found';
|
return 'skip: follower not found';
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFollowing = await this.followingsRepository.exist({
|
const isFollowing = await this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: actor.id,
|
followeeId: actor.id,
|
||||||
|
@ -740,14 +740,14 @@ export class ApInboxService {
|
||||||
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
|
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestExist = await this.followRequestsRepository.exist({
|
const requestExist = await this.followRequestsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: actor.id,
|
followerId: actor.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isFollowing = await this.followingsRepository.exist({
|
const isFollowing = await this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: actor.id,
|
followerId: actor.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
|
|
|
@ -25,8 +25,21 @@ export class ApMfmService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getNoteHtml(note: MiNote): string | null {
|
public getNoteHtml(note: MiNote, apAppend?: string) {
|
||||||
if (!note.text) return '';
|
let noMisskeyContent = false;
|
||||||
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
const srcMfm = (note.text ?? '') + (apAppend ?? '');
|
||||||
|
|
||||||
|
const parsed = mfm.parse(srcMfm);
|
||||||
|
|
||||||
|
if (!apAppend && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
|
||||||
|
noMisskeyContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers));
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
noMisskeyContent,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -330,7 +330,7 @@ export class ApRendererService {
|
||||||
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
||||||
|
|
||||||
if (inReplyToNote != null) {
|
if (inReplyToNote != null) {
|
||||||
const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
|
const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } });
|
||||||
|
|
||||||
if (inReplyToUserExist) {
|
if (inReplyToUserExist) {
|
||||||
if (inReplyToNote.uri) {
|
if (inReplyToNote.uri) {
|
||||||
|
@ -394,17 +394,15 @@ export class ApRendererService {
|
||||||
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
let apText = text;
|
let apAppend = '';
|
||||||
|
|
||||||
if (quote) {
|
if (quote) {
|
||||||
apText += `\n\nRE: ${quote}`;
|
apAppend += `\n\nRE: ${quote}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||||
|
|
||||||
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend);
|
||||||
text: apText,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const emojis = await this.getEmojis(note.emojis);
|
const emojis = await this.getEmojis(note.emojis);
|
||||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||||
|
@ -417,9 +415,6 @@ export class ApRendererService {
|
||||||
|
|
||||||
const asPoll = poll ? {
|
const asPoll = poll ? {
|
||||||
type: 'Question',
|
type: 'Question',
|
||||||
content: this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
|
||||||
text: text,
|
|
||||||
})),
|
|
||||||
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
|
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
|
||||||
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
|
@ -453,11 +448,13 @@ export class ApRendererService {
|
||||||
attributedTo,
|
attributedTo,
|
||||||
summary: summary ?? undefined,
|
summary: summary ?? undefined,
|
||||||
content: content ?? undefined,
|
content: content ?? undefined,
|
||||||
_misskey_content: text,
|
...(noMisskeyContent ? {} : {
|
||||||
source: {
|
_misskey_content: text,
|
||||||
content: text,
|
source: {
|
||||||
mediaType: 'text/x.misskeymarkdown',
|
content: text,
|
||||||
},
|
mediaType: 'text/x.misskeymarkdown',
|
||||||
|
},
|
||||||
|
}),
|
||||||
_misskey_quote: quote,
|
_misskey_quote: quote,
|
||||||
quoteUrl: quote,
|
quoteUrl: quote,
|
||||||
published: this.idService.parse(note.id).date.toISOString(),
|
published: this.idService.parse(note.id).date.toISOString(),
|
||||||
|
@ -658,6 +655,7 @@ export class ApRendererService {
|
||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
'https://w3id.org/security/v1',
|
'https://w3id.org/security/v1',
|
||||||
{
|
{
|
||||||
|
Key: 'sec:Key',
|
||||||
// as non-standards
|
// as non-standards
|
||||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||||
sensitive: 'as:sensitive',
|
sensitive: 'as:sensitive',
|
||||||
|
|
|
@ -51,14 +51,14 @@ export class ChannelEntityService {
|
||||||
|
|
||||||
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
|
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
|
||||||
|
|
||||||
const isFollowing = meId ? await this.channelFollowingsRepository.exist({
|
const isFollowing = meId ? await this.channelFollowingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: meId,
|
followerId: meId,
|
||||||
followeeId: channel.id,
|
followeeId: channel.id,
|
||||||
},
|
},
|
||||||
}) : false;
|
}) : false;
|
||||||
|
|
||||||
const isFavorited = meId ? await this.channelFavoritesRepository.exist({
|
const isFavorited = meId ? await this.channelFavoritesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
userId: meId,
|
userId: meId,
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class ClipEntityService {
|
||||||
description: clip.description,
|
description: clip.description,
|
||||||
isPublic: clip.isPublic,
|
isPublic: clip.isPublic,
|
||||||
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||||
isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class EmojiEntityService {
|
||||||
category: emoji.category,
|
category: emoji.category,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: emoji.publicUrl || emoji.originalUrl,
|
||||||
|
localOnly: emoji.localOnly ? true : undefined,
|
||||||
isSensitive: emoji.isSensitive ? true : undefined,
|
isSensitive: emoji.isSensitive ? true : undefined,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,7 +47,7 @@ export class FlashEntityService {
|
||||||
summary: flash.summary,
|
summary: flash.summary,
|
||||||
script: flash.script,
|
script: flash.script,
|
||||||
likedCount: flash.likedCount,
|
likedCount: flash.likedCount,
|
||||||
isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ export class GalleryPostEntityService {
|
||||||
tags: post.tags.length > 0 ? post.tags : undefined,
|
tags: post.tags.length > 0 ? post.tags : undefined,
|
||||||
isSensitive: post.isSensitive,
|
isSensitive: post.isSensitive,
|
||||||
likedCount: post.likedCount,
|
likedCount: post.likedCount,
|
||||||
isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined,
|
isLiked: meId ? await this.galleryLikesRepository.exists({ where: { postId: post.id, userId: meId } }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
hide = false;
|
hide = false;
|
||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
// フォロワーかどうか
|
||||||
const isFollowing = await this.followingsRepository.exist({
|
const isFollowing = await this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followeeId: packedNote.userId,
|
followeeId: packedNote.userId,
|
||||||
followerId: meId,
|
followerId: meId,
|
||||||
|
|
|
@ -104,7 +104,7 @@ export class PageEntityService {
|
||||||
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
|
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
|
||||||
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null)),
|
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null)),
|
||||||
likedCount: page.likedCount,
|
likedCount: page.likedCount,
|
||||||
isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined,
|
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -158,43 +158,43 @@ export class UserEntityService implements OnModuleInit {
|
||||||
followerId: me,
|
followerId: me,
|
||||||
followeeId: target,
|
followeeId: target,
|
||||||
}),
|
}),
|
||||||
this.followingsRepository.exist({
|
this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: target,
|
followerId: target,
|
||||||
followeeId: me,
|
followeeId: me,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.followRequestsRepository.exist({
|
this.followRequestsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: me,
|
followerId: me,
|
||||||
followeeId: target,
|
followeeId: target,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.followRequestsRepository.exist({
|
this.followRequestsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: target,
|
followerId: target,
|
||||||
followeeId: me,
|
followeeId: me,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.blockingsRepository.exist({
|
this.blockingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
blockerId: me,
|
blockerId: me,
|
||||||
blockeeId: target,
|
blockeeId: target,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.blockingsRepository.exist({
|
this.blockingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
blockerId: target,
|
blockerId: target,
|
||||||
blockeeId: me,
|
blockeeId: me,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.mutingsRepository.exist({
|
this.mutingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
muterId: me,
|
muterId: me,
|
||||||
muteeId: target,
|
muteeId: target,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.renoteMutingsRepository.exist({
|
this.renoteMutingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
muterId: me,
|
muterId: me,
|
||||||
muteeId: target,
|
muteeId: target,
|
||||||
|
@ -251,7 +251,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
/*
|
/*
|
||||||
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
||||||
|
|
||||||
const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({
|
const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
antennaId: In(myAntennas.map(x => x.id)),
|
antennaId: In(myAntennas.map(x => x.id)),
|
||||||
read: false,
|
read: false,
|
||||||
|
|
|
@ -27,6 +27,10 @@ export const packedEmojiSimpleSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
localOnly: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
isSensitive: {
|
isSensitive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
|
|
|
@ -168,12 +168,12 @@ export class SignupApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.emailRequiredForSignup) {
|
if (instance.emailRequiredForSignup) {
|
||||||
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
||||||
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
|
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check deleted username duplication
|
// Check deleted username duplication
|
||||||
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
|
if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) {
|
||||||
throw new FastifyReplyError(400, 'USED_USERNAME');
|
throw new FastifyReplyError(400, 'USED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
|
const exist = await this.promoNotesRepository.exists({ where: { noteId: note.id } });
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyPromoted);
|
throw new ApiError(meta.errors.alreadyPromoted);
|
||||||
|
|
|
@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const accessToken = secureRndstr(32);
|
const accessToken = secureRndstr(32);
|
||||||
|
|
||||||
// Fetch exist access token
|
// Fetch exist access token
|
||||||
const exist = await this.accessTokensRepository.exist({
|
const exist = await this.accessTokensRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
appId: session.appId,
|
appId: session.appId,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if already blocking
|
// Check if already blocking
|
||||||
const exist = await this.blockingsRepository.exist({
|
const exist = await this.blockingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check not blocking
|
// Check not blocking
|
||||||
const exist = await this.blockingsRepository.exist({
|
const exist = await this.blockingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
|
|
|
@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.noSuchClip);
|
throw new ApiError(meta.errors.noSuchClip);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.clipFavoritesRepository.exist({
|
const exist = await this.clipFavoritesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const exist = await this.driveFilesRepository.exist({
|
const exist = await this.driveFilesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
md5: ps.md5,
|
md5: ps.md5,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
// if already liked
|
// if already liked
|
||||||
const exist = await this.flashLikesRepository.exist({
|
const exist = await this.flashLikesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
flashId: flash.id,
|
flashId: flash.id,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
|
@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
const exist = await this.followingsRepository.exist({
|
const exist = await this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
|
|
|
@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check not following
|
// Check not following
|
||||||
const exist = await this.followingsRepository.exist({
|
const exist = await this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
// if already liked
|
// if already liked
|
||||||
const exist = await this.galleryLikesRepository.exist({
|
const exist = await this.galleryLikesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
postId: post.id,
|
postId: post.id,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const userExist = await this.usersRepository.exist({ where: { id: me.id } });
|
const userExist = await this.usersRepository.exists({ where: { id: me.id } });
|
||||||
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
|
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
|
||||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||||
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
|
|
|
@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
if (ps.tokenId) {
|
if (ps.tokenId) {
|
||||||
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
|
const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } });
|
||||||
|
|
||||||
if (tokenExist) {
|
if (tokenExist) {
|
||||||
await this.accessTokensRepository.delete({
|
await this.accessTokensRepository.delete({
|
||||||
|
@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (ps.token) {
|
} else if (ps.token) {
|
||||||
const tokenExist = await this.accessTokensRepository.exist({ where: { token: ps.token } });
|
const tokenExist = await this.accessTokensRepository.exists({ where: { token: ps.token } });
|
||||||
|
|
||||||
if (tokenExist) {
|
if (tokenExist) {
|
||||||
await this.accessTokensRepository.delete({
|
await this.accessTokensRepository.delete({
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if already muting
|
// Check if already muting
|
||||||
const exist = await this.mutingsRepository.exist({
|
const exist = await this.mutingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
muterId: muter.id,
|
muterId: muter.id,
|
||||||
muteeId: mutee.id,
|
muteeId: mutee.id,
|
||||||
|
|
|
@ -282,7 +282,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (renote.userId !== me.id) {
|
if (renote.userId !== me.id) {
|
||||||
const blockExist = await this.blockingsRepository.exist({
|
const blockExist = await this.blockingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
blockerId: renote.userId,
|
blockerId: renote.userId,
|
||||||
blockeeId: me.id,
|
blockeeId: me.id,
|
||||||
|
@ -330,7 +330,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (reply.userId !== me.id) {
|
if (reply.userId !== me.id) {
|
||||||
const blockExist = await this.blockingsRepository.exist({
|
const blockExist = await this.blockingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
blockerId: reply.userId,
|
blockerId: reply.userId,
|
||||||
blockeeId: me.id,
|
blockeeId: me.id,
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
|
|
||||||
// if already favorited
|
// if already favorited
|
||||||
const exist = await this.noteFavoritesRepository.exist({
|
const exist = await this.noteFavoritesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
// if already liked
|
// if already liked
|
||||||
const exist = await this.pageLikesRepository.exist({
|
const exist = await this.pageLikesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
pageId: page.id,
|
pageId: page.id,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
const exist = await this.promoReadsRepository.exist({
|
const exist = await this.promoReadsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
|
@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (me == null) {
|
if (me == null) {
|
||||||
throw new ApiError(meta.errors.forbidden);
|
throw new ApiError(meta.errors.forbidden);
|
||||||
} else if (me.id !== user.id) {
|
} else if (me.id !== user.id) {
|
||||||
const isFollowing = await this.followingsRepository.exist({
|
const isFollowing = await this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
followerId: me.id,
|
followerId: me.id,
|
||||||
|
|
|
@ -109,7 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (me == null) {
|
if (me == null) {
|
||||||
throw new ApiError(meta.errors.forbidden);
|
throw new ApiError(meta.errors.forbidden);
|
||||||
} else if (me.id !== user.id) {
|
} else if (me.id !== user.id) {
|
||||||
const isFollowing = await this.followingsRepository.exist({
|
const isFollowing = await this.followingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
followerId: me.id,
|
followerId: me.id,
|
||||||
|
|
|
@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const listExist = await this.userListsRepository.exist({
|
const listExist = await this.userListsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
id: ps.listId,
|
id: ps.listId,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
|
@ -121,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentUser.id !== me.id) {
|
if (currentUser.id !== me.id) {
|
||||||
const blockExist = await this.blockingsRepository.exist({
|
const blockExist = await this.blockingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
blockerId: currentUser.id,
|
blockerId: currentUser.id,
|
||||||
blockeeId: me.id,
|
blockeeId: me.id,
|
||||||
|
@ -132,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.userListMembershipsRepository.exist({
|
const exist = await this.userListMembershipsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
userListId: userList.id,
|
userListId: userList.id,
|
||||||
userId: currentUser.id,
|
userId: currentUser.id,
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const userListExist = await this.userListsRepository.exist({
|
const userListExist = await this.userListsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
id: ps.listId,
|
id: ps.listId,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
|
@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchList);
|
throw new ApiError(meta.errors.noSuchList);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.userListFavoritesRepository.exist({
|
const exist = await this.userListFavoritesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
userListId: ps.listId,
|
userListId: ps.listId,
|
||||||
|
|
|
@ -104,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (user.id !== me.id) {
|
if (user.id !== me.id) {
|
||||||
const blockExist = await this.blockingsRepository.exist({
|
const blockExist = await this.blockingsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
blockerId: user.id,
|
blockerId: user.id,
|
||||||
blockeeId: me.id,
|
blockeeId: me.id,
|
||||||
|
@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.userListMembershipsRepository.exist({
|
const exist = await this.userListMembershipsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
userListId: userList.id,
|
userListId: userList.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
|
@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
userListId: ps.listId,
|
userListId: ps.listId,
|
||||||
});
|
});
|
||||||
if (me !== null) {
|
if (me !== null) {
|
||||||
additionalProperties.isLiked = await this.userListFavoritesRepository.exist({
|
additionalProperties.isLiked = await this.userListFavoritesRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
userListId: ps.listId,
|
userListId: ps.listId,
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private userListFavoritesRepository: UserListFavoritesRepository,
|
private userListFavoritesRepository: UserListFavoritesRepository,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const userListExist = await this.userListsRepository.exist({
|
const userListExist = await this.userListsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
id: ps.listId,
|
id: ps.listId,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
|
|
|
@ -43,7 +43,7 @@ class UserListChannel extends Channel {
|
||||||
this.withRenotes = params.withRenotes ?? true;
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
|
|
||||||
// Check existence and owner
|
// Check existence and owner
|
||||||
const listExist = await this.userListsRepository.exist({
|
const listExist = await this.userListsRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
id: this.listId,
|
id: this.listId,
|
||||||
userId: this.user!.id,
|
userId: this.user!.id,
|
||||||
|
|
|
@ -216,7 +216,7 @@ describe('OAuth', () => {
|
||||||
assert.ok(location.searchParams.has('code'));
|
assert.ok(location.searchParams.has('code'));
|
||||||
assert.strictEqual(location.searchParams.get('state'), 'state');
|
assert.strictEqual(location.searchParams.get('state'), 'state');
|
||||||
// https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss
|
// https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss
|
||||||
assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local');
|
assert.strictEqual(location.searchParams.get('iss'), 'http://cherrypick.local');
|
||||||
|
|
||||||
const code = new URL(location).searchParams.get('code');
|
const code = new URL(location).searchParams.get('code');
|
||||||
assert.ok(code);
|
assert.ok(code);
|
||||||
|
@ -704,7 +704,7 @@ describe('OAuth', () => {
|
||||||
assert.strictEqual(response.status, 200);
|
assert.strictEqual(response.status, 200);
|
||||||
|
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
assert.strictEqual(body.issuer, 'http://misskey.local');
|
assert.strictEqual(body.issuer, 'http://cherrypick.local');
|
||||||
assert.ok(body.scopes_supported.includes('write:notes'));
|
assert.ok(body.scopes_supported.includes('write:notes'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
44
packages/backend/test/unit/ApMfmService.ts
Normal file
44
packages/backend/test/unit/ApMfmService.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
|
import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { MiNote } from '@/models/Note.js';
|
||||||
|
|
||||||
|
describe('ApMfmService', () => {
|
||||||
|
let apMfmService: ApMfmService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const app = await Test.createTestingModule({
|
||||||
|
imports: [GlobalModule, CoreModule],
|
||||||
|
}).compile();
|
||||||
|
apMfmService = app.get<ApMfmService>(ApMfmService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getNoteHtml', () => {
|
||||||
|
test('Do not provide _misskey_content for simple text', () => {
|
||||||
|
const note: MiNote = {
|
||||||
|
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
|
||||||
|
mentionedRemoteUsers: '[]',
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||||
|
|
||||||
|
assert.equal(noMisskeyContent, true, 'noMisskeyContent');
|
||||||
|
assert.equal(content, '<p>テキスト <a href="http://cherrypick.local/tags/タグ" rel="tag">#タグ</a> <a href="http://cherrypick.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com">https://example.com</a></p>', 'content');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Provide _misskey_content for MFM', () => {
|
||||||
|
const note: MiNote = {
|
||||||
|
text: '$[tada foo]',
|
||||||
|
mentionedRemoteUsers: '[]',
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||||
|
|
||||||
|
assert.equal(noMisskeyContent, false, 'noMisskeyContent');
|
||||||
|
assert.equal(content, '<p><i>foo</i></p>', 'content');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -33,6 +33,12 @@ describe('MfmService', () => {
|
||||||
const output = '<p><span>foo<br>bar<br>baz</span></p>';
|
const output = '<p><span>foo<br>bar<br>baz</span></p>';
|
||||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Do not generate unnecessary span', () => {
|
||||||
|
const input = 'foo $[tada bar]';
|
||||||
|
const output = '<p>foo <i>bar</i></p>';
|
||||||
|
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fromHtml', () => {
|
describe('fromHtml', () => {
|
||||||
|
|
|
@ -4,23 +4,6 @@ import { toPascal } from 'ts-case-convert';
|
||||||
import OpenAPIParser from '@readme/openapi-parser';
|
import OpenAPIParser from '@readme/openapi-parser';
|
||||||
import openapiTS from 'openapi-typescript';
|
import openapiTS from 'openapi-typescript';
|
||||||
|
|
||||||
function generateVersionHeaderComment(openApiDocs: OpenAPIV3_1.Document): string {
|
|
||||||
const contents = {
|
|
||||||
version: openApiDocs.info.version,
|
|
||||||
basedMisskeyVersion: openApiDocs.info.description,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const lines: string[] = [];
|
|
||||||
lines.push('/*');
|
|
||||||
for (const [key, value] of Object.entries(contents)) {
|
|
||||||
lines.push(` * ${key}: ${value}`);
|
|
||||||
}
|
|
||||||
lines.push(' */');
|
|
||||||
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateBaseTypes(
|
async function generateBaseTypes(
|
||||||
openApiDocs: OpenAPIV3_1.Document,
|
openApiDocs: OpenAPIV3_1.Document,
|
||||||
openApiJsonPath: string,
|
openApiJsonPath: string,
|
||||||
|
@ -37,9 +20,6 @@ async function generateBaseTypes(
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
lines.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
lines.push('');
|
|
||||||
|
|
||||||
const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true });
|
const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true });
|
||||||
lines.push(generatedTypes);
|
lines.push(generatedTypes);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
@ -60,8 +40,6 @@ async function generateSchemaEntities(
|
||||||
const schemaNames = Object.keys(schemas);
|
const schemaNames = Object.keys(schemas);
|
||||||
const typeAliasLines: string[] = [];
|
const typeAliasLines: string[] = [];
|
||||||
|
|
||||||
typeAliasLines.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
typeAliasLines.push('');
|
|
||||||
typeAliasLines.push(`import { components } from '${toImportPath(typeFileName)}';`);
|
typeAliasLines.push(`import { components } from '${toImportPath(typeFileName)}';`);
|
||||||
typeAliasLines.push(
|
typeAliasLines.push(
|
||||||
...schemaNames.map(it => `export type ${it} = components['schemas']['${it}'];`),
|
...schemaNames.map(it => `export type ${it} = components['schemas']['${it}'];`),
|
||||||
|
@ -120,9 +98,6 @@ async function generateEndpoints(
|
||||||
|
|
||||||
const entitiesOutputLine: string[] = [];
|
const entitiesOutputLine: string[] = [];
|
||||||
|
|
||||||
entitiesOutputLine.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
entitiesOutputLine.push('');
|
|
||||||
|
|
||||||
entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`);
|
entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`);
|
||||||
entitiesOutputLine.push('');
|
entitiesOutputLine.push('');
|
||||||
|
|
||||||
|
@ -140,9 +115,6 @@ async function generateEndpoints(
|
||||||
|
|
||||||
const endpointOutputLine: string[] = [];
|
const endpointOutputLine: string[] = [];
|
||||||
|
|
||||||
endpointOutputLine.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
endpointOutputLine.push('');
|
|
||||||
|
|
||||||
endpointOutputLine.push('import type {');
|
endpointOutputLine.push('import type {');
|
||||||
endpointOutputLine.push(
|
endpointOutputLine.push(
|
||||||
...[emptyRequest, emptyResponse, ...entities].map(it => '\t' + it.generateName() + ','),
|
...[emptyRequest, emptyResponse, ...entities].map(it => '\t' + it.generateName() + ','),
|
||||||
|
@ -188,9 +160,6 @@ async function generateApiClientJSDoc(
|
||||||
|
|
||||||
const endpointOutputLine: string[] = [];
|
const endpointOutputLine: string[] = [];
|
||||||
|
|
||||||
endpointOutputLine.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
endpointOutputLine.push('');
|
|
||||||
|
|
||||||
endpointOutputLine.push(`import type { SwitchCaseResponseType } from '${toImportPath(apiClientFileName)}';`);
|
endpointOutputLine.push(`import type { SwitchCaseResponseType } from '${toImportPath(apiClientFileName)}';`);
|
||||||
endpointOutputLine.push(`import type { Endpoints } from '${toImportPath(endpointsFileName)}';`);
|
endpointOutputLine.push(`import type { Endpoints } from '${toImportPath(endpointsFileName)}';`);
|
||||||
endpointOutputLine.push('');
|
endpointOutputLine.push('');
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "cherrypick-js",
|
"name": "cherrypick-js",
|
||||||
"version": "4.7.0-beta.1",
|
"version": "4.7.0-beta.2",
|
||||||
"basedMisskeyVersion": "2024.2.0-beta.8",
|
"basedMisskeyVersion": "2024.2.0-beta.10",
|
||||||
"description": "CherryPick SDK for JavaScript",
|
"description": "CherryPick SDK for JavaScript",
|
||||||
"types": "./built/dts/index.d.ts",
|
"types": "./built/dts/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type SharedOptions, rest } from 'msw';
|
import { type SharedOptions, http, HttpResponse } from 'msw';
|
||||||
|
|
||||||
export const onUnhandledRequest = ((req, print) => {
|
export const onUnhandledRequest = ((req, print) => {
|
||||||
if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) {
|
if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) {
|
||||||
|
@ -13,19 +13,31 @@ export const onUnhandledRequest = ((req, print) => {
|
||||||
}) satisfies SharedOptions['onUnhandledRequest'];
|
}) satisfies SharedOptions['onUnhandledRequest'];
|
||||||
|
|
||||||
export const commonHandlers = [
|
export const commonHandlers = [
|
||||||
rest.get('/fluent-emoji/:codepoints.png', async (req, res, ctx) => {
|
http.get('/fluent-emoji/:codepoints.png', async ({ params }) => {
|
||||||
const { codepoints } = req.params;
|
const { codepoints } = params;
|
||||||
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
|
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
|
||||||
return res(ctx.set('Content-Type', 'image/png'), ctx.body(value));
|
return new HttpResponse(value, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/png',
|
||||||
|
},
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
rest.get('/fluent-emojis/:codepoints.png', async (req, res, ctx) => {
|
http.get('/fluent-emojis/:codepoints.png', async ({ params }) => {
|
||||||
const { codepoints } = req.params;
|
const { codepoints } = params;
|
||||||
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
|
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
|
||||||
return res(ctx.set('Content-Type', 'image/png'), ctx.body(value));
|
return new HttpResponse(value, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/png',
|
||||||
|
},
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => {
|
http.get('/twemoji/:codepoints.svg', async ({ params }) => {
|
||||||
const { codepoints } = req.params;
|
const { codepoints } = params;
|
||||||
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob());
|
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob());
|
||||||
return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value));
|
return new HttpResponse(value, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/svg+xml',
|
||||||
|
},
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
|
@ -133,8 +133,8 @@
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "10.0.3",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"micromatch": "4.0.5",
|
"micromatch": "4.0.5",
|
||||||
"msw": "2.1.2",
|
"msw": "2.1.7",
|
||||||
"msw-storybook-addon": "1.10.0",
|
"msw-storybook-addon": "2.0.0-beta.1",
|
||||||
"nodemon": "3.0.3",
|
"nodemon": "3.0.3",
|
||||||
"prettier": "3.2.4",
|
"prettier": "3.2.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { abuseUserReport } from '../../.storybook/fakes.js';
|
import { abuseUserReport } from '../../.storybook/fakes.js';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import MkAbuseReport from './MkAbuseReport.vue';
|
import MkAbuseReport from './MkAbuseReport.vue';
|
||||||
|
@ -44,9 +44,9 @@ export const Default = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/admin/resolve-abuse-user-report', async (req, res, ctx) => {
|
http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => {
|
||||||
action('POST /api/admin/resolve-abuse-user-report')(await req.json());
|
action('POST /api/admin/resolve-abuse-user-report')(await request.json());
|
||||||
return res(ctx.json({}));
|
return HttpResponse.json({});
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { userDetailed } from '../../.storybook/fakes.js';
|
import { userDetailed } from '../../.storybook/fakes.js';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import MkAbuseReportWindow from './MkAbuseReportWindow.vue';
|
import MkAbuseReportWindow from './MkAbuseReportWindow.vue';
|
||||||
|
@ -44,9 +44,9 @@ export const Default = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/users/report-abuse', async (req, res, ctx) => {
|
http.post('/api/users/report-abuse', async ({ request }) => {
|
||||||
action('POST /api/users/report-abuse')(await req.json());
|
action('POST /api/users/report-abuse')(await request.json());
|
||||||
return res(ctx.json({}));
|
return HttpResponse.json({});
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { userDetailed } from '../../.storybook/fakes.js';
|
import { userDetailed } from '../../.storybook/fakes.js';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import MkAchievements from './MkAchievements.vue';
|
import MkAchievements from './MkAchievements.vue';
|
||||||
|
@ -39,8 +39,8 @@ export const Empty = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/users/achievements', (req, res, ctx) => {
|
http.post('/api/users/achievements', () => {
|
||||||
return res(ctx.json([]));
|
return HttpResponse.json([]);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -52,8 +52,8 @@ export const All = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/users/achievements', (req, res, ctx) => {
|
http.post('/api/users/achievements', () => {
|
||||||
return res(ctx.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 }))));
|
return HttpResponse.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 })));
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions';
|
||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { userDetailed } from '../../.storybook/fakes.js';
|
import { userDetailed } from '../../.storybook/fakes.js';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import MkAutocomplete from './MkAutocomplete.vue';
|
import MkAutocomplete from './MkAutocomplete.vue';
|
||||||
|
@ -99,11 +99,11 @@ export const User = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/users/search-by-username-and-host', (req, res, ctx) => {
|
http.post('/api/users/search-by-username-and-host', () => {
|
||||||
return res(ctx.json([
|
return HttpResponse.json([
|
||||||
userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'),
|
userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'),
|
||||||
userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'),
|
userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'),
|
||||||
]));
|
]);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -132,12 +132,12 @@ export const Hashtag = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/hashtags/search', (req, res, ctx) => {
|
http.post('/api/hashtags/search', () => {
|
||||||
return res(ctx.json([
|
return HttpResponse.json([
|
||||||
'気象警報注意報',
|
'気象警報注意報',
|
||||||
'気象警報',
|
'気象警報',
|
||||||
'気象情報',
|
'気象情報',
|
||||||
]));
|
]);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { userDetailed } from '../../.storybook/fakes.js';
|
import { userDetailed } from '../../.storybook/fakes.js';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import MkAvatars from './MkAvatars.vue';
|
import MkAvatars from './MkAvatars.vue';
|
||||||
|
@ -38,12 +38,12 @@ export const Default = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/users/show', (req, res, ctx) => {
|
http.post('/api/users/show', () => {
|
||||||
return res(ctx.json([
|
return HttpResponse.json([
|
||||||
userDetailed('17'),
|
userDetailed('17'),
|
||||||
userDetailed('20'),
|
userDetailed('20'),
|
||||||
userDetailed('18'),
|
userDetailed('18'),
|
||||||
]));
|
]);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div>
|
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { bundledLanguagesInfo } from 'shiki';
|
import { bundledLanguagesInfo } from 'shiki';
|
||||||
import type { BuiltinLanguage } from 'shiki';
|
import type { BuiltinLanguage } from 'shiki';
|
||||||
import { getHighlighter } from '@/scripts/code-highlighter.js';
|
import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
code: string;
|
code: string;
|
||||||
|
@ -21,11 +22,23 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const highlighter = await getHighlighter();
|
const highlighter = await getHighlighter();
|
||||||
|
const darkMode = defaultStore.reactiveState.darkMode;
|
||||||
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
|
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
|
||||||
|
|
||||||
|
const [lightThemeName, darkThemeName] = await Promise.all([
|
||||||
|
getTheme('light', true),
|
||||||
|
getTheme('dark', true),
|
||||||
|
]);
|
||||||
|
|
||||||
const html = computed(() => highlighter.codeToHtml(props.code, {
|
const html = computed(() => highlighter.codeToHtml(props.code, {
|
||||||
lang: codeLang.value,
|
lang: codeLang.value,
|
||||||
theme: 'dark-plus',
|
themes: {
|
||||||
|
fallback: 'dark-plus',
|
||||||
|
light: lightThemeName,
|
||||||
|
dark: darkThemeName,
|
||||||
|
},
|
||||||
|
defaultColor: false,
|
||||||
|
cssVariablePrefix: '--shiki-',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function fetchLanguage(to: string): Promise<void> {
|
async function fetchLanguage(to: string): Promise<void> {
|
||||||
|
@ -64,6 +77,16 @@ watch(() => props.lang, (to) => {
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--divider);
|
||||||
|
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
|
|
||||||
|
color: var(--shiki-fallback);
|
||||||
|
background-color: var(--shiki-fallback-bg);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: var(--shiki-fallback);
|
||||||
|
background-color: var(--shiki-fallback-bg);
|
||||||
|
}
|
||||||
|
|
||||||
& pre,
|
& pre,
|
||||||
& code {
|
& code {
|
||||||
|
@ -71,6 +94,26 @@ watch(() => props.lang, (to) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light.codeBlockRoot :global(.shiki) {
|
||||||
|
color: var(--shiki-light);
|
||||||
|
background-color: var(--shiki-light-bg);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: var(--shiki-light);
|
||||||
|
background-color: var(--shiki-light-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.codeBlockRoot :global(.shiki) {
|
||||||
|
color: var(--shiki-dark);
|
||||||
|
background-color: var(--shiki-dark-bg);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: var(--shiki-dark);
|
||||||
|
background-color: var(--shiki-dark-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.codeBlockRoot.codeEditor {
|
.codeBlockRoot.codeEditor {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -79,6 +122,7 @@ watch(() => props.lang, (to) => {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
min-height: 130px;
|
min-height: 130px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
min-width: calc(100% - 24px);
|
min-width: calc(100% - 24px);
|
||||||
|
@ -90,6 +134,11 @@ watch(() => props.lang, (to) => {
|
||||||
text-rendering: inherit;
|
text-rendering: inherit;
|
||||||
text-transform: inherit;
|
text-transform: inherit;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
||||||
|
& span {
|
||||||
|
display: inline-block;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -53,7 +53,6 @@ function copy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.codeBlockCopyButton {
|
.codeBlockCopyButton {
|
||||||
color: #D4D4D4;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
@ -67,8 +66,7 @@ function copy() {
|
||||||
.codeBlockFallbackRoot {
|
.codeBlockFallbackRoot {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: #D4D4D4;
|
background: var(--bg);
|
||||||
background: #1E1E1E;
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -91,8 +89,8 @@ function copy() {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #D4D4D4;
|
color: var(--fg);
|
||||||
background: #1E1E1E;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.codePlaceholderContainer {
|
.codePlaceholderContainer {
|
||||||
|
|
|
@ -196,10 +196,11 @@ watch(v, newValue => {
|
||||||
resize: none;
|
resize: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
caret-color: rgb(225, 228, 232);
|
caret-color: var(--fg);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
min-width: calc(100% - 24px);
|
min-width: calc(100% - 24px);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -210,6 +211,6 @@ watch(v, newValue => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea::selection {
|
.textarea::selection {
|
||||||
color: #fff;
|
color: var(--bg);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,8 +18,7 @@ const props = defineProps<{
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: #D4D4D4;
|
background: var(--bg);
|
||||||
background: #1E1E1E;
|
|
||||||
padding: .1em;
|
padding: .1em;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
|
@ -127,6 +128,7 @@ const props = withDefaults(defineProps<{
|
||||||
asDrawer?: boolean;
|
asDrawer?: boolean;
|
||||||
asWindow?: boolean;
|
asWindow?: boolean;
|
||||||
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
||||||
|
targetNote?: Misskey.entities.Note;
|
||||||
}>(), {
|
}>(), {
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
});
|
});
|
||||||
|
@ -341,7 +343,7 @@ watch(q, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
|
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
|
||||||
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false;
|
return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
|
|
|
@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:showPinned="showPinned"
|
:showPinned="showPinned"
|
||||||
:pinnedEmojis="pinnedEmojis"
|
:pinnedEmojis="pinnedEmojis"
|
||||||
:asReactionPicker="asReactionPicker"
|
:asReactionPicker="asReactionPicker"
|
||||||
|
:targetNote="targetNote"
|
||||||
:asDrawer="type === 'drawer'"
|
:asDrawer="type === 'drawer'"
|
||||||
:max-height="maxHeight"
|
:max-height="maxHeight"
|
||||||
@chosen="chosen"
|
@chosen="chosen"
|
||||||
|
@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import * as Misskey from 'cherrypick-js';
|
||||||
import { shallowRef } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||||
|
@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
pinnedEmojis?: string[],
|
pinnedEmojis?: string[],
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
|
targetNote?: Misskey.entities.Note;
|
||||||
choseAndClose?: boolean;
|
choseAndClose?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
manualShowing: null,
|
manualShowing: null,
|
||||||
|
|
|
@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:front="true"
|
:front="true"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/>
|
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
|
||||||
</MkWindow>
|
</MkWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
|
import * as Misskey from 'cherrypick-js';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ withDefaults(defineProps<{
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
|
targetNote?: Misskey.entities.Note
|
||||||
}>(), {
|
}>(), {
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
|
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
|
||||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js';
|
||||||
|
|
||||||
const rootEl = shallowRef<HTMLDivElement>();
|
const rootEl = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
@ -49,16 +49,16 @@ const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontal
|
||||||
// ▼ しきい値 ▼ //
|
// ▼ しきい値 ▼ //
|
||||||
|
|
||||||
// スワイプと判定される最小の距離
|
// スワイプと判定される最小の距離
|
||||||
const MIN_SWIPE_DISTANCE = 50;
|
const MIN_SWIPE_DISTANCE = 20;
|
||||||
|
|
||||||
// スワイプ時の動作を発火する最小の距離
|
// スワイプ時の動作を発火する最小の距離
|
||||||
const SWIPE_DISTANCE_THRESHOLD = 125;
|
const SWIPE_DISTANCE_THRESHOLD = 70;
|
||||||
|
|
||||||
// スワイプを中断するY方向の移動距離
|
// スワイプを中断するY方向の移動距離
|
||||||
const SWIPE_ABORT_Y_THRESHOLD = 75;
|
const SWIPE_ABORT_Y_THRESHOLD = 75;
|
||||||
|
|
||||||
// スワイプできる最大の距離
|
// スワイプできる最大の距離
|
||||||
const MAX_SWIPE_DISTANCE = 150;
|
const MAX_SWIPE_DISTANCE = 120;
|
||||||
|
|
||||||
// ▲ しきい値 ▲ //
|
// ▲ しきい値 ▲ //
|
||||||
|
|
||||||
|
@ -68,7 +68,6 @@ let startScreenY: number | null = null;
|
||||||
const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value));
|
const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value));
|
||||||
|
|
||||||
const pullDistance = ref(0);
|
const pullDistance = ref(0);
|
||||||
const isSwiping = ref(false);
|
|
||||||
const isSwipingForClass = ref(false);
|
const isSwipingForClass = ref(false);
|
||||||
let swipeAborted = false;
|
let swipeAborted = false;
|
||||||
|
|
||||||
|
@ -77,6 +76,8 @@ function touchStart(event: TouchEvent) {
|
||||||
|
|
||||||
if (event.touches.length !== 1) return;
|
if (event.touches.length !== 1) return;
|
||||||
|
|
||||||
|
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
|
||||||
|
|
||||||
startScreenX = event.touches[0].screenX;
|
startScreenX = event.touches[0].screenX;
|
||||||
startScreenY = event.touches[0].screenY;
|
startScreenY = event.touches[0].screenY;
|
||||||
}
|
}
|
||||||
|
@ -90,6 +91,8 @@ function touchMove(event: TouchEvent) {
|
||||||
|
|
||||||
if (swipeAborted) return;
|
if (swipeAborted) return;
|
||||||
|
|
||||||
|
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
|
||||||
|
|
||||||
let distanceX = event.touches[0].screenX - startScreenX;
|
let distanceX = event.touches[0].screenX - startScreenX;
|
||||||
let distanceY = event.touches[0].screenY - startScreenY;
|
let distanceY = event.touches[0].screenY - startScreenY;
|
||||||
|
|
||||||
|
@ -139,6 +142,8 @@ function touchEnd(event: TouchEvent) {
|
||||||
|
|
||||||
if (!isSwiping.value) return;
|
if (!isSwiping.value) return;
|
||||||
|
|
||||||
|
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
|
||||||
|
|
||||||
const distance = event.changedTouches[0].screenX - startScreenX;
|
const distance = event.changedTouches[0].screenX - startScreenX;
|
||||||
|
|
||||||
if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
|
if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
|
||||||
|
@ -162,6 +167,24 @@ function touchEnd(event: TouchEvent) {
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 横スワイプに関与する可能性のある要素を調べる */
|
||||||
|
function hasSomethingToDoWithXSwipe(el: HTMLElement) {
|
||||||
|
if (['INPUT', 'TEXTAREA'].includes(el.tagName)) return true;
|
||||||
|
if (el.isContentEditable) return true;
|
||||||
|
if (el.scrollWidth > el.clientWidth) return true;
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
if (['absolute', 'fixed', 'sticky'].includes(style.position)) return true;
|
||||||
|
if (['scroll', 'auto'].includes(style.overflowX)) return true;
|
||||||
|
if (style.touchAction === 'pan-x') return true;
|
||||||
|
|
||||||
|
if (el.parentElement && el.parentElement !== rootEl.value) {
|
||||||
|
return hasSomethingToDoWithXSwipe(el.parentElement);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined);
|
const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined);
|
||||||
|
|
||||||
watch(tabModel, (newTab, oldTab) => {
|
watch(tabModel, (newTab, oldTab) => {
|
||||||
|
@ -182,6 +205,7 @@ watch(tabModel, (newTab, oldTab) => {
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.transitionRoot {
|
.transitionRoot {
|
||||||
|
touch-action: pan-y pinch-zoom;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100%;
|
grid-template-columns: 100%;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { userDetailed, inviteCode } from '../../.storybook/fakes.js';
|
import { userDetailed, inviteCode } from '../../.storybook/fakes.js';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import MkInviteCode from './MkInviteCode.vue';
|
import MkInviteCode from './MkInviteCode.vue';
|
||||||
|
@ -39,8 +39,8 @@ export const Default = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/users/show', (req, res, ctx) => {
|
http.post('/api/users/show', ({ params }) => {
|
||||||
return res(ctx.json(userDetailed(req.params.userId as string)));
|
return HttpResponse.json(userDetailed(params.userId as string));
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -119,6 +119,7 @@ function close() {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .indicatorWithValue {
|
> .indicatorWithValue {
|
||||||
|
|
|
@ -314,7 +314,7 @@ const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entiti
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||||
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null);
|
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
|
||||||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||||
const isMFM = shouldMfmCollapsed(appearNote.value);
|
const isMFM = shouldMfmCollapsed(appearNote.value);
|
||||||
const collapsed = ref(appearNote.value.cw == null && (isLong || (isMFM && defaultStore.state.collapseDefault) || (appearNote.value.files.length > 0 && defaultStore.state.allMediaNoteCollapse)));
|
const collapsed = ref(appearNote.value.cw == null && (isLong || (isMFM && defaultStore.state.collapseDefault) || (appearNote.value.files.length > 0 && defaultStore.state.allMediaNoteCollapse)));
|
||||||
|
@ -508,7 +508,7 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
emit('reaction', reaction);
|
emit('reaction', reaction);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -374,7 +374,7 @@ const translating = ref(false);
|
||||||
const viewTextSource = ref(false);
|
const viewTextSource = ref(false);
|
||||||
const noNyaize = ref(false);
|
const noNyaize = ref(false);
|
||||||
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
||||||
const urls = parsed ? extractUrlFromMfm(parsed) : null;
|
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||||
const conversation = ref<Misskey.entities.Note[]>([]);
|
const conversation = ref<Misskey.entities.Note[]>([]);
|
||||||
const replies = ref<Misskey.entities.Note[]>([]);
|
const replies = ref<Misskey.entities.Note[]>([]);
|
||||||
|
@ -507,7 +507,7 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||||
toggleReaction(reaction);
|
toggleReaction(reaction);
|
||||||
}, () => {
|
}, () => {
|
||||||
focus();
|
focus();
|
||||||
|
|
|
@ -920,7 +920,7 @@ function cancel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertMention() {
|
function insertMention() {
|
||||||
os.selectUser().then(user => {
|
os.selectUser({ localOnly: localOnly.value, includeSelf: true }).then(user => {
|
||||||
insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
|
insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||||
|
import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
|
||||||
import { vibrate } from '@/scripts/vibrate.js';
|
import { vibrate } from '@/scripts/vibrate.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
|
@ -133,7 +134,7 @@ function moveEnd() {
|
||||||
function moving(event: TouchEvent | PointerEvent) {
|
function moving(event: TouchEvent | PointerEvent) {
|
||||||
if (!isPullStart.value || isRefreshing.value || disabled) return;
|
if (!isPullStart.value || isRefreshing.value || disabled) return;
|
||||||
|
|
||||||
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)) {
|
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value) || isHorizontalSwipeSwiping.value) {
|
||||||
pullDistance.value = 0;
|
pullDistance.value = 0;
|
||||||
isPullEnd.value = false;
|
isPullEnd.value = false;
|
||||||
moveEnd();
|
moveEnd();
|
||||||
|
@ -152,6 +153,10 @@ function moving(event: TouchEvent | PointerEvent) {
|
||||||
if (event.cancelable) event.preventDefault();
|
if (event.cancelable) event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pullDistance.value > SCROLL_STOP) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
|
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
|
||||||
|
|
||||||
if (!isPullEnd.value) isVibrate = false;
|
if (!isPullEnd.value) isVibrate = false;
|
||||||
|
|
|
@ -32,8 +32,9 @@ import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { customEmojis } from '@/custom-emojis.js';
|
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
|
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||||
|
import { customEmojis } from '@/custom-emojis.js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -51,6 +52,16 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const buttonEl = shallowRef<HTMLElement>();
|
const buttonEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
|
const isCustomEmoji = computed(() => props.reaction.includes(':'));
|
||||||
|
const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null);
|
||||||
|
|
||||||
|
const canToggle = computed(() => {
|
||||||
|
return !props.reaction.match(/@\w/) && $i
|
||||||
|
&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
|
||||||
|
|| !isCustomEmoji.value;
|
||||||
|
});
|
||||||
|
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
||||||
|
|
||||||
const reactionName = computed(() => {
|
const reactionName = computed(() => {
|
||||||
const r = props.reaction.replace(':', '');
|
const r = props.reaction.replace(':', '');
|
||||||
return r.slice(0, r.indexOf('@'));
|
return r.slice(0, r.indexOf('@'));
|
||||||
|
@ -58,16 +69,12 @@ const reactionName = computed(() => {
|
||||||
|
|
||||||
const alternative: ComputedRef<string | null> = computed(() => defaultStore.state.reactableRemoteReactionEnabled ? (customEmojis.value.find(it => it.name === reactionName.value)?.name ?? null) : null);
|
const alternative: ComputedRef<string | null> = computed(() => defaultStore.state.reactableRemoteReactionEnabled ? (customEmojis.value.find(it => it.name === reactionName.value)?.name ?? null) : null);
|
||||||
|
|
||||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
|
||||||
|
|
||||||
async function toggleReaction(ev: MouseEvent) {
|
async function toggleReaction(ev: MouseEvent) {
|
||||||
if (!canToggle.value) {
|
if (!canToggle.value) {
|
||||||
chooseAlternative(ev);
|
chooseAlternative(ev);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: その絵文字を使う権限があるかどうか確認
|
|
||||||
|
|
||||||
const oldReaction = props.note.myReaction;
|
const oldReaction = props.note.myReaction;
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
|
@ -146,8 +153,8 @@ function stealReaction(ev: MouseEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function menu(ev) {
|
async function menu(ev) {
|
||||||
if (!canToggle.value) return;
|
if (!canGetInfo.value) return;
|
||||||
if (!props.reaction.includes(':')) return;
|
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
type: 'label',
|
type: 'label',
|
||||||
text: `:${reactionName.value}:`,
|
text: `:${reactionName.value}:`,
|
||||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.caption"><slot name="caption"></slot></div>
|
<div :class="$style.caption"><slot name="caption"></slot></div>
|
||||||
|
|
||||||
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -141,6 +141,7 @@ function show() {
|
||||||
active: computed(() => v.value === option.props?.value),
|
active: computed(() => v.value === option.props?.value),
|
||||||
action: () => {
|
action: () => {
|
||||||
v.value = option.props?.value;
|
v.value = option.props?.value;
|
||||||
|
changed.value = true;
|
||||||
emit('changeByUser', v.value);
|
emit('changeByUser', v.value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -291,6 +292,10 @@ function show() {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
transition: transform 0.1s ease-out;
|
transition: transform 0.1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,7 +309,7 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
emit('reaction', reaction);
|
emit('reaction', reaction);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -17,9 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, watch, onMounted, onUnmounted, provide, shallowRef } from 'vue';
|
import { computed, watch, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||||
import Misskey from 'cherrypick-js';
|
import * as Misskey from 'cherrypick-js';
|
||||||
import { Connection } from 'cherrypick-js/built/streaming.js';
|
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
|
@ -93,8 +92,8 @@ function prepend(note) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let connection: Connection;
|
let connection: Misskey.ChannelConnection | null = null;
|
||||||
let connection2: Connection;
|
let connection2: Misskey.ChannelConnection | null = null;
|
||||||
let paginationQuery: Paging | null = null;
|
let paginationQuery: Paging | null = null;
|
||||||
|
|
||||||
const stream = useStream();
|
const stream = useStream();
|
||||||
|
@ -162,7 +161,7 @@ function connectChannel() {
|
||||||
roleId: props.role,
|
roleId: props.role,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (props.src !== 'directs' && props.src !== 'mentions') connection.on('note', prepend);
|
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnectChannel() {
|
function disconnectChannel() {
|
||||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
|
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
|
||||||
sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin"
|
sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin"
|
||||||
scrolling="no"
|
scrolling="no"
|
||||||
:allow="player.allow.join(';')"
|
:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
|
||||||
:class="$style.playerIframe"
|
:class="$style.playerIframe"
|
||||||
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
|
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
|
||||||
:style="{ border: 0 }"
|
:style="{ border: 0 }"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import { userDetailed } from '../../.storybook/fakes.js';
|
import { userDetailed } from '../../.storybook/fakes.js';
|
||||||
import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
|
import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
|
||||||
|
@ -38,17 +38,17 @@ export const Default = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/users', (req, res, ctx) => {
|
http.post('/api/users', () => {
|
||||||
return res(ctx.json([
|
return HttpResponse.json([
|
||||||
userDetailed('44'),
|
userDetailed('44'),
|
||||||
userDetailed('49'),
|
userDetailed('49'),
|
||||||
]));
|
]);
|
||||||
}),
|
}),
|
||||||
rest.post('/api/pinned-users', (req, res, ctx) => {
|
http.post('/api/pinned-users', () => {
|
||||||
return res(ctx.json([
|
return HttpResponse.json([
|
||||||
userDetailed('44'),
|
userDetailed('44'),
|
||||||
userDetailed('49'),
|
userDetailed('49'),
|
||||||
]));
|
]);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import Misskey from 'cherrypick-js';
|
import * as Misskey from 'cherrypick-js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import XUser from '@/components/MkUserSetupDialog.User.vue';
|
import XUser from '@/components/MkUserSetupDialog.User.vue';
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import { userDetailed } from '../../.storybook/fakes.js';
|
import { userDetailed } from '../../.storybook/fakes.js';
|
||||||
import MkUserSetupDialog from './MkUserSetupDialog.vue';
|
import MkUserSetupDialog from './MkUserSetupDialog.vue';
|
||||||
|
@ -38,17 +38,17 @@ export const Default = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/users', (req, res, ctx) => {
|
http.post('/api/users', () => {
|
||||||
return res(ctx.json([
|
return HttpResponse.json([
|
||||||
userDetailed('44'),
|
userDetailed('44'),
|
||||||
userDetailed('49'),
|
userDetailed('49'),
|
||||||
]));
|
]);
|
||||||
}),
|
}),
|
||||||
rest.post('/api/pinned-users', (req, res, ctx) => {
|
http.post('/api/pinned-users', () => {
|
||||||
return res(ctx.json([
|
return HttpResponse.json([
|
||||||
userDetailed('44'),
|
userDetailed('44'),
|
||||||
userDetailed('49'),
|
userDetailed('49'),
|
||||||
]));
|
]);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { commonHandlers } from '../../../.storybook/mocks.js';
|
import { commonHandlers } from '../../../.storybook/mocks.js';
|
||||||
import MkUrl from './MkUrl.vue';
|
import MkUrl from './MkUrl.vue';
|
||||||
export const Default = {
|
export const Default = {
|
||||||
|
@ -59,8 +59,8 @@ export const Default = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.get('/url', (req, res, ctx) => {
|
http.get('/url', () => {
|
||||||
return res(ctx.json({
|
return HttpResponse.json({
|
||||||
title: 'Misskey Hub',
|
title: 'Misskey Hub',
|
||||||
icon: 'https://misskey-hub.net/favicon.ico',
|
icon: 'https://misskey-hub.net/favicon.ico',
|
||||||
description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。',
|
description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。',
|
||||||
|
@ -74,7 +74,7 @@ export const Default = {
|
||||||
sitename: 'misskey-hub.net',
|
sitename: 'misskey-hub.net',
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
url: 'https://misskey-hub.net/',
|
url: 'https://misskey-hub.net/',
|
||||||
}));
|
});
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<KeepAlive :max="defaultStore.state.numberOfPageCache">
|
<KeepAlive
|
||||||
|
:max="defaultStore.state.numberOfPageCache"
|
||||||
|
:exclude="pageCacheController"
|
||||||
|
>
|
||||||
<Suspense :timeout="0">
|
<Suspense :timeout="0">
|
||||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
||||||
|
|
||||||
|
@ -16,9 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
|
import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
|
||||||
import { IRouter, Resolved } from '@/nirax.js';
|
import { IRouter, Resolved, RouteDef } from '@/nirax.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { globalEvents } from '@/events.js';
|
||||||
|
import MkLoadingPage from '@/pages/_loading_.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
router?: IRouter;
|
router?: IRouter;
|
||||||
|
@ -46,20 +51,47 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = resolveNested(router.current)!;
|
const current = resolveNested(router.current)!;
|
||||||
const currentPageComponent = shallowRef(current.route.component);
|
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
|
||||||
const currentPageProps = ref(current.props);
|
const currentPageProps = ref(current.props);
|
||||||
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
|
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
|
||||||
|
|
||||||
function onChange({ resolved, key: newKey }) {
|
function onChange({ resolved, key: newKey }) {
|
||||||
const current = resolveNested(resolved);
|
const current = resolveNested(resolved);
|
||||||
if (current == null) return;
|
if (current == null || 'redirect' in current.route) return;
|
||||||
currentPageComponent.value = current.route.component;
|
currentPageComponent.value = current.route.component;
|
||||||
currentPageProps.value = current.props;
|
currentPageProps.value = current.props;
|
||||||
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
|
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
// ページ遷移完了後に再びキャッシュを有効化
|
||||||
|
if (clearCacheRequested.value) {
|
||||||
|
clearCacheRequested.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
router.addListener('change', onChange);
|
router.addListener('change', onChange);
|
||||||
|
|
||||||
|
// #region キャッシュ制御
|
||||||
|
|
||||||
|
/**
|
||||||
|
* キャッシュクリアが有効になったら、全キャッシュをクリアする
|
||||||
|
*
|
||||||
|
* keepAlive側にwatcherがあるのですぐ消えるとはおもうけど、念のためページ遷移完了まではキャッシュを無効化しておく。
|
||||||
|
* キャッシュ有効時向けにexcludeを使いたい場合は、pageCacheControllerに並列に突っ込むのではなく、下に追記すること
|
||||||
|
*/
|
||||||
|
const pageCacheController = computed(() => clearCacheRequested.value ? /.*/ : undefined);
|
||||||
|
const clearCacheRequested = ref(false);
|
||||||
|
|
||||||
|
globalEvents.on('requestClearPageCache', () => {
|
||||||
|
if (_DEV_) console.log('clear page cache requested');
|
||||||
|
if (!clearCacheRequested.value) {
|
||||||
|
clearCacheRequested.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
router.removeListener('change', onChange);
|
router.removeListener('change', onChange);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import * as Misskey from 'cherrypick-js';
|
||||||
|
|
||||||
// TODO: 型付け
|
export const globalEvents = new EventEmitter<{
|
||||||
export const globalEvents = new EventEmitter();
|
themeChanged: () => void;
|
||||||
|
clientNotification: (notification: Misskey.entities.Notification) => void;
|
||||||
|
requestClearPageCache: () => void;
|
||||||
|
}>();
|
||||||
|
|
|
@ -293,13 +293,13 @@ const patronsWithIconWithMisskey = [{
|
||||||
icon: 'https://assets.misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg',
|
icon: 'https://assets.misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg',
|
||||||
}, {
|
}, {
|
||||||
name: 'taichan',
|
name: 'taichan',
|
||||||
icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.png',
|
icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.jpg',
|
||||||
}, {
|
}, {
|
||||||
name: '猫吉よりお',
|
name: '猫吉よりお',
|
||||||
icon: 'https://assets.misskey-hub.net/patrons/a11518b3b34b4536a4bdd7178ba76a7b.png',
|
icon: 'https://assets.misskey-hub.net/patrons/a11518b3b34b4536a4bdd7178ba76a7b.jpg',
|
||||||
}, {
|
}, {
|
||||||
name: '有栖かずみ',
|
name: '有栖かずみ',
|
||||||
icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.png',
|
icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.jpg',
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const patronsWithCherryPick = [
|
const patronsWithCherryPick = [
|
||||||
|
|
|
@ -148,9 +148,9 @@ function save() {
|
||||||
themeColor: themeColor.value === '' ? null : themeColor.value,
|
themeColor: themeColor.value === '' ? null : themeColor.value,
|
||||||
defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value,
|
defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value,
|
||||||
defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value,
|
defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value,
|
||||||
infoImageUrl: infoImageUrl.value,
|
infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value,
|
||||||
notFoundImageUrl: notFoundImageUrl.value,
|
notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value,
|
||||||
serverErrorImageUrl: serverErrorImageUrl.value,
|
serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value,
|
||||||
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
|
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
|
|
|
@ -893,7 +893,6 @@ function getGameImageDriveFile() {
|
||||||
formData.append('file', blob);
|
formData.append('file', blob);
|
||||||
formData.append('name', `bubble-game-${Date.now()}.png`);
|
formData.append('name', `bubble-game-${Date.now()}.png`);
|
||||||
formData.append('isSensitive', 'false');
|
formData.append('isSensitive', 'false');
|
||||||
formData.append('comment', 'null');
|
|
||||||
formData.append('i', $i.token);
|
formData.append('i', $i.token);
|
||||||
if (defaultStore.state.uploadFolder) {
|
if (defaultStore.state.uploadFolder) {
|
||||||
formData.append('folderId', defaultStore.state.uploadFolder);
|
formData.append('folderId', defaultStore.state.uploadFolder);
|
||||||
|
|
|
@ -57,6 +57,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
[$style.boardCell_prev]: engine.prevPos === i
|
[$style.boardCell_prev]: engine.prevPos === i
|
||||||
}]"
|
}]"
|
||||||
@click="putStone(i)"
|
@click="putStone(i)"
|
||||||
|
@mouseover="defaultStore.state.showingAnimatedImages === 'interaction' && stone === true ? playAnimation = true : ''"
|
||||||
|
@mouseout="defaultStore.state.showingAnimatedImages === 'interaction' && stone === true ? playAnimation = false : ''"
|
||||||
|
@touchstart="defaultStore.state.showingAnimatedImages === 'interaction' && stone === true ? playAnimation = true : ''"
|
||||||
|
@touchend="defaultStore.state.showingAnimatedImages === 'interaction' && stone === true ? playAnimation = false : ''"
|
||||||
>
|
>
|
||||||
<Transition
|
<Transition
|
||||||
:enterActiveClass="$style.transition_flip_enterActive"
|
:enterActiveClass="$style.transition_flip_enterActive"
|
||||||
|
@ -66,8 +70,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
mode="default"
|
mode="default"
|
||||||
>
|
>
|
||||||
<template v-if="useAvatarAsStone">
|
<template v-if="useAvatarAsStone">
|
||||||
<img v-if="stone === true" :class="$style.boardCellStone" :src="blackUser.avatarUrl ?? undefined"/>
|
<img v-if="stone === true" :class="$style.boardCellStone" :src="blackUserUrl ?? undefined"/>
|
||||||
<img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUser.avatarUrl ?? undefined"/>
|
<img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUserUrl ?? undefined"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<img v-if="stone === true" :class="$style.boardCellStone" src="/client-assets/reversi/stone_b.png"/>
|
<img v-if="stone === true" :class="$style.boardCellStone" src="/client-assets/reversi/stone_b.png"/>
|
||||||
|
@ -157,6 +161,8 @@ import { userPage } from '@/filters/user.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { confetti } from '@/scripts/confetti.js';
|
import { confetti } from '@/scripts/confetti.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
|
@ -227,6 +233,20 @@ const cellsStyle = computed(() => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const playAnimation = ref(true);
|
||||||
|
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation.value = false;
|
||||||
|
let playAnimationTimer = setTimeout(() => playAnimation.value = false, 5000);
|
||||||
|
const blackUserUrl = computed(() => {
|
||||||
|
if (blackUser.value.avatarUrl == null) return null;
|
||||||
|
if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation.value)) return getStaticImageUrl(blackUser.value.avatarUrl);
|
||||||
|
return blackUser.value.avatarUrl;
|
||||||
|
});
|
||||||
|
const whiteUserUrl = computed(() => {
|
||||||
|
if (whiteUser.value.avatarUrl == null) return null;
|
||||||
|
if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation.value)) return getStaticImageUrl(whiteUser.value.avatarUrl);
|
||||||
|
return whiteUser.value.avatarUrl;
|
||||||
|
});
|
||||||
|
|
||||||
watch(logPos, (v) => {
|
watch(logPos, (v) => {
|
||||||
if (!game.value.isEnded) return;
|
if (!game.value.isEnded) return;
|
||||||
engine.value = Reversi.Serializer.restoreGame({
|
engine.value = Reversi.Serializer.restoreGame({
|
||||||
|
@ -447,11 +467,23 @@ function share() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetTimer() {
|
||||||
|
playAnimation.value = true;
|
||||||
|
clearTimeout(playAnimationTimer);
|
||||||
|
playAnimationTimer = setTimeout(() => playAnimation.value = false, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.connection != null) {
|
if (props.connection != null) {
|
||||||
props.connection.on('log', onStreamLog);
|
props.connection.on('log', onStreamLog);
|
||||||
props.connection.on('ended', onStreamEnded);
|
props.connection.on('ended', onStreamEnded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||||
|
window.addEventListener('mousemove', resetTimer);
|
||||||
|
window.addEventListener('touchstart', resetTimer);
|
||||||
|
window.addEventListener('touchend', resetTimer);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
|
@ -473,6 +505,12 @@ onUnmounted(() => {
|
||||||
props.connection.off('log', onStreamLog);
|
props.connection.off('log', onStreamLog);
|
||||||
props.connection.off('ended', onStreamEnded);
|
props.connection.off('ended', onStreamEnded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||||
|
window.removeEventListener('mousemove', resetTimer);
|
||||||
|
window.removeEventListener('touchstart', resetTimer);
|
||||||
|
window.removeEventListener('touchend', resetTimer);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,7 @@ const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
|
||||||
const setDefaultEmoji = () => setDefault(pinnedEmojis);
|
const setDefaultEmoji = () => setDefault(pinnedEmojis);
|
||||||
|
|
||||||
function previewReaction(ev: MouseEvent) {
|
function previewReaction(ev: MouseEvent) {
|
||||||
reactionPicker.show(getHTMLElement(ev));
|
reactionPicker.show(getHTMLElement(ev), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewEmoji(ev: MouseEvent) {
|
function previewEmoji(ev: MouseEvent) {
|
||||||
|
|
|
@ -125,6 +125,7 @@ import { langmap } from '@/scripts/langmap.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { globalEvents } from '@/events.js';
|
||||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
@ -174,6 +175,7 @@ function saveFields() {
|
||||||
os.apiWithDialog('i/update', {
|
os.apiWithDialog('i/update', {
|
||||||
fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })),
|
fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })),
|
||||||
});
|
});
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
@ -192,6 +194,7 @@ function save() {
|
||||||
isBot: !!profile.isBot,
|
isBot: !!profile.isBot,
|
||||||
isCat: !!profile.isCat,
|
isCat: !!profile.isCat,
|
||||||
});
|
});
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
claimAchievement('profileFilled');
|
claimAchievement('profileFilled');
|
||||||
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
|
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
|
||||||
claimAchievement('setNameToSyuilo');
|
claimAchievement('setNameToSyuilo');
|
||||||
|
@ -234,6 +237,7 @@ function changeAvatar(ev) {
|
||||||
});
|
});
|
||||||
$i.avatarId = i.avatarId;
|
$i.avatarId = i.avatarId;
|
||||||
$i.avatarUrl = i.avatarUrl;
|
$i.avatarUrl = i.avatarUrl;
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
claimAchievement('profileFilled');
|
claimAchievement('profileFilled');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -260,6 +264,7 @@ function changeBanner(ev) {
|
||||||
});
|
});
|
||||||
$i.bannerId = i.bannerId;
|
$i.bannerId = i.bannerId;
|
||||||
$i.bannerUrl = i.bannerUrl;
|
$i.bannerUrl = i.bannerUrl;
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js';
|
||||||
import { fetchThemes, getThemes } from '@/theme-store.js';
|
import { fetchThemes, getThemes } from '@/theme-store.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
async function reloadAsk() {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'info',
|
||||||
|
text: i18n.ts.reloadToApplySetting,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
unisonReload();
|
||||||
|
}
|
||||||
|
|
||||||
const installedThemes = ref(getThemes());
|
const installedThemes = ref(getThemes());
|
||||||
const builtinThemes = getBuiltinThemesRef();
|
const builtinThemes = getBuiltinThemesRef();
|
||||||
|
@ -124,6 +136,7 @@ const lightThemeId = computed({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||||
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
|
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
|
||||||
|
@ -141,7 +154,7 @@ watch(wallpaper, () => {
|
||||||
} else {
|
} else {
|
||||||
miLocalStorage.setItem('wallpaper', wallpaper.value);
|
miLocalStorage.setItem('wallpaper', wallpaper.value);
|
||||||
}
|
}
|
||||||
location.reload();
|
reloadAsk();
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { rest } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { userDetailed } from '../../../.storybook/fakes.js';
|
import { userDetailed } from '../../../.storybook/fakes.js';
|
||||||
import { commonHandlers } from '../../../.storybook/mocks.js';
|
import { commonHandlers } from '../../../.storybook/mocks.js';
|
||||||
import home_ from './home.vue';
|
import home_ from './home.vue';
|
||||||
|
@ -39,12 +39,13 @@ export const Default = {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
...commonHandlers,
|
...commonHandlers,
|
||||||
rest.post('/api/users/notes', (req, res, ctx) => {
|
http.post('/api/users/notes', () => {
|
||||||
return res(ctx.json([]));
|
return HttpResponse.json([]);
|
||||||
}),
|
}),
|
||||||
rest.get('/api/charts/user/notes', (req, res, ctx) => {
|
http.get('/api/charts/user/notes', ({ request }) => {
|
||||||
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
const url = new URL(request.url);
|
||||||
return res(ctx.json({
|
const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||||
|
return HttpResponse.json({
|
||||||
total: Array.from({ length }, () => 0),
|
total: Array.from({ length }, () => 0),
|
||||||
inc: Array.from({ length }, () => 0),
|
inc: Array.from({ length }, () => 0),
|
||||||
dec: Array.from({ length }, () => 0),
|
dec: Array.from({ length }, () => 0),
|
||||||
|
@ -54,11 +55,12 @@ export const Default = {
|
||||||
renote: Array.from({ length }, () => 0),
|
renote: Array.from({ length }, () => 0),
|
||||||
withFile: Array.from({ length }, () => 0),
|
withFile: Array.from({ length }, () => 0),
|
||||||
},
|
},
|
||||||
}));
|
});
|
||||||
}),
|
}),
|
||||||
rest.get('/api/charts/user/pv', (req, res, ctx) => {
|
http.get('/api/charts/user/pv', ({ request }) => {
|
||||||
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
const url = new URL(request.url);
|
||||||
return res(ctx.json({
|
const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||||
|
return HttpResponse.json({
|
||||||
upv: {
|
upv: {
|
||||||
user: Array.from({ length }, () => 0),
|
user: Array.from({ length }, () => 0),
|
||||||
visitor: Array.from({ length }, () => 0),
|
visitor: Array.from({ length }, () => 0),
|
||||||
|
@ -67,7 +69,7 @@ export const Default = {
|
||||||
user: Array.from({ length }, () => 0),
|
user: Array.from({ length }, () => 0),
|
||||||
visitor: Array.from({ length }, () => 0),
|
visitor: Array.from({ length }, () => 0),
|
||||||
},
|
},
|
||||||
}));
|
});
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
import { deepMerge } from '@/scripts/merge.js';
|
||||||
|
|
||||||
type StateDef = Record<string, {
|
type StateDef = Record<string, {
|
||||||
where: 'account' | 'device' | 'deviceAccount';
|
where: 'account' | 'device' | 'deviceAccount';
|
||||||
|
@ -84,29 +85,9 @@ export class Storage<T extends StateDef> {
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* valueにないキーをdefからもらう(再帰的)\
|
|
||||||
* nullはそのまま、undefinedはdefの値
|
|
||||||
**/
|
|
||||||
private mergeObject<X>(value: X, def: X): X {
|
|
||||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
|
||||||
const result = structuredClone(value) as X;
|
|
||||||
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
|
|
||||||
result[k] = v;
|
|
||||||
} else if (this.isPureObject(v) && this.isPureObject(result[k])) {
|
|
||||||
const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>;
|
|
||||||
result[k] = this.mergeObject<typeof v>(child, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private mergeState<X>(value: X, def: X): X {
|
private mergeState<X>(value: X, def: X): X {
|
||||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||||
const merged = this.mergeObject(value, def);
|
const merged = deepMerge(value, def);
|
||||||
|
|
||||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
||||||
|
|
||||||
|
@ -258,7 +239,7 @@ export class Storage<T extends StateDef> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 特定のキーの、簡易的なgetter/setterを作ります
|
* 特定のキーの、簡易的なgetter/setterを作ります
|
||||||
* 主にvue場で設定コントロールのmodelとして使う用
|
* 主にvue上で設定コントロールのmodelとして使う用
|
||||||
*/
|
*/
|
||||||
public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): {
|
public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): {
|
||||||
get: () => T[K]['default'];
|
get: () => T[K]['default'];
|
||||||
|
|
|
@ -80,6 +80,10 @@ class MainRouterProxy implements IRouter {
|
||||||
return this.supplier().resolve(path);
|
return this.supplier().resolve(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.supplier().init();
|
||||||
|
}
|
||||||
|
|
||||||
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
|
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
|
||||||
return this.supplier().eventNames();
|
return this.supplier().eventNames();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as Misskey from 'cherrypick-js';
|
||||||
|
|
||||||
|
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean {
|
||||||
|
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
|
||||||
|
return !(emoji.localOnly && note.user.host !== me.host)
|
||||||
|
&& !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
|
||||||
|
&& (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
|
||||||
|
}
|
|
@ -8,13 +8,13 @@
|
||||||
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
||||||
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
||||||
|
|
||||||
type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[];
|
export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
|
||||||
|
|
||||||
export function deepClone<T extends Cloneable>(x: T): T {
|
export function deepClone<T extends Cloneable>(x: T): T {
|
||||||
if (typeof x === 'object') {
|
if (typeof x === 'object') {
|
||||||
if (x === null) return x;
|
if (x === null) return x;
|
||||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||||
const obj = {} as Record<string, Cloneable>;
|
const obj = {} as Record<string | number | symbol, Cloneable>;
|
||||||
for (const [k, v] of Object.entries(x)) {
|
for (const [k, v] of Object.entries(x)) {
|
||||||
obj[k] = v === undefined ? undefined : deepClone(v);
|
obj[k] = v === undefined ? undefined : deepClone(v);
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue