Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
7f643f7961
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -49,6 +49,12 @@
|
|||
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
||||
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
||||
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
||||
- Enhance: リモートへの引用リノートと同一のリンクにはリンクプレビューを表示しないように
|
||||
- Enhance: コードのシンタックスハイライトにテーマを適用できるように
|
||||
- Enhance: リアクション権限がない場合、ハートにフォールバックするのではなくリアクションピッカーなどから打てないように
|
||||
- リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合
|
||||
- センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合
|
||||
- ロールが必要な絵文字をリアクションしようとした場合
|
||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||
|
@ -65,6 +71,10 @@
|
|||
- Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正
|
||||
- Fix: 画像をクロップ時、正常に完了できない問題の修正
|
||||
- Fix: キャプションが空の画像をクロップするとキャプションにnullという文字列が入ってしまう問題の修正
|
||||
- Fix: プロフィールを編集してもリロードするまで反映されない問題を修正
|
||||
- Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正
|
||||
- Fix: MkCodeEditorで行がずれていってしまう問題の修正
|
||||
- Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196
|
||||
|
||||
### Server
|
||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||
|
@ -79,6 +89,7 @@
|
|||
- Fix: properly handle cc followers
|
||||
- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec
|
||||
- Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122
|
||||
- Enhance: 連合向けのノート配信を軽量化 #13192
|
||||
|
||||
### Service Worker
|
||||
- Enhance: オフライン表示のデザインを改善・多言語対応
|
||||
|
|
|
@ -293,12 +293,10 @@ export const argTypes = {
|
|||
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
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
export const handlers = [
|
||||
rest.post('/api/notes/timeline', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.json([]),
|
||||
);
|
||||
http.post('/api/notes/timeline', ({ request }) => {
|
||||
return HttpResponse.json([]);
|
||||
}),
|
||||
];
|
||||
```
|
||||
|
|
|
@ -1237,7 +1237,7 @@ _initialAccountSetting:
|
|||
pushNotificationDescription: "Activant les notificacions emergents et permetrà rebre notificacions de {name} directament al teu dispositiu."
|
||||
initialAccountSettingCompleted: "Configuració del perfil completada!"
|
||||
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"
|
||||
skipAreYouSure: "Et vols saltar la configuració del perfil?"
|
||||
laterAreYouSure: "Vols continuar la configuració del perfil més tard?"
|
||||
|
@ -1248,10 +1248,10 @@ _initialTutorial:
|
|||
skipAreYouSure: "Sortir del tutorial?"
|
||||
_landing:
|
||||
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:
|
||||
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."
|
||||
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."
|
||||
|
@ -1265,7 +1265,7 @@ _initialTutorial:
|
|||
reactDone: "Pots desfer una reacció fent clic al botó '-'."
|
||||
_timeline:
|
||||
title: "El concepte de les línies de temps"
|
||||
description1: "Misskey mostra diferents línies de temps basades en l'ús (algunes poden no estar disponibles depenent de la política del servidor)"
|
||||
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"
|
||||
local: "Pots veure les notes dels usuaris del servidor."
|
||||
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}."
|
||||
_postNote:
|
||||
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:
|
||||
description: "Pots limitar qui pot veure les teves notes."
|
||||
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."
|
||||
_done:
|
||||
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:
|
||||
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."
|
||||
|
@ -1330,7 +1330,7 @@ _accountMigration:
|
|||
moveTo: "Migrar aquest compte a un altre"
|
||||
moveToLabel: "Compte al qual es vol migrar:"
|
||||
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"
|
||||
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."
|
||||
|
@ -1439,7 +1439,7 @@ _achievements:
|
|||
_login1000:
|
||||
title: "Mestre de les Notes III"
|
||||
description: "Vas iniciar sessió fa mil dies"
|
||||
flavor: "Gràcies per fer servir MissKey!"
|
||||
flavor: "Gràcies per fer servir CherryPick!"
|
||||
_noteClipped1:
|
||||
title: "He de retallar-te!"
|
||||
description: "Retalla la teva primera nota"
|
||||
|
@ -1499,18 +1499,18 @@ _achievements:
|
|||
title: "M'agraden els èxits "
|
||||
description: "Mira la teva llista d'assoliments durant més de 3 minuts"
|
||||
_iLoveMisskey:
|
||||
title: "Estimo Misskey"
|
||||
description: "Publica \"I ❤ #Misskey\""
|
||||
flavor: "L'equip de desenvolupament de Misskey agraeix el vostre suport!"
|
||||
title: "Estimo CherryPick"
|
||||
description: "Publica \"I ❤ #CherryPick\""
|
||||
flavor: "L'equip de desenvolupament de CherryPick agraeix el vostre suport!"
|
||||
_foundTreasure:
|
||||
title: "A la Recerca del Tresor"
|
||||
description: "Has trobat el tresor amagat"
|
||||
_client30min:
|
||||
title: "Parem una estona"
|
||||
description: "Mantingues obert Misskey per 30 minuts"
|
||||
description: "Mantingues obert CherryPick per 30 minuts"
|
||||
_client60min:
|
||||
title: "A totes amb Misskey"
|
||||
description: "Mantingues Misskey obert per 60 minuts"
|
||||
title: "A totes amb CherryPick"
|
||||
description: "Mantingues CherryPick obert per 60 minuts"
|
||||
_noteDeletedWithin1min:
|
||||
title: "No et preocupis"
|
||||
description: "Esborra una nota al minut de publicar-la"
|
||||
|
|
|
@ -1235,7 +1235,7 @@ _initialTutorial:
|
|||
description: "Hier kannst du sehen, wie CherryPick funktioniert"
|
||||
_note:
|
||||
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."
|
||||
_reaction:
|
||||
title: "Was sind Reaktionen?"
|
||||
|
|
|
@ -541,7 +541,7 @@ volume: "음량"
|
|||
masterVolume: "대빵 음량"
|
||||
notUseSound: "음소거하기"
|
||||
useSoundOnlyWhenActive: "CherryPick이 활성화되어 있을 때만 소리 내기"
|
||||
details: "좀 더"
|
||||
details: "자세히"
|
||||
chooseEmoji: "이모지 선택"
|
||||
unableToProcess: "작업 다 몬 했십니다"
|
||||
recentUsed: "최근 쓴 놈"
|
||||
|
@ -691,7 +691,7 @@ _achievements:
|
|||
_myNoteFavorited1:
|
||||
description: "다런 사람이 내 노트럴 질겨찾기에 담앗십니다"
|
||||
_iLoveMisskey:
|
||||
description: "“I ❤ #Misskey”럴 섰어예"
|
||||
description: "“I ❤ #CherryPick”럴 섰어예"
|
||||
_postedAt0min0sec:
|
||||
description: "0분 0초에 노트를 섰어예"
|
||||
_tutorialCompleted:
|
||||
|
|
|
@ -2369,9 +2369,9 @@ _permissions:
|
|||
"write:report-abuse": "위반 내용 신고하기"
|
||||
_auth:
|
||||
shareAccessTitle: "애플리케이션 접근 허가"
|
||||
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용할까요?"
|
||||
shareAccess: "‘{name}’에서 계정에 접근하는 것을 허용할까요?"
|
||||
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용할까요?"
|
||||
permission: "{name}에서 다음 권한을 요청했어요"
|
||||
permission: "‘{name}’에서 다음 권한을 요청했어요"
|
||||
permissionAsk: "이 앱은 다음 권한을 요청하고 있습니다"
|
||||
pleaseGoBack: "앱으로 돌아가서 계속 진행해 주세요"
|
||||
callback: "앱으로 돌아갈게요!"
|
||||
|
@ -2837,7 +2837,7 @@ _reversi:
|
|||
freeMatch: "프리 매치"
|
||||
lookingForPlayer: "대전 상대를 찾고 있어요"
|
||||
gameCanceled: "대국이 취소되었어요"
|
||||
shareToTlTheGameWhenStart: "시작 시 대국을 타임라인에 게시"
|
||||
shareToTlTheGameWhenStart: "대국 시작 시 타임라인에 대국 게시하기"
|
||||
iStartedAGame: "대국이 시작되었어요! #MisskeyReversi"
|
||||
opponentHasSettingsChanged: "상대방이 게임 설정을 변경했어요"
|
||||
allowIrregularRules: "변칙 허용(완전 자유)"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "lycheebridge",
|
||||
"version": "2024.2.0-alpha.3",
|
||||
"basedCherryPickVersion": "4.7.0-beta.1",
|
||||
"basedCherryPickVersion": "4.7.0-beta.2",
|
||||
"basedMisskeyVersion": "2024.2.0-beta.10",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
|
|
|
@ -388,7 +388,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
*/
|
||||
@bindThis
|
||||
public checkDuplicate(name: string): Promise<boolean> {
|
||||
return this.emojisRepository.exist({ where: { name, host: IsNull() } });
|
||||
return this.emojisRepository.exists({ where: { name, host: IsNull() } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -419,6 +419,10 @@ export class MfmService {
|
|||
},
|
||||
|
||||
text: (node) => {
|
||||
if (!node.props.text.match(/[\r\n]/)) {
|
||||
return doc.createTextNode(node.props.text);
|
||||
}
|
||||
|
||||
const el = doc.createElement('span');
|
||||
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
||||
|
||||
|
|
|
@ -628,7 +628,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
if (data.reply) {
|
||||
// 通知
|
||||
if (data.reply.userHost === null) {
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
|
||||
where: {
|
||||
userId: data.reply.userId,
|
||||
threadId: data.reply.threadId ?? data.reply.id,
|
||||
|
@ -766,7 +766,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) {
|
||||
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
|
||||
where: {
|
||||
userId: u.id,
|
||||
threadId: note.threadId ?? note.id,
|
||||
|
|
|
@ -49,7 +49,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
//#endregion
|
||||
|
||||
// スレッドミュート
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
|
||||
where: {
|
||||
userId: userId,
|
||||
threadId: note.threadId ?? note.id,
|
||||
|
@ -70,7 +70,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
|
||||
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
|
||||
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;
|
||||
|
||||
|
|
|
@ -74,12 +74,12 @@ export class SignupService {
|
|||
const secret = generateUserToken();
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
let autoAccept = false;
|
||||
|
||||
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
|
@ -156,7 +156,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
|
||||
// フォローしているユーザーは自動承認オプション
|
||||
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
|
||||
const isFollowed = await this.followingsRepository.exist({
|
||||
const isFollowed = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followerId: followee.id,
|
||||
followeeId: follower.id,
|
||||
|
@ -170,7 +170,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
if (followee.isLocked && !autoAccept) {
|
||||
autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs(
|
||||
follower,
|
||||
(oldSrc, newSrc) => this.followingsRepository.exist({
|
||||
(oldSrc, newSrc) => this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: followee.id,
|
||||
followerId: newSrc.id,
|
||||
|
@ -233,7 +233,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
|
||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||
|
||||
const requestExist = await this.followRequestsRepository.exist({
|
||||
const requestExist = await this.followRequestsRepository.exists({
|
||||
where: {
|
||||
followeeId: followee.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: {
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
|
|
|
@ -683,7 +683,7 @@ export class ApInboxService {
|
|||
return 'skip: follower not found';
|
||||
}
|
||||
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: actor.id,
|
||||
|
@ -740,14 +740,14 @@ export class ApInboxService {
|
|||
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
|
||||
}
|
||||
|
||||
const requestExist = await this.followRequestsRepository.exist({
|
||||
const requestExist = await this.followRequestsRepository.exists({
|
||||
where: {
|
||||
followerId: actor.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followerId: actor.id,
|
||||
followeeId: followee.id,
|
||||
|
|
|
@ -25,8 +25,21 @@ export class ApMfmService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public getNoteHtml(note: MiNote): string | null {
|
||||
if (!note.text) return '';
|
||||
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
||||
public getNoteHtml(note: MiNote, apAppend?: string) {
|
||||
let noMisskeyContent = false;
|
||||
const srcMfm = (note.text ?? '') + (apAppend ?? '');
|
||||
|
||||
const parsed = mfm.parse(srcMfm);
|
||||
|
||||
if (!apAppend && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
|
||||
noMisskeyContent = true;
|
||||
}
|
||||
|
||||
const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers));
|
||||
|
||||
return {
|
||||
content,
|
||||
noMisskeyContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -330,7 +330,7 @@ export class ApRendererService {
|
|||
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
||||
|
||||
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 (inReplyToNote.uri) {
|
||||
|
@ -394,17 +394,15 @@ export class ApRendererService {
|
|||
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||
}
|
||||
|
||||
let apText = text;
|
||||
let apAppend = '';
|
||||
|
||||
if (quote) {
|
||||
apText += `\n\nRE: ${quote}`;
|
||||
apAppend += `\n\nRE: ${quote}`;
|
||||
}
|
||||
|
||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||
|
||||
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
||||
text: apText,
|
||||
}));
|
||||
const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend);
|
||||
|
||||
const emojis = await this.getEmojis(note.emojis);
|
||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||
|
@ -417,9 +415,6 @@ export class ApRendererService {
|
|||
|
||||
const asPoll = poll ? {
|
||||
type: 'Question',
|
||||
content: this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
||||
text: text,
|
||||
})),
|
||||
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
|
||||
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
||||
type: 'Note',
|
||||
|
@ -453,11 +448,13 @@ export class ApRendererService {
|
|||
attributedTo,
|
||||
summary: summary ?? undefined,
|
||||
content: content ?? undefined,
|
||||
...(noMisskeyContent ? {} : {
|
||||
_misskey_content: text,
|
||||
source: {
|
||||
content: text,
|
||||
mediaType: 'text/x.misskeymarkdown',
|
||||
},
|
||||
}),
|
||||
_misskey_quote: quote,
|
||||
quoteUrl: quote,
|
||||
published: this.idService.parse(note.id).date.toISOString(),
|
||||
|
@ -658,6 +655,7 @@ export class ApRendererService {
|
|||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{
|
||||
Key: 'sec:Key',
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
|
|
|
@ -51,14 +51,14 @@ export class ChannelEntityService {
|
|||
|
||||
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: {
|
||||
followerId: meId,
|
||||
followeeId: channel.id,
|
||||
},
|
||||
}) : false;
|
||||
|
||||
const isFavorited = meId ? await this.channelFavoritesRepository.exist({
|
||||
const isFavorited = meId ? await this.channelFavoritesRepository.exists({
|
||||
where: {
|
||||
userId: meId,
|
||||
channelId: channel.id,
|
||||
|
|
|
@ -46,7 +46,7 @@ export class ClipEntityService {
|
|||
description: clip.description,
|
||||
isPublic: clip.isPublic,
|
||||
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||
isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
||||
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ export class EmojiEntityService {
|
|||
category: emoji.category,
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
localOnly: emoji.localOnly ? true : undefined,
|
||||
isSensitive: emoji.isSensitive ? true : undefined,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
||||
};
|
||||
|
|
|
@ -47,7 +47,7 @@ export class FlashEntityService {
|
|||
summary: flash.summary,
|
||||
script: flash.script,
|
||||
likedCount: flash.likedCount,
|
||||
isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
||||
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ export class GalleryPostEntityService {
|
|||
tags: post.tags.length > 0 ? post.tags : undefined,
|
||||
isSensitive: post.isSensitive,
|
||||
likedCount: post.likedCount,
|
||||
isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined,
|
||||
isLiked: meId ? await this.galleryLikesRepository.exists({ where: { postId: post.id, userId: meId } }) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
|
|
|
@ -104,7 +104,7 @@ export class PageEntityService {
|
|||
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)),
|
||||
likedCount: page.likedCount,
|
||||
isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined,
|
||||
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -158,43 +158,43 @@ export class UserEntityService implements OnModuleInit {
|
|||
followerId: me,
|
||||
followeeId: target,
|
||||
}),
|
||||
this.followingsRepository.exist({
|
||||
this.followingsRepository.exists({
|
||||
where: {
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
},
|
||||
}),
|
||||
this.followRequestsRepository.exist({
|
||||
this.followRequestsRepository.exists({
|
||||
where: {
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
},
|
||||
}),
|
||||
this.followRequestsRepository.exist({
|
||||
this.followRequestsRepository.exists({
|
||||
where: {
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
},
|
||||
}),
|
||||
this.blockingsRepository.exist({
|
||||
this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: me,
|
||||
blockeeId: target,
|
||||
},
|
||||
}),
|
||||
this.blockingsRepository.exist({
|
||||
this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: target,
|
||||
blockeeId: me,
|
||||
},
|
||||
}),
|
||||
this.mutingsRepository.exist({
|
||||
this.mutingsRepository.exists({
|
||||
where: {
|
||||
muterId: me,
|
||||
muteeId: target,
|
||||
},
|
||||
}),
|
||||
this.renoteMutingsRepository.exist({
|
||||
this.renoteMutingsRepository.exists({
|
||||
where: {
|
||||
muterId: me,
|
||||
muteeId: target,
|
||||
|
@ -251,7 +251,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
/*
|
||||
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: {
|
||||
antennaId: In(myAntennas.map(x => x.id)),
|
||||
read: false,
|
||||
|
|
|
@ -27,6 +27,10 @@ export const packedEmojiSimpleSchema = {
|
|||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
localOnly: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
|
|
|
@ -168,12 +168,12 @@ export class SignupApiService {
|
|||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw e;
|
||||
});
|
||||
|
||||
const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
|
||||
const exist = await this.promoNotesRepository.exists({ where: { noteId: note.id } });
|
||||
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyPromoted);
|
||||
|
|
|
@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const accessToken = secureRndstr(32);
|
||||
|
||||
// Fetch exist access token
|
||||
const exist = await this.accessTokensRepository.exist({
|
||||
const exist = await this.accessTokensRepository.exists({
|
||||
where: {
|
||||
appId: session.appId,
|
||||
userId: me.id,
|
||||
|
|
|
@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
// Check if already blocking
|
||||
const exist = await this.blockingsRepository.exist({
|
||||
const exist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
|
|
|
@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
// Check not blocking
|
||||
const exist = await this.blockingsRepository.exist({
|
||||
const exist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
|
|
|
@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
|
||||
const exist = await this.clipFavoritesRepository.exist({
|
||||
const exist = await this.clipFavoritesRepository.exists({
|
||||
where: {
|
||||
clipId: clip.id,
|
||||
userId: me.id,
|
||||
|
|
|
@ -38,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private driveFilesRepository: DriveFilesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const exist = await this.driveFilesRepository.exist({
|
||||
const exist = await this.driveFilesRepository.exists({
|
||||
where: {
|
||||
md5: ps.md5,
|
||||
userId: me.id,
|
||||
|
|
|
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await this.flashLikesRepository.exist({
|
||||
const exist = await this.flashLikesRepository.exists({
|
||||
where: {
|
||||
flashId: flash.id,
|
||||
userId: me.id,
|
||||
|
|
|
@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
// Check if already following
|
||||
const exist = await this.followingsRepository.exist({
|
||||
const exist = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
|
|
|
@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
// Check not following
|
||||
const exist = await this.followingsRepository.exist({
|
||||
const exist = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
|
|
|
@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await this.galleryLikesRepository.exist({
|
||||
const exist = await this.galleryLikesRepository.exists({
|
||||
where: {
|
||||
postId: post.id,
|
||||
userId: me.id,
|
||||
|
|
|
@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private downloadService: DownloadService,
|
||||
) {
|
||||
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);
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
||||
|
|
|
@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
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) {
|
||||
await this.accessTokensRepository.delete({
|
||||
|
@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
}
|
||||
} 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) {
|
||||
await this.accessTokensRepository.delete({
|
||||
|
|
|
@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
// Check if already muting
|
||||
const exist = await this.mutingsRepository.exist({
|
||||
const exist = await this.mutingsRepository.exists({
|
||||
where: {
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
|
|
|
@ -282,7 +282,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
// Check blocking
|
||||
if (renote.userId !== me.id) {
|
||||
const blockExist = await this.blockingsRepository.exist({
|
||||
const blockExist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: renote.userId,
|
||||
blockeeId: me.id,
|
||||
|
@ -330,7 +330,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
// Check blocking
|
||||
if (reply.userId !== me.id) {
|
||||
const blockExist = await this.blockingsRepository.exist({
|
||||
const blockExist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: reply.userId,
|
||||
blockeeId: me.id,
|
||||
|
|
|
@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
// if already favorited
|
||||
const exist = await this.noteFavoritesRepository.exist({
|
||||
const exist = await this.noteFavoritesRepository.exists({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
userId: me.id,
|
||||
|
|
|
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await this.pageLikesRepository.exist({
|
||||
const exist = await this.pageLikesRepository.exists({
|
||||
where: {
|
||||
pageId: page.id,
|
||||
userId: me.id,
|
||||
|
|
|
@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw err;
|
||||
});
|
||||
|
||||
const exist = await this.promoReadsRepository.exist({
|
||||
const exist = await this.promoReadsRepository.exists({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
userId: me.id,
|
||||
|
|
|
@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
|
|
|
@ -109,7 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
|
|
|
@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const listExist = await this.userListsRepository.exist({
|
||||
const listExist = await this.userListsRepository.exists({
|
||||
where: {
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
|
@ -121,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
if (currentUser.id !== me.id) {
|
||||
const blockExist = await this.blockingsRepository.exist({
|
||||
const blockExist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: currentUser.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: {
|
||||
userListId: userList.id,
|
||||
userId: currentUser.id,
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const userListExist = await this.userListsRepository.exist({
|
||||
const userListExist = await this.userListsRepository.exists({
|
||||
where: {
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
|
@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
const exist = await this.userListFavoritesRepository.exist({
|
||||
const exist = await this.userListFavoritesRepository.exists({
|
||||
where: {
|
||||
userId: me.id,
|
||||
userListId: ps.listId,
|
||||
|
|
|
@ -104,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
// Check blocking
|
||||
if (user.id !== me.id) {
|
||||
const blockExist = await this.blockingsRepository.exist({
|
||||
const blockExist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: user.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: {
|
||||
userListId: userList.id,
|
||||
userId: user.id,
|
||||
|
|
|
@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
userListId: ps.listId,
|
||||
});
|
||||
if (me !== null) {
|
||||
additionalProperties.isLiked = await this.userListFavoritesRepository.exist({
|
||||
additionalProperties.isLiked = await this.userListFavoritesRepository.exists({
|
||||
where: {
|
||||
userId: me.id,
|
||||
userListId: ps.listId,
|
||||
|
|
|
@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private userListFavoritesRepository: UserListFavoritesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const userListExist = await this.userListsRepository.exist({
|
||||
const userListExist = await this.userListsRepository.exists({
|
||||
where: {
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
|
|
|
@ -43,7 +43,7 @@ class UserListChannel extends Channel {
|
|||
this.withRenotes = params.withRenotes ?? true;
|
||||
|
||||
// Check existence and owner
|
||||
const listExist = await this.userListsRepository.exist({
|
||||
const listExist = await this.userListsRepository.exists({
|
||||
where: {
|
||||
id: this.listId,
|
||||
userId: this.user!.id,
|
||||
|
|
|
@ -216,7 +216,7 @@ describe('OAuth', () => {
|
|||
assert.ok(location.searchParams.has('code'));
|
||||
assert.strictEqual(location.searchParams.get('state'), 'state');
|
||||
// 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');
|
||||
assert.ok(code);
|
||||
|
@ -704,7 +704,7 @@ describe('OAuth', () => {
|
|||
assert.strictEqual(response.status, 200);
|
||||
|
||||
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'));
|
||||
});
|
||||
|
||||
|
|
44
packages/backend/test/unit/ApMfmService.ts
Normal file
44
packages/backend/test/unit/ApMfmService.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import * as assert from 'assert';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
|
||||
describe('ApMfmService', () => {
|
||||
let apMfmService: ApMfmService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = await Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule],
|
||||
}).compile();
|
||||
apMfmService = app.get<ApMfmService>(ApMfmService);
|
||||
});
|
||||
|
||||
describe('getNoteHtml', () => {
|
||||
test('Do not provide _misskey_content for simple text', () => {
|
||||
const note: MiNote = {
|
||||
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
|
||||
mentionedRemoteUsers: '[]',
|
||||
} as any;
|
||||
|
||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||
|
||||
assert.equal(noMisskeyContent, true, 'noMisskeyContent');
|
||||
assert.equal(content, '<p>テキスト <a href="http://cherrypick.local/tags/タグ" rel="tag">#タグ</a> <a href="http://cherrypick.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com">https://example.com</a></p>', 'content');
|
||||
});
|
||||
|
||||
test('Provide _misskey_content for MFM', () => {
|
||||
const note: MiNote = {
|
||||
text: '$[tada foo]',
|
||||
mentionedRemoteUsers: '[]',
|
||||
} as any;
|
||||
|
||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||
|
||||
assert.equal(noMisskeyContent, false, 'noMisskeyContent');
|
||||
assert.equal(content, '<p><i>foo</i></p>', 'content');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -33,6 +33,12 @@ describe('MfmService', () => {
|
|||
const output = '<p><span>foo<br>bar<br>baz</span></p>';
|
||||
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', () => {
|
||||
|
|
|
@ -4,23 +4,6 @@ import { toPascal } from 'ts-case-convert';
|
|||
import OpenAPIParser from '@readme/openapi-parser';
|
||||
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(
|
||||
openApiDocs: OpenAPIV3_1.Document,
|
||||
openApiJsonPath: string,
|
||||
|
@ -37,9 +20,6 @@ async function generateBaseTypes(
|
|||
}
|
||||
lines.push('');
|
||||
|
||||
lines.push(generateVersionHeaderComment(openApiDocs));
|
||||
lines.push('');
|
||||
|
||||
const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true });
|
||||
lines.push(generatedTypes);
|
||||
lines.push('');
|
||||
|
@ -60,8 +40,6 @@ async function generateSchemaEntities(
|
|||
const schemaNames = Object.keys(schemas);
|
||||
const typeAliasLines: string[] = [];
|
||||
|
||||
typeAliasLines.push(generateVersionHeaderComment(openApiDocs));
|
||||
typeAliasLines.push('');
|
||||
typeAliasLines.push(`import { components } from '${toImportPath(typeFileName)}';`);
|
||||
typeAliasLines.push(
|
||||
...schemaNames.map(it => `export type ${it} = components['schemas']['${it}'];`),
|
||||
|
@ -120,9 +98,6 @@ async function generateEndpoints(
|
|||
|
||||
const entitiesOutputLine: string[] = [];
|
||||
|
||||
entitiesOutputLine.push(generateVersionHeaderComment(openApiDocs));
|
||||
entitiesOutputLine.push('');
|
||||
|
||||
entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`);
|
||||
entitiesOutputLine.push('');
|
||||
|
||||
|
@ -140,9 +115,6 @@ async function generateEndpoints(
|
|||
|
||||
const endpointOutputLine: string[] = [];
|
||||
|
||||
endpointOutputLine.push(generateVersionHeaderComment(openApiDocs));
|
||||
endpointOutputLine.push('');
|
||||
|
||||
endpointOutputLine.push('import type {');
|
||||
endpointOutputLine.push(
|
||||
...[emptyRequest, emptyResponse, ...entities].map(it => '\t' + it.generateName() + ','),
|
||||
|
@ -188,9 +160,6 @@ async function generateApiClientJSDoc(
|
|||
|
||||
const endpointOutputLine: string[] = [];
|
||||
|
||||
endpointOutputLine.push(generateVersionHeaderComment(openApiDocs));
|
||||
endpointOutputLine.push('');
|
||||
|
||||
endpointOutputLine.push(`import type { SwitchCaseResponseType } from '${toImportPath(apiClientFileName)}';`);
|
||||
endpointOutputLine.push(`import type { Endpoints } from '${toImportPath(endpointsFileName)}';`);
|
||||
endpointOutputLine.push('');
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "cherrypick-js",
|
||||
"version": "4.7.0-beta.1",
|
||||
"basedMisskeyVersion": "2024.2.0-beta.8",
|
||||
"version": "4.7.0-beta.2",
|
||||
"basedMisskeyVersion": "2024.2.0-beta.10",
|
||||
"description": "CherryPick SDK for JavaScript",
|
||||
"types": "./built/dts/index.d.ts",
|
||||
"exports": {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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) => {
|
||||
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'];
|
||||
|
||||
export const commonHandlers = [
|
||||
rest.get('/fluent-emoji/:codepoints.png', async (req, res, ctx) => {
|
||||
const { codepoints } = req.params;
|
||||
http.get('/fluent-emoji/:codepoints.png', async ({ params }) => {
|
||||
const { codepoints } = params;
|
||||
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) => {
|
||||
const { codepoints } = req.params;
|
||||
http.get('/fluent-emojis/:codepoints.png', async ({ params }) => {
|
||||
const { codepoints } = params;
|
||||
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) => {
|
||||
const { codepoints } = req.params;
|
||||
http.get('/twemoji/:codepoints.svg', async ({ params }) => {
|
||||
const { codepoints } = params;
|
||||
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob());
|
||||
return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value));
|
||||
return new HttpResponse(value, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
},
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
|
|
@ -133,8 +133,8 @@
|
|||
"happy-dom": "10.0.3",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.5",
|
||||
"msw": "2.1.2",
|
||||
"msw-storybook-addon": "1.10.0",
|
||||
"msw": "2.1.7",
|
||||
"msw-storybook-addon": "2.0.0-beta.1",
|
||||
"nodemon": "3.0.3",
|
||||
"prettier": "3.2.4",
|
||||
"react": "18.2.0",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { abuseUserReport } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAbuseReport from './MkAbuseReport.vue';
|
||||
|
@ -44,9 +44,9 @@ export const Default = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/admin/resolve-abuse-user-report', async (req, res, ctx) => {
|
||||
action('POST /api/admin/resolve-abuse-user-report')(await req.json());
|
||||
return res(ctx.json({}));
|
||||
http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => {
|
||||
action('POST /api/admin/resolve-abuse-user-report')(await request.json());
|
||||
return HttpResponse.json({});
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAbuseReportWindow from './MkAbuseReportWindow.vue';
|
||||
|
@ -44,9 +44,9 @@ export const Default = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/report-abuse', async (req, res, ctx) => {
|
||||
action('POST /api/users/report-abuse')(await req.json());
|
||||
return res(ctx.json({}));
|
||||
http.post('/api/users/report-abuse', async ({ request }) => {
|
||||
action('POST /api/users/report-abuse')(await request.json());
|
||||
return HttpResponse.json({});
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAchievements from './MkAchievements.vue';
|
||||
|
@ -39,8 +39,8 @@ export const Empty = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/achievements', (req, res, ctx) => {
|
||||
return res(ctx.json([]));
|
||||
http.post('/api/users/achievements', () => {
|
||||
return HttpResponse.json([]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -52,8 +52,8 @@ export const All = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/achievements', (req, res, ctx) => {
|
||||
return res(ctx.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 }))));
|
||||
http.post('/api/users/achievements', () => {
|
||||
return HttpResponse.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 })));
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions';
|
|||
import { expect } from '@storybook/jest';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAutocomplete from './MkAutocomplete.vue';
|
||||
|
@ -99,11 +99,11 @@ export const User = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/search-by-username-and-host', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/users/search-by-username-and-host', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'),
|
||||
userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
@ -132,12 +132,12 @@ export const Hashtag = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/hashtags/search', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/hashtags/search', () => {
|
||||
return HttpResponse.json([
|
||||
'気象警報注意報',
|
||||
'気象警報',
|
||||
'気象情報',
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAvatars from './MkAvatars.vue';
|
||||
|
@ -38,12 +38,12 @@ export const Default = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/show', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/users/show', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('17'),
|
||||
userDetailed('20'),
|
||||
userDetailed('18'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { bundledLanguagesInfo } 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<{
|
||||
code: string;
|
||||
|
@ -21,11 +22,23 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const highlighter = await getHighlighter();
|
||||
|
||||
const darkMode = defaultStore.reactiveState.darkMode;
|
||||
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, {
|
||||
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> {
|
||||
|
@ -64,6 +77,16 @@ watch(() => props.lang, (to) => {
|
|||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
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,
|
||||
& 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 {
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
|
@ -79,6 +122,7 @@ watch(() => props.lang, (to) => {
|
|||
padding: 12px;
|
||||
margin: 0;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
min-height: 130px;
|
||||
pointer-events: none;
|
||||
min-width: calc(100% - 24px);
|
||||
|
@ -90,6 +134,11 @@ watch(() => props.lang, (to) => {
|
|||
text-rendering: inherit;
|
||||
text-transform: inherit;
|
||||
white-space: pre;
|
||||
|
||||
& span {
|
||||
display: inline-block;
|
||||
min-height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -53,7 +53,6 @@ function copy() {
|
|||
}
|
||||
|
||||
.codeBlockCopyButton {
|
||||
color: #D4D4D4;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
|
@ -67,8 +66,7 @@ function copy() {
|
|||
.codeBlockFallbackRoot {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
background: var(--bg);
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
|
@ -91,8 +89,8 @@ function copy() {
|
|||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-top: 4px;
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.codePlaceholderContainer {
|
||||
|
|
|
@ -196,10 +196,11 @@ watch(v, newValue => {
|
|||
resize: none;
|
||||
text-align: left;
|
||||
color: transparent;
|
||||
caret-color: rgb(225, 228, 232);
|
||||
caret-color: var(--fg);
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
min-width: calc(100% - 24px);
|
||||
height: 100%;
|
||||
|
@ -210,6 +211,6 @@ watch(v, newValue => {
|
|||
}
|
||||
|
||||
.textarea::selection {
|
||||
color: #fff;
|
||||
color: var(--bg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -18,8 +18,7 @@ const props = defineProps<{
|
|||
display: inline-block;
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
overflow-wrap: anywhere;
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
background: var(--bg);
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showPinned?: boolean;
|
||||
|
@ -127,6 +128,7 @@ const props = withDefaults(defineProps<{
|
|||
asDrawer?: boolean;
|
||||
asWindow?: boolean;
|
||||
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
||||
targetNote?: Misskey.entities.Note;
|
||||
}>(), {
|
||||
showPinned: true,
|
||||
});
|
||||
|
@ -341,7 +343,7 @@ watch(q, () => {
|
|||
});
|
||||
|
||||
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() {
|
||||
|
|
|
@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:showPinned="showPinned"
|
||||
:pinnedEmojis="pinnedEmojis"
|
||||
:asReactionPicker="asReactionPicker"
|
||||
:targetNote="targetNote"
|
||||
:asDrawer="type === 'drawer'"
|
||||
:max-height="maxHeight"
|
||||
@chosen="chosen"
|
||||
|
@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import { shallowRef } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||
|
@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{
|
|||
showPinned?: boolean;
|
||||
pinnedEmojis?: string[],
|
||||
asReactionPicker?: boolean;
|
||||
targetNote?: Misskey.entities.Note;
|
||||
choseAndClose?: boolean;
|
||||
}>(), {
|
||||
manualShowing: null,
|
||||
|
|
|
@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:front="true"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||
|
||||
|
@ -26,6 +27,7 @@ withDefaults(defineProps<{
|
|||
src?: HTMLElement;
|
||||
showPinned?: boolean;
|
||||
asReactionPicker?: boolean;
|
||||
targetNote?: Misskey.entities.Note
|
||||
}>(), {
|
||||
showPinned: true,
|
||||
});
|
||||
|
|
|
@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
|
||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js';
|
||||
|
||||
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方向の移動距離
|
||||
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 pullDistance = ref(0);
|
||||
const isSwiping = ref(false);
|
||||
const isSwipingForClass = ref(false);
|
||||
let swipeAborted = false;
|
||||
|
||||
|
@ -77,6 +76,8 @@ function touchStart(event: TouchEvent) {
|
|||
|
||||
if (event.touches.length !== 1) return;
|
||||
|
||||
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
|
||||
|
||||
startScreenX = event.touches[0].screenX;
|
||||
startScreenY = event.touches[0].screenY;
|
||||
}
|
||||
|
@ -90,6 +91,8 @@ function touchMove(event: TouchEvent) {
|
|||
|
||||
if (swipeAborted) return;
|
||||
|
||||
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
|
||||
|
||||
let distanceX = event.touches[0].screenX - startScreenX;
|
||||
let distanceY = event.touches[0].screenY - startScreenY;
|
||||
|
||||
|
@ -139,6 +142,8 @@ function touchEnd(event: TouchEvent) {
|
|||
|
||||
if (!isSwiping.value) return;
|
||||
|
||||
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
|
||||
|
||||
const distance = event.changedTouches[0].screenX - startScreenX;
|
||||
|
||||
if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
|
||||
|
@ -162,6 +167,24 @@ function touchEnd(event: TouchEvent) {
|
|||
}, 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);
|
||||
|
||||
watch(tabModel, (newTab, oldTab) => {
|
||||
|
@ -182,6 +205,7 @@ watch(tabModel, (newTab, oldTab) => {
|
|||
|
||||
<style lang="scss" module>
|
||||
.transitionRoot {
|
||||
touch-action: pan-y pinch-zoom;
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
overflow: clip;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed, inviteCode } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkInviteCode from './MkInviteCode.vue';
|
||||
|
@ -39,8 +39,8 @@ export const Default = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/show', (req, res, ctx) => {
|
||||
return res(ctx.json(userDetailed(req.params.userId as string)));
|
||||
http.post('/api/users/show', ({ params }) => {
|
||||
return HttpResponse.json(userDetailed(params.userId as string));
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -119,6 +119,7 @@ function close() {
|
|||
margin-top: 12px;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .indicatorWithValue {
|
||||
|
|
|
@ -314,7 +314,7 @@ const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entiti
|
|||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(false);
|
||||
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 isMFM = shouldMfmCollapsed(appearNote.value);
|
||||
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 {
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
||||
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||
if (props.mock) {
|
||||
emit('reaction', reaction);
|
||||
return;
|
||||
|
|
|
@ -374,7 +374,7 @@ const translating = ref(false);
|
|||
const viewTextSource = ref(false);
|
||||
const noNyaize = ref(false);
|
||||
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 conversation = ref<Misskey.entities.Note[]>([]);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
|
@ -507,7 +507,7 @@ function react(viaKeyboard = false): void {
|
|||
}
|
||||
} else {
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
||||
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||
toggleReaction(reaction);
|
||||
}, () => {
|
||||
focus();
|
||||
|
|
|
@ -920,7 +920,7 @@ function cancel() {
|
|||
}
|
||||
|
||||
function insertMention() {
|
||||
os.selectUser().then(user => {
|
||||
os.selectUser({ localOnly: localOnly.value, includeSelf: true }).then(user => {
|
||||
insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||
import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
|
||||
import { vibrate } from '@/scripts/vibrate.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
@ -133,7 +134,7 @@ function moveEnd() {
|
|||
function moving(event: TouchEvent | PointerEvent) {
|
||||
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;
|
||||
isPullEnd.value = false;
|
||||
moveEnd();
|
||||
|
@ -152,6 +153,10 @@ function moving(event: TouchEvent | PointerEvent) {
|
|||
if (event.cancelable) event.preventDefault();
|
||||
}
|
||||
|
||||
if (pullDistance.value > SCROLL_STOP) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
|
||||
|
||||
if (!isPullEnd.value) isVibrate = false;
|
||||
|
|
|
@ -32,8 +32,9 @@ import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
|||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { customEmojis } from '@/custom-emojis.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';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -51,6 +52,16 @@ const emit = defineEmits<{
|
|||
|
||||
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 r = props.reaction.replace(':', '');
|
||||
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 canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||
|
||||
async function toggleReaction(ev: MouseEvent) {
|
||||
if (!canToggle.value) {
|
||||
chooseAlternative(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: その絵文字を使う権限があるかどうか確認
|
||||
|
||||
const oldReaction = props.note.myReaction;
|
||||
if (oldReaction) {
|
||||
const confirm = await os.confirm({
|
||||
|
@ -146,8 +153,8 @@ function stealReaction(ev: MouseEvent) {
|
|||
}
|
||||
|
||||
async function menu(ev) {
|
||||
if (!canToggle.value) return;
|
||||
if (!props.reaction.includes(':')) return;
|
||||
if (!canGetInfo.value) return;
|
||||
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: `:${reactionName.value}:`,
|
||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</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>
|
||||
</template>
|
||||
|
||||
|
@ -141,6 +141,7 @@ function show() {
|
|||
active: computed(() => v.value === option.props?.value),
|
||||
action: () => {
|
||||
v.value = option.props?.value;
|
||||
changed.value = true;
|
||||
emit('changeByUser', v.value);
|
||||
},
|
||||
});
|
||||
|
@ -291,6 +292,10 @@ function show() {
|
|||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.save {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
|
|
@ -309,7 +309,7 @@ function react(viaKeyboard = false): void {
|
|||
}
|
||||
} else {
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
||||
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||
if (props.mock) {
|
||||
emit('reaction', reaction);
|
||||
return;
|
||||
|
|
|
@ -17,9 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onMounted, onUnmounted, provide, shallowRef } from 'vue';
|
||||
import Misskey from 'cherrypick-js';
|
||||
import { Connection } from 'cherrypick-js/built/streaming.js';
|
||||
import { computed, watch, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
|
@ -93,8 +92,8 @@ function prepend(note) {
|
|||
}
|
||||
}
|
||||
|
||||
let connection: Connection;
|
||||
let connection2: Connection;
|
||||
let connection: Misskey.ChannelConnection | null = null;
|
||||
let connection2: Misskey.ChannelConnection | null = null;
|
||||
let paginationQuery: Paging | null = null;
|
||||
|
||||
const stream = useStream();
|
||||
|
@ -162,7 +161,7 @@ function connectChannel() {
|
|||
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() {
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
|
||||
sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin"
|
||||
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"
|
||||
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
|
||||
:style="{ border: 0 }"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
|
||||
|
@ -38,17 +38,17 @@ export const Default = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
rest.post('/api/pinned-users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/pinned-users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Misskey from 'cherrypick-js';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import XUser from '@/components/MkUserSetupDialog.User.vue';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import MkUserSetupDialog from './MkUserSetupDialog.vue';
|
||||
|
@ -38,17 +38,17 @@ export const Default = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
rest.post('/api/pinned-users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
http.post('/api/pinned-users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { expect } from '@storybook/jest';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../../.storybook/mocks.js';
|
||||
import MkUrl from './MkUrl.vue';
|
||||
export const Default = {
|
||||
|
@ -59,8 +59,8 @@ export const Default = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.get('/url', (req, res, ctx) => {
|
||||
return res(ctx.json({
|
||||
http.get('/url', () => {
|
||||
return HttpResponse.json({
|
||||
title: 'Misskey Hub',
|
||||
icon: 'https://misskey-hub.net/favicon.ico',
|
||||
description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。',
|
||||
|
@ -74,7 +74,7 @@ export const Default = {
|
|||
sitename: 'misskey-hub.net',
|
||||
sensitive: false,
|
||||
url: 'https://misskey-hub.net/',
|
||||
}));
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<KeepAlive :max="defaultStore.state.numberOfPageCache">
|
||||
<KeepAlive
|
||||
:max="defaultStore.state.numberOfPageCache"
|
||||
:exclude="pageCacheController"
|
||||
>
|
||||
<Suspense :timeout="0">
|
||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
||||
|
||||
|
@ -16,9 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
|
||||
import { IRouter, Resolved } from '@/nirax.js';
|
||||
import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
|
||||
import { IRouter, Resolved, RouteDef } from '@/nirax.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import MkLoadingPage from '@/pages/_loading_.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
router?: IRouter;
|
||||
|
@ -46,20 +51,47 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
|
|||
}
|
||||
|
||||
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 key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
|
||||
|
||||
function onChange({ resolved, key: newKey }) {
|
||||
const current = resolveNested(resolved);
|
||||
if (current == null) return;
|
||||
if (current == null || 'redirect' in current.route) return;
|
||||
currentPageComponent.value = current.route.component;
|
||||
currentPageProps.value = current.props;
|
||||
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
|
||||
|
||||
nextTick(() => {
|
||||
// ページ遷移完了後に再びキャッシュを有効化
|
||||
if (clearCacheRequested.value) {
|
||||
clearCacheRequested.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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(() => {
|
||||
router.removeListener('change', onChange);
|
||||
});
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
|
||||
// TODO: 型付け
|
||||
export const globalEvents = new EventEmitter();
|
||||
export const globalEvents = new EventEmitter<{
|
||||
themeChanged: () => void;
|
||||
clientNotification: (notification: Misskey.entities.Notification) => void;
|
||||
requestClearPageCache: () => void;
|
||||
}>();
|
||||
|
|
|
@ -293,13 +293,13 @@ const patronsWithIconWithMisskey = [{
|
|||
icon: 'https://assets.misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg',
|
||||
}, {
|
||||
name: 'taichan',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.png',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.jpg',
|
||||
}, {
|
||||
name: '猫吉よりお',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/a11518b3b34b4536a4bdd7178ba76a7b.png',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/a11518b3b34b4536a4bdd7178ba76a7b.jpg',
|
||||
}, {
|
||||
name: '有栖かずみ',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.png',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.jpg',
|
||||
}];
|
||||
|
||||
const patronsWithCherryPick = [
|
||||
|
|
|
@ -148,9 +148,9 @@ function save() {
|
|||
themeColor: themeColor.value === '' ? null : themeColor.value,
|
||||
defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value,
|
||||
defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value,
|
||||
infoImageUrl: infoImageUrl.value,
|
||||
notFoundImageUrl: notFoundImageUrl.value,
|
||||
serverErrorImageUrl: serverErrorImageUrl.value,
|
||||
infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value,
|
||||
notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value,
|
||||
serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value,
|
||||
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
|
|
|
@ -893,7 +893,6 @@ function getGameImageDriveFile() {
|
|||
formData.append('file', blob);
|
||||
formData.append('name', `bubble-game-${Date.now()}.png`);
|
||||
formData.append('isSensitive', 'false');
|
||||
formData.append('comment', 'null');
|
||||
formData.append('i', $i.token);
|
||||
if (defaultStore.state.uploadFolder) {
|
||||
formData.append('folderId', defaultStore.state.uploadFolder);
|
||||
|
|
|
@ -57,6 +57,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
[$style.boardCell_prev]: engine.prevPos === i
|
||||
}]"
|
||||
@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
|
||||
:enterActiveClass="$style.transition_flip_enterActive"
|
||||
|
@ -66,8 +70,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
mode="default"
|
||||
>
|
||||
<template v-if="useAvatarAsStone">
|
||||
<img v-if="stone === true" :class="$style.boardCellStone" :src="blackUser.avatarUrl ?? undefined"/>
|
||||
<img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUser.avatarUrl ?? undefined"/>
|
||||
<img v-if="stone === true" :class="$style.boardCellStone" :src="blackUserUrl ?? undefined"/>
|
||||
<img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUserUrl ?? undefined"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<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 os from '@/os.js';
|
||||
import { confetti } from '@/scripts/confetti.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
|
||||
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) => {
|
||||
if (!game.value.isEnded) return;
|
||||
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(() => {
|
||||
if (props.connection != null) {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
}
|
||||
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.addEventListener('mousemove', resetTimer);
|
||||
window.addEventListener('touchstart', resetTimer);
|
||||
window.addEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
|
@ -473,6 +505,12 @@ onUnmounted(() => {
|
|||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
}
|
||||
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.removeEventListener('mousemove', resetTimer);
|
||||
window.removeEventListener('touchstart', resetTimer);
|
||||
window.removeEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -157,7 +157,7 @@ const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
|
|||
const setDefaultEmoji = () => setDefault(pinnedEmojis);
|
||||
|
||||
function previewReaction(ev: MouseEvent) {
|
||||
reactionPicker.show(getHTMLElement(ev));
|
||||
reactionPicker.show(getHTMLElement(ev), null);
|
||||
}
|
||||
|
||||
function previewEmoji(ev: MouseEvent) {
|
||||
|
|
|
@ -125,6 +125,7 @@ import { langmap } from '@/scripts/langmap.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
@ -174,6 +175,7 @@ function saveFields() {
|
|||
os.apiWithDialog('i/update', {
|
||||
fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })),
|
||||
});
|
||||
globalEvents.emit('requestClearPageCache');
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
@ -192,6 +194,7 @@ function save() {
|
|||
isBot: !!profile.isBot,
|
||||
isCat: !!profile.isCat,
|
||||
});
|
||||
globalEvents.emit('requestClearPageCache');
|
||||
claimAchievement('profileFilled');
|
||||
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
|
||||
claimAchievement('setNameToSyuilo');
|
||||
|
@ -234,6 +237,7 @@ function changeAvatar(ev) {
|
|||
});
|
||||
$i.avatarId = i.avatarId;
|
||||
$i.avatarUrl = i.avatarUrl;
|
||||
globalEvents.emit('requestClearPageCache');
|
||||
claimAchievement('profileFilled');
|
||||
});
|
||||
}
|
||||
|
@ -260,6 +264,7 @@ function changeBanner(ev) {
|
|||
});
|
||||
$i.bannerId = i.bannerId;
|
||||
$i.bannerUrl = i.bannerUrl;
|
||||
globalEvents.emit('requestClearPageCache');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js';
|
|||
import { fetchThemes, getThemes } from '@/theme-store.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.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 builtinThemes = getBuiltinThemesRef();
|
||||
|
@ -124,6 +136,7 @@ const lightThemeId = computed({
|
|||
}
|
||||
},
|
||||
});
|
||||
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
|
||||
|
@ -141,7 +154,7 @@ watch(wallpaper, () => {
|
|||
} else {
|
||||
miLocalStorage.setItem('wallpaper', wallpaper.value);
|
||||
}
|
||||
location.reload();
|
||||
reloadAsk();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../../.storybook/mocks.js';
|
||||
import home_ from './home.vue';
|
||||
|
@ -39,12 +39,13 @@ export const Default = {
|
|||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/notes', (req, res, ctx) => {
|
||||
return res(ctx.json([]));
|
||||
http.post('/api/users/notes', () => {
|
||||
return HttpResponse.json([]);
|
||||
}),
|
||||
rest.get('/api/charts/user/notes', (req, res, ctx) => {
|
||||
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return res(ctx.json({
|
||||
http.get('/api/charts/user/notes', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return HttpResponse.json({
|
||||
total: Array.from({ length }, () => 0),
|
||||
inc: Array.from({ length }, () => 0),
|
||||
dec: Array.from({ length }, () => 0),
|
||||
|
@ -54,11 +55,12 @@ export const Default = {
|
|||
renote: Array.from({ length }, () => 0),
|
||||
withFile: Array.from({ length }, () => 0),
|
||||
},
|
||||
}));
|
||||
});
|
||||
}),
|
||||
rest.get('/api/charts/user/pv', (req, res, ctx) => {
|
||||
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return res(ctx.json({
|
||||
http.get('/api/charts/user/pv', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
|
||||
return HttpResponse.json({
|
||||
upv: {
|
||||
user: Array.from({ length }, () => 0),
|
||||
visitor: Array.from({ length }, () => 0),
|
||||
|
@ -67,7 +69,7 @@ export const Default = {
|
|||
user: Array.from({ length }, () => 0),
|
||||
visitor: Array.from({ length }, () => 0),
|
||||
},
|
||||
}));
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
|
||||
type StateDef = Record<string, {
|
||||
where: 'account' | 'device' | 'deviceAccount';
|
||||
|
@ -84,29 +85,9 @@ export class Storage<T extends StateDef> {
|
|||
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 {
|
||||
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);
|
||||
|
||||
|
@ -258,7 +239,7 @@ export class Storage<T extends StateDef> {
|
|||
|
||||
/**
|
||||
* 特定のキーの、簡易的なgetter/setterを作ります
|
||||
* 主にvue場で設定コントロールのmodelとして使う用
|
||||
* 主にvue上で設定コントロールのmodelとして使う用
|
||||
*/
|
||||
public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): {
|
||||
get: () => T[K]['default'];
|
||||
|
|
|
@ -80,6 +80,10 @@ class MainRouterProxy implements IRouter {
|
|||
return this.supplier().resolve(path);
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.supplier().init();
|
||||
}
|
||||
|
||||
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
|
||||
return this.supplier().eventNames();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import * as Misskey from 'cherrypick-js';
|
||||
|
||||
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean {
|
||||
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
|
||||
return !(emoji.localOnly && note.user.host !== me.host)
|
||||
&& !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
|
||||
&& (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
|
||||
}
|
|
@ -8,13 +8,13 @@
|
|||
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
||||
// 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 {
|
||||
if (typeof x === 'object') {
|
||||
if (x === null) return x;
|
||||
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)) {
|
||||
obj[k] = v === undefined ? undefined : deepClone(v);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue