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

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

View file

@ -49,6 +49,12 @@
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
- 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: オフライン表示のデザインを改善・多言語対応

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,44 @@
import * as assert from 'assert';
import { Test } from '@nestjs/testing';
import { CoreModule } from '@/core/CoreModule.js';
import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { MiNote } from '@/models/Note.js';
describe('ApMfmService', () => {
let apMfmService: ApMfmService;
beforeAll(async () => {
const app = await Test.createTestingModule({
imports: [GlobalModule, CoreModule],
}).compile();
apMfmService = app.get<ApMfmService>(ApMfmService);
});
describe('getNoteHtml', () => {
test('Do not provide _misskey_content for simple text', () => {
const note: MiNote = {
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
mentionedRemoteUsers: '[]',
} as any;
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, true, 'noMisskeyContent');
assert.equal(content, '<p>テキスト <a href="http://cherrypick.local/tags/タグ" rel="tag">#タグ</a> <a href="http://cherrypick.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com">https://example.com</a></p>', 'content');
});
test('Provide _misskey_content for MFM', () => {
const note: MiNote = {
text: '$[tada foo]',
mentionedRemoteUsers: '[]',
} as any;
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, false, 'noMisskeyContent');
assert.equal(content, '<p><i>foo</i></p>', 'content');
});
});
});

View file

@ -33,6 +33,12 @@ describe('MfmService', () => {
const output = '<p><span>foo<br>bar<br>baz</span></p>';
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', () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -119,6 +119,7 @@ function close() {
margin-top: 12px;
font-size: 0.8em;
line-height: 1.5em;
text-align: center;
}
> .indicatorWithValue {

View file

@ -314,7 +314,7 @@ const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entiti
const isMyRenote = $i && ($i.id === note.value.userId);
const 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
import * as Misskey from 'cherrypick-js';
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean {
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
return !(emoji.localOnly && note.user.host !== me.host)
&& !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
&& (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
}

View file

@ -8,13 +8,13 @@
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
// 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