Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
아르페 2024-02-08 17:21:08 +09:00
commit 7f643f7961
No known key found for this signature in database
GPG key ID: B1EFBBF5C93FF78F
111 changed files with 707 additions and 337 deletions

View file

@ -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: オフライン表示のデザインを改善・多言語対応

View file

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

View file

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

View file

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

View file

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

View file

@ -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: "변칙 허용(완전 자유)"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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', () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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([
'気象警報注意報', '気象警報注意報',
'気象警報', '気象警報',
'気象情報', '気象情報',
])); ]);
}), }),
], ],
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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