From 316ffcea54eb7f1f4b04c8b9937b390c629d088c Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 24 Dec 2023 14:20:43 +0900 Subject: [PATCH 01/99] =?UTF-8?q?ci:=20Get=20api.json=20from=20Misskey?= =?UTF-8?q?=E3=81=A7upload-artifact@v4=E3=81=A7=E5=90=8C=E5=90=8Dartifact?= =?UTF-8?q?=E3=81=A7=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AB=E3=81=AA=E3=82=8B?= =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#12770)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: upload-artifact@v4で同名artifactでエラーになるのを修正 Co-authored-by: おさむのひと <46447427+samunohito@users.noreply.github.com> * report-api-diff.ymlの最中にエラーが発生したときに分かりづらいので、PRにコメントを残すようにする * 古いget-api-diffを使ってるactionとの互換性をもたせる --------- Co-authored-by: おさむのひと <46447427+samunohito@users.noreply.github.com> --- .github/workflows/get-api-diff.yml | 4 +-- .github/workflows/report-api-diff.yml | 36 ++++++++++++++++++--------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index d604f9b16d..bf92e701b2 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -56,7 +56,7 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: api-artifact + name: api-artifact-${{ matrix.api-json-name }} path: ${{ matrix.api-json-name }} save-pr-number: @@ -69,5 +69,5 @@ jobs: echo "$PR_NUMBER" > ./pr_number - uses: actions/upload-artifact@v4 with: - name: api-artifact + name: api-artifact-pr-number path: pr_number diff --git a/.github/workflows/report-api-diff.yml b/.github/workflows/report-api-diff.yml index 309516772f..54da8b4a83 100644 --- a/.github/workflows/report-api-diff.yml +++ b/.github/workflows/report-api-diff.yml @@ -19,24 +19,28 @@ jobs: uses: actions/github-script@v7 with: script: | + const fs = require('fs'); let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: context.payload.workflow_run.id, }); - let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { - return artifact.name == "api-artifact" - })[0]; - let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', + let matchArtifacts = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name.startsWith("api-artifact-") || artifact.name == "api-artifact" }); - let fs = require('fs'); - fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/api-artifact.zip`, Buffer.from(download.data)); - - name: Extract artifact - run: unzip api-artifact.zip -d artifacts + await Promise.all(matchArtifacts.map(async (artifact) => { + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: artifact.id, + archive_format: 'zip', + }); + await fs.promises.writeFile(`${process.env.GITHUB_WORKSPACE}/${artifact.name}.zip`, Buffer.from(download.data)); + })); + - name: Extract all artifacts + run: | + find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec unzip {} -d artifacts ';' + ls -la - name: Load PR Number id: load-pr-num run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT" @@ -83,3 +87,11 @@ jobs: pr_number: ${{ steps.load-pr-num.outputs.pr-number }} comment_tag: show_diff filePath: ./output.md + - name: Tell error to PR + uses: thollander/actions-comment-pull-request@v2 + if: failure() && steps.load-pr-num.outputs.pr-number + with: + pr_number: ${{ steps.load-pr-num.outputs.pr-number }} + comment_tag: show_diff_error + message: | + api.jsonの差分作成中にエラーが発生しました。詳細は[Workflowのログ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})を確認してください。 From 6fce36374d8756f47f96c7a04cd388c994bd047f Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 24 Dec 2023 15:23:56 +0900 Subject: [PATCH 02/99] =?UTF-8?q?enhance(backend):=20=E3=82=BB=E3=83=B3?= =?UTF-8?q?=E3=82=B7=E3=83=86=E3=82=A3=E3=83=96=E3=83=AF=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=81=8C=E3=83=8F=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=82=BF=E3=82=B0=E3=83=88=E3=83=AC=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=81=AB=E3=82=82=E9=81=A9=E7=94=A8=E3=81=95=E3=82=8C=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 ++++++++ packages/backend/src/core/HashtagService.ts | 3 ++ .../backend/src/core/NoteCreateService.ts | 27 +----------------- packages/backend/src/core/UtilityService.ts | 28 +++++++++++++++++++ 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac31bc0d28..af2aea7996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,17 @@ --> +## 2023.12.1 + +### General +- + +### Client +- + +### Server +- Enhance: センシティブワードの設定がハッシュタグトレンドにも適用されるようになりました + ## 2023.12.0 ### Note diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index d378999907..5a2417c9cd 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -15,6 +15,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { MetaService } from '@/core/MetaService.js'; +import { UtilityService } from '@/core/UtilityService.js'; @Injectable() export class HashtagService { @@ -29,6 +30,7 @@ export class HashtagService { private featuredService: FeaturedService, private idService: IdService, private metaService: MetaService, + private utilityService: UtilityService, ) { } @@ -161,6 +163,7 @@ export class HashtagService { const instance = await this.metaService.fetch(); const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); if (hiddenTags.includes(hashtag)) return; + if (this.utilityService.isSensitiveWordIncluded(hashtag, instance.sensitiveWords)) return; // YYYYMMDDHHmm (10分間隔) const now = new Date(); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 2bdff872ad..35baa1447d 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -253,7 +253,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.visibility === 'public' && data.channel == null) { const sensitiveWords = meta.sensitiveWords; - if (this.isSensitive(data, sensitiveWords)) { + if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; @@ -704,31 +704,6 @@ export class NoteCreateService implements OnApplicationShutdown { this.index(note); } - @bindThis - private isSensitive(note: Option, sensitiveWord: string[]): boolean { - if (sensitiveWord.length > 0) { - const text = note.cw ?? note.text ?? ''; - if (text === '') return false; - const matched = sensitiveWord.some(filter => { - // represents RegExp - const regexp = filter.match(/^\/(.+)\/(.*)$/); - // This should never happen due to input sanitisation. - if (!regexp) { - const words = filter.split(' '); - return words.every(keyword => text.includes(keyword)); - } - try { - return new RE2(regexp[1], regexp[2]).test(text); - } catch (err) { - // This should never happen due to input sanitisation. - return false; - } - }); - if (matched) return true; - } - return false; - } - @bindThis private isQuote(note: Option): note is Option & { renote: MiNote } { // sync with misc/is-quote.ts diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index b95e41167b..5dec36c89e 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -6,6 +6,7 @@ import { URL } from 'node:url'; import { toASCII } from 'punycode'; import { Inject, Injectable } from '@nestjs/common'; +import RE2 from 're2'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; @@ -41,6 +42,33 @@ export class UtilityService { return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); } + @bindThis + public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean { + if (sensitiveWords.length === 0) return false; + if (text === '') return false; + + const regexpregexp = /^\/(.+)\/(.*)$/; + + const matched = sensitiveWords.some(filter => { + // represents RegExp + const regexp = filter.match(regexpregexp); + // This should never happen due to input sanitisation. + if (!regexp) { + const words = filter.split(' '); + return words.every(keyword => text.includes(keyword)); + } + try { + // TODO: RE2インスタンスをキャッシュ + return new RE2(regexp[1], regexp[2]).test(text); + } catch (err) { + // This should never happen due to input sanitisation. + return false; + } + }); + + return matched; + } + @bindThis public extractDbHost(uri: string): string { const url = new URL(uri); From 36701f8a7c867a68bcce814bfc4548624f43916b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Sun, 24 Dec 2023 15:24:26 +0900 Subject: [PATCH 03/99] =?UTF-8?q?fix(backend):=201702718871541-ffVisibilit?= =?UTF-8?q?y.js=E3=81=AEdown=E3=81=8C=E5=A3=8A=E3=82=8C=E3=81=A6=E3=81=84?= =?UTF-8?q?=E3=82=8B=20(#12767)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/migration/1702718871541-ffVisibility.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/migration/1702718871541-ffVisibility.js b/packages/backend/migration/1702718871541-ffVisibility.js index 24b1873134..e9e820c897 100644 --- a/packages/backend/migration/1702718871541-ffVisibility.js +++ b/packages/backend/migration/1702718871541-ffVisibility.js @@ -24,9 +24,11 @@ export class ffVisibility1702718871541 { async down(queryRunner) { await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`); await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`); + await queryRunner.query(`CREATE CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum") WITH INOUT AS ASSIGNMENT`); - await queryRunner.query(`UPDATE "user_profile" SET ffVisibility = "user_profile"."followingVisibility"`); + await queryRunner.query(`UPDATE "user_profile" SET "ffVisibility" = "followingVisibility"`); await queryRunner.query(`DROP CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum")`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followersVisibility"`); await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followingVisibility"`); await queryRunner.query(`DROP TYPE "public"."user_profile_followersVisibility_enum"`); From cae40e68e4c84693fbeaf0e801e705ccb876e2c0 Mon Sep 17 00:00:00 2001 From: Nya Candy Date: Sun, 24 Dec 2023 14:24:51 +0800 Subject: [PATCH 04/99] fix: lint (#12761) --- packages/backend/src/core/EmailService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 3a61e353f1..7fc7800783 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -7,7 +7,6 @@ import { URLSearchParams } from 'node:url'; import * as nodemailer from 'nodemailer'; import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; -import { SubOutputFormat } from 'deep-email-validator/dist/output/output.js'; import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; @@ -166,7 +165,10 @@ export class EmailService { email: emailAddress, }); - let validated; + let validated: { + valid: boolean, + reason?: string | null, + }; if (meta.enableActiveEmailValidation) { if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) { From 0393d8f53cb8607ff9448208e125c7b9900ab422 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 24 Dec 2023 15:25:13 +0900 Subject: [PATCH 05/99] New Crowdin updates (#12759) * New translations ja-jp.yml (Spanish) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (Korean) * New translations ja-jp.yml (Korean) --- locales/es-ES.yml | 78 +++++++++++++++++++++++++++++++++++++++++++++++ locales/ko-KR.yml | 5 +-- locales/zh-TW.yml | 15 ++++----- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/locales/es-ES.yml b/locales/es-ES.yml index a079cf01f9..80cf905f75 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -121,6 +121,12 @@ sensitive: "Marcado como sensible" add: "Agregar" reaction: "Reacción" reactions: "Reacción" +emojiPicker: "Selector de emojis" +pinnedEmojisForReactionSettingDescription: "Puedes seleccionar reacciones para fijarlos en el selector" +pinnedEmojisSettingDescription: "Puedes seleccionar emojis para fijarlos en el selector" +emojiPickerDisplay: "Mostrar el selector de emojis" +overwriteFromPinnedEmojisForReaction: "Sobreescribir las reacciones fijadas" +overwriteFromPinnedEmojis: "Sobreescribir los emojis fijados" reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir." rememberNoteVisibility: "Recordar visibilidad" attachCancel: "Quitar adjunto" @@ -260,6 +266,7 @@ removed: "Borrado" removeAreYouSure: "¿Desea borrar \"{x}\"?" deleteAreYouSure: "¿Desea borrar \"{x}\"?" resetAreYouSure: "¿Desea reestablecer?" +areYouSure: "¿Estás conforme?" saved: "Guardado" messaging: "Chat" upload: "Subir" @@ -640,6 +647,7 @@ smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP" smtpSecureInfo: "Apagar cuando se use STARTTLS" testEmail: "Prueba de envío" wordMute: "Silenciar palabras" +hardWordMute: "Filtro de palabra fuerte" regexpError: "Error de la expresión regular" regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line} de las palabras muteadas {tab}" instanceMute: "Instancias silenciadas" @@ -873,6 +881,8 @@ makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán pú classic: "Clásico" muteThread: "Silenciar hilo" unmuteThread: "Mostrar hilo" +followingVisibility: "Visibilidad de seguidos" +followersVisibility: "Visibilidad de seguidores" continueThread: "Ver la continuación del hilo" deleteAccountConfirm: "La cuenta será borrada. ¿Está seguro?" incorrectPassword: "La contraseña es incorrecta" @@ -1024,6 +1034,7 @@ sensitiveWords: "Palabras sensibles" sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea" sensitiveWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares." hiddenTags: "Hashtags ocultos" +hiddenTagsDescription: "Selecciona las etiquetas que no se mostrarán en tendencias. Una etiqueta por línea." notesSearchNotAvailable: "No se puede buscar una nota" license: "Licencia" unfavoriteConfirm: "¿Desea quitar de favoritos?" @@ -1152,6 +1163,7 @@ tosAndPrivacyPolicy: "Condiciones de Uso y Política de Privacidad" avatarDecorations: "Decoraciones de avatar" attach: "Acoplar" detach: "Quitar" +detachAll: "Quitar todo" angle: "Ángulo" flip: "Echar de un capirotazo" showAvatarDecorations: "Mostrar decoraciones de avatar" @@ -1165,6 +1177,10 @@ cwNotationRequired: "Si se ha activado \"ocultar contenido\", es necesario propo doReaction: "Añadir reacción" code: "Código" reloadRequiredToApplySettings: "Es necesario recargar para que se aplique la configuración." +remainingN: "Faltan: {n}" +overwriteContentConfirm: "¿Quieres sustituir todo el contenido actual?" +seasonalScreenEffect: "Efectos de pantalla asociados a estaciones" +decorate: "Decorar" _announcement: forExistingUsers: "Solo para usuarios registrados" forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán." @@ -1222,6 +1238,45 @@ _initialTutorial: home: "Puedes ver los posts de las cuentas que sigues." local: "Puedes ver los posts de todos los usuarios de este servidor." social: "Se ven los posts de la línea de tiempo de inicio junto con los de la línea de tiempo local." + global: "Puedes ver notas de todos los servidores conectados." + description2: "Puedes cambiar la línea de tiempo en la parte superior de la pantalla cuando quieras." + description3: "Además, hay listas de líneas de tiempo y listas de canales. Para más detalle, por favor visita este enlace: {link}" + _postNote: + title: "Ajustes de publicación de nota" + description1: "Cuando publicas una nota en Misskey, hay varias opciones disponibles. El formulario tiene este aspecto." + _visibility: + description: "Puedes limitar quién puede ver tu nota." + public: "Tu nota será visible para todos los usuarios." + home: "Publicar solo en la línea de tiempo de Inicio. La nota se verá en tu perfil, la verán tus seguidores y también cuando sea renotada." + followers: "Visible solo para seguidores. Sólo tus seguidores podrán ver la nota, y no podrá ser renotada por otras personas." + direct: "Visible sólo para usuarios específicos, y el destinatario será notificado. Puede usarse como alternativa a la mensajería directa." + doNotSendConfidencialOnDirect1: "¡Ten cuidado cuando vayas a enviar información sensible!" + doNotSendConfidencialOnDirect2: "Los administradores del servidor pueden leer lo que escribes. Ten cuidado cuando envíes información sensible en notas directas en servidores no confiables." + localOnly: "Publicando con esta opción seleccionada, la nota no se federará hacia otros servidores. Los usuarios de otros servidores no podrán ver estas notas directamente, sin importar los ajustes seleccionados más arriba." + _cw: + title: "Alerta de contenido (CW)" + description: "En lugar de mostrarse el contenido de la nota, se mostrará lo que escribas en el campo \"comentarios\". Pulsando en \"leer más\" desplegará el contenido de la nota." + _exampleNote: + cw: "¡Esto te hará tener hambre!" + note: "Acabo de comerme un donut de chocolate glaseado 🍩😋" + useCases: "Esto se usa cuando las normas del servidor lo requieren, o para ocultar spoilers o contenido sensible." + _howToMakeAttachmentsSensitive: + title: "¿Cómo puedo marcar adjuntos como contenido sensible?" + description: "Cuando las normas del servidor lo requieran, o el contenido lo requiera, marca la opción de \"contenido sensible\" para el adjunto." + tryThisFile: "¡Prueba a marcar la imagen adjunta como contenido sensible!" + _exampleNote: + note: "Ups, la he liado al abrir la tapa del natto..." + method: "Para marcar un adjunto como sensible, haz clic en la miniatura, abre el menú, y haz clic en \"Marcar como sensible\"." + sensitiveSucceeded: "Cuando adjuntes archivos, por favor, ten en cuenta las normas del servidor para marcarlos como contenido sensible." + doItToContinue: "Marca el archivo adjunto como sensible para continuar." + _done: + title: "¡Has completado el tutorial! 🎉" + description: "Las funciones que mostramos aquí son sólo una pequeña parte. Para más detalles sobre el funcionamiento de Misskey, pulsa en este enlace: {link}" +_timelineDescription: + home: "En la línea de tiempo de Inicio puedes ver las notas de las cuentas a las que sigues." + local: "En la línea de tiempo Local puedes ver las notas de todos los usuarios del servidor." + social: "En la línea de tiempo Social verás las notas de Inicio y Local a la vez." + global: "En la línea de tiempo Global verás las notas de todos los servidores conectados." _serverRules: description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado." _serverSettings: @@ -1233,6 +1288,7 @@ _serverSettings: manifestJsonOverride: "Sobreescribir manifest.json" shortName: "Nombre corto" shortNameDescription: "Forma corta del nombre de la instancia que puede mostrarse si el nombre completo es demasiado largo." + fanoutTimelineDescription: "Incrementa el rendimiento de forma significativa cuando se obtienen las líneas de tiempo y reduce la carga en la base de datos. A cambio, el uso de la memoria en Redis incrementará. Considera desactivar esta opción en caso de que tu servidor tenga poca memoria o detectes inestabilidad." _accountMigration: moveFrom: "Trasladar de otra cuenta a ésta" moveFromSub: "Crear un alias para otra cuenta." @@ -1490,6 +1546,9 @@ _achievements: _smashTestNotificationButton: title: "Sobrecarga de pruebas" description: "Envía muchas notificaciones de prueba en un corto espacio de tiempo" + _tutorialCompleted: + title: "Diploma del Curso Básico de Misskey" + description: "Tutorial completado" _role: new: "Crear rol" edit: "Editar rol" @@ -1500,7 +1559,9 @@ _role: assignTarget: "Asignar objetivo" descriptionOfAssignTarget: "Manual Para cambiar manualmente lo que se incluye en este rol.\nCondicional configura una condición, y los usuarios que cumplan la condición serán incluídos automáticamente." manual: "manual" + manualRoles: "Roles manuales" conditional: "condicional" + conditionalRoles: "Roles condicionales" condition: "condición" isConditionalRole: "Esto es un rol condicional" isPublic: "Publicar rol" @@ -1549,6 +1610,7 @@ _role: canHideAds: "Puede ocultar anuncios" canSearchNotes: "Uso de la búsqueda de notas" canUseTranslator: "Uso de traductor" + avatarDecorationLimit: "Número máximo de decoraciones de avatar" _condition: isLocal: "Usuario local" isRemote: "Usuario remoto" @@ -1577,6 +1639,7 @@ _emailUnavailable: disposable: "No es un correo reutilizable" mx: "Servidor de correo inválido" smtp: "Servidor de correo no disponible" + banned: "Email no disponible" _ffVisibility: public: "Publicar" followers: "Visible solo para seguidores" @@ -1653,6 +1716,7 @@ _aboutMisskey: donate: "Donar a Misskey" morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰" patrons: "Patrocinadores" + projectMembers: "Miembros del proyecto" _displayOfSensitiveMedia: respect: "Esconder medios marcados como sensibles" ignore: "Mostrar medios marcados como sensibles" @@ -1677,6 +1741,7 @@ _channel: notesCount: "{n} notas" nameAndDescription: "Nombre y descripción" nameOnly: "Sólo nombre" + allowRenoteToExternal: "Permitir renotas y menciones fuera del canal" _menuDisplay: sideFull: "Horizontal" sideIcon: "Horizontal (ícono)" @@ -1780,6 +1845,12 @@ _ago: yearsAgo: "Hace {n} años" invalid: "No hay nada que ver aqui" _timeIn: + seconds: "En {n} segundos" + minutes: "En {n}m" + hours: "En {n}h" + days: "En {n}d" + weeks: "En {n}sem." + months: "En {n}M" years: "En {n} años" _time: second: "Segundos" @@ -1906,6 +1977,7 @@ _widgets: _userList: chooseList: "Seleccione una lista" clicker: "Cliqueador" + birthdayFollowings: "Hoy cumplen años" _cw: hide: "Ocultar" show: "Ver más" @@ -1968,6 +2040,7 @@ _profile: changeAvatar: "Cambiar avatar" changeBanner: "Cambiar banner" verifiedLinkDescription: "Introduciendo una URL que contiene un enlace a tu perfil, se puede mostrar un icono de verificación de propiedad al lado del campo." + avatarDecorationMax: "Puedes añadir un máximo de {max} decoraciones de avatar." _exportOrImport: allNotes: "Todas las notas" favoritedNotes: "Notas favoritas" @@ -2089,6 +2162,7 @@ _notification: pollEnded: "Estan disponibles los resultados de la encuesta" newNote: "Nueva nota" unreadAntennaNote: "Antena {name}" + roleAssigned: "Rol asignado" emptyPushNotificationMessage: "Se han actualizado las notificaciones push" achievementEarned: "Logro desbloqueado" testNotification: "Notificación de prueba" @@ -2110,6 +2184,7 @@ _notification: pollEnded: "La encuesta terminó" receiveFollowRequest: "Recibió una solicitud de seguimiento" followRequestAccepted: "El seguimiento fue aceptado" + roleAssigned: "Rol asignado" achievementEarned: "Logro desbloqueado" app: "Notificaciones desde aplicaciones" _actions: @@ -2255,3 +2330,6 @@ _externalResourceInstaller: _themeInstallFailed: title: "Instalación de tema fallida" description: "Ha ocurrido un problema al instalar el tema. Por favor, inténtalo de nuevo. Se pueden ver más detalles del error en la consola de Javascript." +_dataSaver: + _media: + title: "Cargando Multimedia" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 6cdcc2c246..63d0812e93 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -114,7 +114,7 @@ quote: "인용" inChannelRenote: "채널 내 리노트" inChannelQuote: "채널 내 인용" pinnedNote: "고정된 노트" -pinned: "프로필에 고정" +pinned: "고정하기" you: "나" clickToShow: "클릭하여 보기" sensitive: "열람 주의" @@ -1179,7 +1179,7 @@ code: "문자열" reloadRequiredToApplySettings: "설정을 적용하려면 새로고침을 해야 합니다." remainingN: "나머지: {n}" overwriteContentConfirm: "현재 내용을 덮어쓰기 합니다. 계속 진행하시겠습니까?" -seasonalScreenEffect: "철에 맞는 화면으로 꾸미기" +seasonalScreenEffect: "계절에 따른 효과 보이기" decorate: "장식하기" _announcement: forExistingUsers: "기존 유저에게만 알림" @@ -1641,6 +1641,7 @@ _emailUnavailable: disposable: "임시 이메일 주소는 사용할 수 없습니다" mx: "메일 서버가 올바르지 않습니다" smtp: "메일 서버가 응답하지 않습니다" + banned: "이 메일 주소는 사용할 수 없습니다" _ffVisibility: public: "공개" followers: "팔로워에게만 공개" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 51ba42e66c..782f871b1e 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -632,11 +632,11 @@ tokenRequested: "允許存取帳戶" pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。" notificationType: "通知形式" edit: "編輯" -emailServer: "電郵伺服器" -enableEmail: "啟用發送電郵功能" -emailConfigInfo: "用於確認電郵地址及密碼重置" +emailServer: "電子郵件伺服器" +enableEmail: "啟用發送電子郵件功能" +emailConfigInfo: "用於確認電子郵件地址及密碼重置" email: "電子郵件" -emailAddress: "電郵地址" +emailAddress: "電子郵件位址" smtpConfig: "SMTP 伺服器設定" smtpHost: "主機" smtpPort: "埠" @@ -731,7 +731,7 @@ disableShowingAnimatedImages: "不播放動態圖檔" highlightSensitiveMedia: "強調敏感標記" verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。" notSet: "未設定" -emailVerified: "已成功驗證您的電郵" +emailVerified: "已成功驗證您的電子郵件地址" noteFavoritesCount: "我的最愛貼文的數目" pageLikesCount: "頁面被按讚次數" pageLikedCount: "頁面被按讚次數" @@ -783,7 +783,7 @@ capacity: "容量" inUse: "已使用" editCode: "編輯代碼" apply: "套用" -receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知" +receiveAnnouncementFromInstance: "接收來自伺服器的通知" emailNotification: "郵件通知" publish: "發布" inChannelSearch: "頻道内搜尋" @@ -955,7 +955,7 @@ cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制 beta: "測試版" enableAutoSensitive: "自動 NSFW 判定" enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依實例規則而自動啟用。" -activeEmailValidationDescription: "積極驗證使用者的電郵地址,以判斷它是否可以通訊。關閉此選項代表只會檢查地址是否符合格式。" +activeEmailValidationDescription: "主動地驗證使用者的電子郵件地址,以確定是否是一次性地址以及是否可以真正與其進行通訊。關閉時,僅檢查格式是否正確。" navbar: "導覽列" shuffle: "隨機" account: "帳戶" @@ -1641,6 +1641,7 @@ _emailUnavailable: disposable: "不是永久可用的地址" mx: "郵件伺服器不正確" smtp: "郵件伺服器沒有應答" + banned: "無法使用此電子郵件地址註冊" _ffVisibility: public: "公開" followers: "只有關注您的使用者能看到" From 7167bb397e6a40fa715254e2aa7f901956275975 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 24 Dec 2023 15:31:48 +0900 Subject: [PATCH 06/99] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af2aea7996..4751fff654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,6 +110,7 @@ - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように - Enhance: Meilisearchを有効にした検索で、ユーザーのミュートやブロックを考慮するように - Enhance: カスタム絵文字のインポート時の動作を改善 +- Enhance: json-schema(OpenAPIの戻り値として使用されるスキーマ定義)を出来る限り最新化 #12311 - Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303 - Fix: ロールタイムラインが保存されない問題を修正 - Fix: api.jsonの生成ロジックを改善 #12402 @@ -137,7 +138,6 @@ - Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました - Enhance: ローカリゼーションの更新 - Enhance: 依存関係の更新 -- Enhance: json-schema(OpenAPIの戻り値として使用されるスキーマ定義)を出来る限り最新化 #12311 ### Client - Enhance: MFMでルビを振れるように From bf45c2309845640de9a0d0472bb4480b2a9dc8af Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 24 Dec 2023 15:38:03 +0900 Subject: [PATCH 07/99] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4751fff654..432b39afb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,13 +15,14 @@ ## 2023.12.1 ### General -- +- Enhance: ローカリゼーションの更新 ### Client - ### Server - Enhance: センシティブワードの設定がハッシュタグトレンドにも適用されるようになりました +- Fix: 1702718871541-ffVisibility.jsのdownが壊れている ## 2023.12.0 From 0009aa332bec1bb52ee5600d528419c0455576d2 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Sun, 24 Dec 2023 16:16:58 +0900 Subject: [PATCH 08/99] =?UTF-8?q?refactor(frontend):=20import=E5=AE=A3?= =?UTF-8?q?=E8=A8=80=E5=91=A8=E3=82=8A=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=20(#12773)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...lugin-unwind-css-module-class-name.test.ts | 2 +- .../components/MkAbuseReport.stories.impl.ts | 4 ++-- .../MkAbuseReportWindow.stories.impl.ts | 4 ++-- .../components/MkAccountMoved.stories.impl.ts | 2 +- .../components/MkAchievements.stories.impl.ts | 4 ++-- .../components/MkAutocomplete.stories.impl.ts | 4 ++-- .../src/components/MkAvatars.stories.impl.ts | 4 ++-- .../frontend/src/components/MkContextMenu.vue | 2 +- .../src/components/MkDateSeparatedList.vue | 4 ++-- .../MkGalleryPostPreview.stories.impl.ts | 2 +- .../components/MkInviteCode.stories.impl.ts | 4 ++-- .../frontend/src/components/MkMediaList.vue | 2 +- packages/frontend/src/components/MkMenu.vue | 2 +- .../frontend/src/components/MkPageWindow.vue | 2 +- .../frontend/src/components/MkPagination.vue | 2 +- .../MkUserSetupDialog.Follow.stories.impl.ts | 4 ++-- .../MkUserSetupDialog.User.stories.impl.ts | 2 +- .../MkUserSetupDialog.stories.impl.ts | 4 ++-- .../frontend/src/components/MkWidgets.vue | 2 +- .../components/global/MkAcct.stories.impl.ts | 2 +- .../global/MkAvatar.stories.impl.ts | 2 +- .../MkMisskeyFlavoredMarkdown.stories.impl.ts | 2 +- .../components/global/MkStickyContainer.vue | 2 +- .../components/global/MkUrl.stories.impl.ts | 2 +- .../global/MkUserName.stories.impl.ts | 2 +- .../src/components/global/RouterView.vue | 2 +- .../src/components/page/page.block.vue | 2 +- .../src/components/page/page.image.vue | 2 +- .../src/components/page/page.note.vue | 2 +- .../src/components/page/page.section.vue | 2 +- .../src/components/page/page.text.vue | 2 +- packages/frontend/src/directives/hotkey.ts | 2 +- packages/frontend/src/directives/index.ts | 22 +++++++++---------- .../frontend/src/pages/admin/_header_.vue | 2 +- .../frontend/src/pages/admin/roles.edit.vue | 2 +- packages/frontend/src/pages/admin/roles.vue | 2 +- packages/frontend/src/pages/clip.vue | 2 +- .../frontend/src/pages/my-antennas/edit.vue | 2 +- .../frontend/src/pages/my-antennas/index.vue | 2 +- .../frontend/src/pages/my-clips/index.vue | 2 +- .../frontend/src/pages/my-lists/index.vue | 2 +- .../frontend/src/pages/settings/navbar.vue | 2 +- .../src/pages/settings/theme.manage.vue | 2 +- .../frontend/src/pages/settings/theme.vue | 2 +- packages/frontend/src/pages/theme-editor.vue | 2 +- .../src/pages/user/home.stories.impl.ts | 4 ++-- .../src/scripts/upload/compress-config.ts | 2 +- packages/frontend/src/ui/_common_/common.vue | 4 ++-- .../src/ui/_common_/navbar-for-mobile.vue | 4 ++-- packages/frontend/src/ui/classic.header.vue | 4 ++-- packages/frontend/src/ui/deck/column.vue | 2 +- packages/frontend/src/ui/deck/list-column.vue | 2 +- packages/frontend/src/ui/deck/main-column.vue | 2 +- .../src/widgets/server-metric/index.vue | 2 +- packages/frontend/test/init.ts | 4 ++-- packages/frontend/vite.config.local-dev.ts | 2 +- packages/frontend/vite.config.ts | 7 +++--- 57 files changed, 84 insertions(+), 83 deletions(-) diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts index 550e08d7f7..535adc9c85 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts @@ -6,7 +6,7 @@ import { parse } from 'acorn'; import { generate } from 'astring'; import { describe, expect, it } from 'vitest'; -import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name'; +import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name.js'; import type * as estree from 'estree'; function parseExpression(code: string): estree.Expression { diff --git a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts b/packages/frontend/src/components/MkAbuseReport.stories.impl.ts index 3b64529620..77e7c84d5c 100644 --- a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts +++ b/packages/frontend/src/components/MkAbuseReport.stories.impl.ts @@ -7,8 +7,8 @@ import { action } from '@storybook/addon-actions'; import { StoryObj } from '@storybook/vue3'; import { rest } from 'msw'; -import { abuseUserReport } from '../../.storybook/fakes'; -import { commonHandlers } from '../../.storybook/mocks'; +import { abuseUserReport } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; import MkAbuseReport from './MkAbuseReport.vue'; export const Default = { render(args) { diff --git a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts index b45d54679b..dc842b3d1b 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts +++ b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts @@ -7,8 +7,8 @@ import { action } from '@storybook/addon-actions'; import { StoryObj } from '@storybook/vue3'; import { rest } from 'msw'; -import { userDetailed } from '../../.storybook/fakes'; -import { commonHandlers } from '../../.storybook/mocks'; +import { userDetailed } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; import MkAbuseReportWindow from './MkAbuseReportWindow.vue'; export const Default = { render(args) { diff --git a/packages/frontend/src/components/MkAccountMoved.stories.impl.ts b/packages/frontend/src/components/MkAccountMoved.stories.impl.ts index a6d4d18c1b..33c6c24631 100644 --- a/packages/frontend/src/components/MkAccountMoved.stories.impl.ts +++ b/packages/frontend/src/components/MkAccountMoved.stories.impl.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { StoryObj } from '@storybook/vue3'; -import { userDetailed } from '../../.storybook/fakes'; +import { userDetailed } from '../../.storybook/fakes.js'; import MkAccountMoved from './MkAccountMoved.vue'; export const Default = { render(args) { diff --git a/packages/frontend/src/components/MkAchievements.stories.impl.ts b/packages/frontend/src/components/MkAchievements.stories.impl.ts index a67e1def13..6d972467b1 100644 --- a/packages/frontend/src/components/MkAchievements.stories.impl.ts +++ b/packages/frontend/src/components/MkAchievements.stories.impl.ts @@ -6,8 +6,8 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { StoryObj } from '@storybook/vue3'; import { rest } from 'msw'; -import { userDetailed } from '../../.storybook/fakes'; -import { commonHandlers } from '../../.storybook/mocks'; +import { userDetailed } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; import MkAchievements from './MkAchievements.vue'; import { ACHIEVEMENT_TYPES } from '@/scripts/achievements.js'; export const Empty = { diff --git a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts index 8232759ba0..969519386f 100644 --- a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts +++ b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts @@ -9,8 +9,8 @@ import { expect } from '@storybook/jest'; import { userEvent, waitFor, within } from '@storybook/testing-library'; import { StoryObj } from '@storybook/vue3'; import { rest } from 'msw'; -import { userDetailed } from '../../.storybook/fakes'; -import { commonHandlers } from '../../.storybook/mocks'; +import { userDetailed } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; import MkAutocomplete from './MkAutocomplete.vue'; import MkInput from './MkInput.vue'; import { tick } from '@/scripts/test-utils.js'; diff --git a/packages/frontend/src/components/MkAvatars.stories.impl.ts b/packages/frontend/src/components/MkAvatars.stories.impl.ts index 659c0eebdf..d41b64695f 100644 --- a/packages/frontend/src/components/MkAvatars.stories.impl.ts +++ b/packages/frontend/src/components/MkAvatars.stories.impl.ts @@ -6,8 +6,8 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { StoryObj } from '@storybook/vue3'; import { rest } from 'msw'; -import { userDetailed } from '../../.storybook/fakes'; -import { commonHandlers } from '../../.storybook/mocks'; +import { userDetailed } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; import MkAvatars from './MkAvatars.vue'; export const Default = { render(args) { diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index b78252be89..e29cf472f7 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 33a6786d03..f1bcdec7fb 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -224,7 +224,7 @@ import { claimAchievement } from '@/scripts/achievements.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; -import MkPagination from '@/components/MkPagination.vue'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; @@ -307,7 +307,7 @@ const renotesPagination = computed(() => ({ params: { noteId: appearNote.value.id, }, -})); +} satisfies Paging)); const reactionsPagination = computed(() => ({ endpoint: 'notes/reactions', @@ -316,7 +316,7 @@ const reactionsPagination = computed(() => ({ noteId: appearNote.value.id, type: reactionTabType.value, }, -})); +} satisfies Paging)); useNoteCapture({ rootEl: el, diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue index 5f3f5b81dd..d924a54ffb 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue @@ -37,15 +37,15 @@ SPDX-License-Identifier: AGPL-3.0-only import { i18n } from '@/i18n.js'; import MkFolder from '@/components/MkFolder.vue'; import XUser from '@/components/MkUserSetupDialog.User.vue'; -import MkPagination from '@/components/MkPagination.vue'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; -const pinnedUsers = { endpoint: 'pinned-users', noPaging: true }; +const pinnedUsers = { endpoint: 'pinned-users', noPaging: true } satisfies Paging; const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { state: 'alive', origin: 'local', sort: '+follower', -} }; +} } satisfies Paging; diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index baee85866c..9cf4be778c 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -527,6 +527,10 @@ export const routes = [{ path: '/clicker', component: page(() => import('./pages/clicker.vue')), loginRequired: true, +}, { + path: '/drop-and-fusion', + component: page(() => import('./pages/drop-and-fusion.vue')), + loginRequired: true, }, { path: '/timeline', component: page(() => import('./pages/timeline.vue')), diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 0b966ff199..acde78f5fd 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -92,7 +92,13 @@ export type OperationType = typeof operationTypes[number]; * @param soundStore サウンド設定 * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする */ -export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) { +export async function loadAudio(soundStore: { + type: Exclude; +} | { + type: '_driveFile_'; + fileId: string; + fileUrl: string; +}, options?: { useCache?: boolean; }) { if (_DEV_) console.log('loading audio. opts:', options); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { @@ -179,18 +185,31 @@ export async function playFile(soundStore: SoundStore) { createSourceNode(buffer, soundStore.volume)?.start(); } -export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null { +export async function playRaw(type: Exclude, volume = 1, pan = 0, playbackRate = 1) { + const buffer = await loadAudio({ type }); + if (!buffer) return; + createSourceNode(buffer, volume, pan, playbackRate)?.start(); +} + +export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1) : AudioBufferSourceNode | null { const masterVolume = defaultStore.state.sound_masterVolume; if (isMute() || masterVolume === 0 || volume === 0) { return null; } + const panNode = ctx.createStereoPanner(); + panNode.pan.value = pan; + const gainNode = ctx.createGain(); gainNode.gain.value = masterVolume * volume; const soundSource = ctx.createBufferSource(); soundSource.buffer = buffer; - soundSource.connect(gainNode).connect(ctx.destination); + soundSource.playbackRate.value = playbackRate; + soundSource + .connect(panNode) + .connect(gainNode) + .connect(ctx.destination); return soundSource; } diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index b970ff1df4..e50002dc2c 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -27,6 +27,11 @@ function toolsMenuItems(): MenuItem[] { to: '/clicker', text: '🍪👈', icon: 'ti ti-cookie', + }, { + type: 'link', + to: '/drop-and-fusion', + text: 'Drop & Fusion', + icon: 'ti ti-apple', }, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? { type: 'link', to: '/custom-emojis-manager', From 9eae82de1d4f9157602451e26e734c8f4ae94bea Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sat, 6 Jan 2024 13:33:56 +0100 Subject: [PATCH 73/99] chore(dependabot) open-pull-requests-limit=10? Somehow it's not opening any PR, so try higher count --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c5755315fc..d4678ec5e0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,7 +17,7 @@ updates: directory: "/" schedule: interval: daily - open-pull-requests-limit: 5 + open-pull-requests-limit: 10 # List dependencies required to be updated together, sharing the same version numbers. # Those who simply have the common owner (e.g. @fastify) don't need to be listed. groups: From 0815a5235d226434e17ead0166227f5ec60133b8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 7 Jan 2024 09:24:04 +0900 Subject: [PATCH 74/99] tweak game --- .../assets/drop-and-fusion/dropper.png | Bin 0 -> 32415 bytes .../assets/drop-and-fusion/frame-dark.svg | 28 ++++++ .../assets/drop-and-fusion/frame-light.svg | 28 ++++++ .../frontend/assets/drop-and-fusion/frame.svg | 25 ------ .../frontend/src/pages/drop-and-fusion.vue | 80 ++++++++++++++---- 5 files changed, 120 insertions(+), 41 deletions(-) create mode 100644 packages/frontend/assets/drop-and-fusion/dropper.png create mode 100644 packages/frontend/assets/drop-and-fusion/frame-dark.svg create mode 100644 packages/frontend/assets/drop-and-fusion/frame-light.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/frame.svg diff --git a/packages/frontend/assets/drop-and-fusion/dropper.png b/packages/frontend/assets/drop-and-fusion/dropper.png new file mode 100644 index 0000000000000000000000000000000000000000..f4300aa5c06f6001a87c01f8525847b294f43be3 GIT binary patch literal 32415 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 I=akR{02N9P_y7O^ literal 0 HcmV?d00001 diff --git a/packages/frontend/assets/drop-and-fusion/frame-dark.svg b/packages/frontend/assets/drop-and-fusion/frame-dark.svg new file mode 100644 index 0000000000..3fa7c0da81 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/frame-dark.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/frontend/assets/drop-and-fusion/frame-light.svg b/packages/frontend/assets/drop-and-fusion/frame-light.svg new file mode 100644 index 0000000000..6052ccbaa0 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/frame-light.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/frontend/assets/drop-and-fusion/frame.svg b/packages/frontend/assets/drop-and-fusion/frame.svg deleted file mode 100644 index 4276dae833..0000000000 --- a/packages/frontend/assets/drop-and-fusion/frame.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index d0ca5157ef..7f4a885b44 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -11,7 +11,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- SCORE: +
SCORE:
+
HIGH SCORE: -
@@ -33,7 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- + +
{{ comboPrev }} Chain!
+ (); const canvasEl = shallowRef(); @@ -191,7 +196,7 @@ const FRUITS = [{ const GAME_WIDTH = 450; const GAME_HEIGHT = 600; -const PHYSICS_QUALITY_FACTOR = 32; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる +const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる let viewScaleX = 1; let viewScaleY = 1; @@ -203,6 +208,7 @@ const comboPrev = ref(0); const dropReady = ref(true); const gameOver = ref(false); const gameStarted = ref(false); +const highScore = ref(null); class Game extends EventEmitter<{ changeScore: (score: number) => void; @@ -251,6 +257,8 @@ class Game extends EventEmitter<{ this.emit('changeScore', value); } + private comboIntervalId: number | null = null; + constructor() { super(); @@ -294,6 +302,8 @@ class Game extends EventEmitter<{ //#region walls const WALL_OPTIONS: Matter.IChamferableBodyDefinition = { isStatic: true, + friction: 0.7, + slop: 1.0, render: { strokeStyle: 'transparent', fillStyle: 'transparent', @@ -308,7 +318,7 @@ class Game extends EventEmitter<{ ]); //#endregion - this.overflowCollider = Matter.Bodies.rectangle(GAME_WIDTH / 2, 0, GAME_WIDTH, 125, { + this.overflowCollider = Matter.Bodies.rectangle(GAME_WIDTH / 2, 0, GAME_WIDTH, 200, { isStatic: true, isSensor: true, render: { @@ -328,11 +338,13 @@ class Game extends EventEmitter<{ private createBody(fruit: typeof FRUITS[number], x: number, y: number) { return Matter.Bodies.circle(x, y, fruit.size / 2, { label: fruit.id, - density: 0.0005, + //density: 0.0005, + density: fruit.size / 1000, + restitution: 0.2, frictionAir: 0.01, - restitution: 0.4, - friction: 0.5, + friction: 0.7, frictionStatic: 5, + slop: 1.0, //mass: 0, render: { sprite: { @@ -372,7 +384,7 @@ class Game extends EventEmitter<{ this.activeBodyIds.push(body.id); }, 100); - const additionalScore = Math.round(currentFruit.score * (1 + (this.combo / 3))); + const additionalScore = Math.round(currentFruit.score * (1 + ((this.combo - 1) / 3))); this.score += additionalScore; const pan = ((newX / GAME_WIDTH) - 0.5) * 2; @@ -449,7 +461,7 @@ class Game extends EventEmitter<{ } }); - window.setInterval(() => { + this.comboIntervalId = window.setInterval(() => { if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) { this.combo = 0; } @@ -469,7 +481,7 @@ class Game extends EventEmitter<{ this.emit('changeStock', this.stock); const x = Math.min(GAME_WIDTH - this.PLAYAREA_MARGIN - (st.fruit.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.fruit.size / 2), _x)); - const body = this.createBody(st.fruit, x, st.fruit.size / 2); + const body = this.createBody(st.fruit, x, 50 + st.fruit.size / 2); Matter.Composite.add(this.engine.world, body); this.activeBodyIds.push(body.id); this.latestDroppedBodyId = body.id; @@ -480,6 +492,7 @@ class Game extends EventEmitter<{ } public dispose() { + if (this.comboIntervalId) window.clearInterval(this.comboIntervalId); Matter.Render.stop(this.render); Matter.Runner.stop(this.runner); Matter.World.clear(this.engine.world, false); @@ -567,10 +580,28 @@ function attachGame() { currentPick.value = null; dropReady.value = false; gameOver.value = true; + + if (score.value > (highScore.value ?? 0)) { + highScore.value = score.value; + + misskeyApi('i/registry/set', { + scope: ['dropAndFusionGame'], + key: 'highScore', + value: highScore.value, + }); + } }); } -onMounted(() => { +onMounted(async () => { + try { + highScore.value = await misskeyApi('i/registry/get', { + scope: ['dropAndFusionGame'], + key: 'highScore', + }); + } catch (err) { + } + game = new Game(); attachGame(); @@ -667,7 +698,9 @@ definePageMetadata({ top: 0; left: 0; width: 100%; - filter: drop-shadow(0 6px 16px #0007); + // なんかiOSでちらつく + //filter: drop-shadow(0 6px 16px #0007); + border-radius: 16px; pointer-events: none; user-select: none; } @@ -699,13 +732,28 @@ definePageMetadata({ text-align: center; font-weight: bold; font-style: oblique; + color: #fff; + -webkit-text-stroke: 1px rgb(255, 145, 0); + text-shadow: 0 0 6px #0005; pointer-events: none; user-select: none; } .currentFruit { position: absolute; - margin-top: 20px; + margin-top: 80px; + z-index: 2; + filter: drop-shadow(0 6px 16px #0007); + pointer-events: none; + user-select: none; +} + +.dropper { + position: absolute; + top: 0; + width: 70px; + margin-top: -10px; + margin-left: -30px; z-index: 2; filter: drop-shadow(0 6px 16px #0007); pointer-events: none; @@ -714,7 +762,7 @@ definePageMetadata({ .currentFruitArrow { position: absolute; - margin-top: 20px; + margin-top: 100px; z-index: 3; animation: currentFruitArrow 2s ease infinite; pointer-events: none; @@ -723,10 +771,10 @@ definePageMetadata({ .dropGuide { position: absolute; - top: 50px; + top: 120px; z-index: 3; width: 3px; - height: calc(100% - 50px); + height: calc(100% - 120px); background: #f002; pointer-events: none; user-select: none; From f2dee7b25eb473796ff77e2abfae88f174fd5b90 Mon Sep 17 00:00:00 2001 From: _ Date: Sun, 7 Jan 2024 09:57:01 +0900 Subject: [PATCH 75/99] =?UTF-8?q?Fix:=20=E3=83=AA=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3=E3=81=AE?= =?UTF-8?q?=E3=80=8C=E3=83=AA=E3=83=8E=E3=83=BC=E3=83=88=E3=82=92=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=80=8D=E3=81=8C=E6=AD=A3=E3=81=97=E3=81=8F=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=81=97=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#12932)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: list timeline withRenotes * add CHANGELOG --- CHANGELOG.md | 1 + packages/backend/src/server/api/stream/channels/user-list.ts | 4 ++++ packages/frontend/src/components/MkTimeline.vue | 5 ++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6e2db950..8c27349f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### General - Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加 +- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正 ### Client - Feat: 新しいゲームを追加 diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 909b5a5e03..e0245814c4 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -21,6 +21,7 @@ class UserListChannel extends Channel { private membershipsMap: Record | undefined> = {}; private listUsersClock: NodeJS.Timeout; private withFiles: boolean; + private withRenotes: boolean; constructor( private userListsRepository: UserListsRepository, @@ -39,6 +40,7 @@ class UserListChannel extends Channel { public async init(params: any) { this.listId = params.listId as string; this.withFiles = params.withFiles ?? false; + this.withRenotes = params.withRenotes ?? true; // Check existence and owner const listExist = await this.userListsRepository.exist({ @@ -104,6 +106,8 @@ class UserListChannel extends Channel { } } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index d5adc02ca7..63f779dbde 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -132,6 +132,7 @@ function connectChannel() { connection.on('mention', onNote); } else if (props.src === 'list') { connection = stream.useChannel('userList', { + withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list, }); @@ -198,6 +199,7 @@ function updatePaginationQuery() { } else if (props.src === 'list') { endpoint = 'notes/user-list-timeline'; query = { + withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list, }; @@ -236,8 +238,9 @@ function refreshEndpointAndChannel() { updatePaginationQuery(); } +// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる // IDが切り替わったら切り替え先のTLを表示させたい -watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel); +watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel); // 初回表示用 refreshEndpointAndChannel(); From 4ea030d66916777595bf1429fab4d5c1b93d4a5d Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 7 Jan 2024 10:35:39 +0900 Subject: [PATCH 76/99] tweak game --- .../frontend/src/pages/drop-and-fusion.vue | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 7f4a885b44..6014931562 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
@@ -221,10 +221,10 @@ class Game extends EventEmitter<{ private COMBO_INTERVAL = 1000; public readonly DROP_INTERVAL = 500; private PLAYAREA_MARGIN = 25; + private STOCK_MAX = 4; private engine: Matter.Engine; private render: Matter.Render; private runner: Matter.Runner; - private detector: Matter.Detector; private overflowCollider: Matter.Body; private isGameOver = false; @@ -286,7 +286,7 @@ class Game extends EventEmitter<{ wireframeBackground: 'transparent', // transparent to hide wireframes: false, showSleeping: false, - pixelRatio: window.devicePixelRatio, + pixelRatio: Math.max(2, window.devicePixelRatio), }, }); @@ -295,8 +295,6 @@ class Game extends EventEmitter<{ this.runner = Matter.Runner.create(); Matter.Runner.run(this.runner, this.engine); - this.detector = Matter.Detector.create(); - this.engine.world.bodies = []; //#region walls @@ -412,7 +410,7 @@ class Game extends EventEmitter<{ } public start() { - for (let i = 0; i < 4; i++) { + for (let i = 0; i < this.STOCK_MAX; i++) { this.stock.push({ id: Math.random().toString(), fruit: FRUITS.filter(x => x.available)[Math.floor(Math.random() * FRUITS.filter(x => x.available).length)], @@ -423,8 +421,8 @@ class Game extends EventEmitter<{ // TODO: fusion予約状態のアイテムは光らせるなどの演出をすると楽しそう let fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = []; - const minCollisionDepthForSound = 2.5; - const maxCollisionDepthForSound = 9; + const minCollisionEnergyForSound = 2.5; + const maxCollisionEnergyForSound = 9; const soundPitchMax = 4; const soundPitchMin = 0.5; @@ -451,8 +449,8 @@ class Game extends EventEmitter<{ } } else { const energy = pairs.collision.depth; - if (energy > minCollisionDepthForSound) { - const vol = (Math.min(maxCollisionDepthForSound, energy - minCollisionDepthForSound) / maxCollisionDepthForSound) / 4; + if (energy > minCollisionEnergyForSound) { + const vol = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4; const pan = ((((bodyA.position.x + bodyB.position.x) / 2) / GAME_WIDTH) - 0.5) * 2; const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10))); sound.playRaw('syuilo/poi1', vol, pan, pitch); @@ -700,7 +698,6 @@ definePageMetadata({ width: 100%; // なんかiOSでちらつく //filter: drop-shadow(0 6px 16px #0007); - border-radius: 16px; pointer-events: none; user-select: none; } @@ -710,7 +707,8 @@ definePageMetadata({ display: block; z-index: 1; margin-top: -50px; - max-width: 100%; + width: 100% !important; + height: auto !important; pointer-events: none; user-select: none; } From 2a9db983fcd79e1993d5ea5b03e4979c1a578d7d Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 7 Jan 2024 02:35:58 +0100 Subject: [PATCH 77/99] feat: export clips (#12931) * feat: export clips * Update CHANGELOG.md --- CHANGELOG.md | 1 + locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + packages/backend/src/core/QueueService.ts | 10 + .../backend/src/queue/QueueProcessorModule.ts | 2 + .../src/queue/QueueProcessorService.ts | 3 + .../processors/ExportClipsProcessorService.ts | 206 ++++++++++++++++++ .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 4 +- .../server/api/endpoints/i/export-clips.ts | 35 +++ packages/backend/test/e2e/exports.ts | 194 +++++++++++++++++ packages/backend/test/utils.ts | 2 +- .../src/pages/settings/import-export.vue | 12 + 13 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/queue/processors/ExportClipsProcessorService.ts create mode 100644 packages/backend/src/server/api/endpoints/i/export-clips.ts create mode 100644 packages/backend/test/e2e/exports.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c27349f61..0d2fb4ccd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ ### Server - Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました - Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) +- Enhance: クリップをエクスポートできるように ## 2023.12.2 diff --git a/locales/index.d.ts b/locales/index.d.ts index 99bc0fc04f..75517fa2ad 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2256,6 +2256,7 @@ export interface Locale { "_exportOrImport": { "allNotes": string; "favoritedNotes": string; + "clips": string; "followingList": string; "muteList": string; "blockingList": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7cf5663a72..8b6b119d7e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2159,6 +2159,7 @@ _profile: _exportOrImport: allNotes: "全てのノート" favoritedNotes: "お気に入りにしたノート" + clips: "クリップ" followingList: "フォロー" muteList: "ミュート" blockingList: "ブロック" diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 4f99dee64e..dc3f248da4 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -182,6 +182,16 @@ export class QueueService { }); } + @bindThis + public createExportClipsJob(user: ThinUser) { + return this.dbQueue.add('exportClips', { + user: { id: user.id }, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + @bindThis public createExportFavoritesJob(user: ThinUser) { return this.dbQueue.add('exportFavorites', { diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index e6327002c5..9c52c7d76a 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -24,6 +24,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; +import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js'; import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; @@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor DeleteDriveFilesProcessorService, ExportCustomEmojisProcessorService, ExportNotesProcessorService, + ExportClipsProcessorService, ExportFavoritesProcessorService, ExportFollowingProcessorService, ExportMutingProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index b872dd65f7..bcc1a69f80 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -16,6 +16,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; +import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; @@ -91,6 +92,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService, private exportNotesProcessorService: ExportNotesProcessorService, + private exportClipsProcessorService: ExportClipsProcessorService, private exportFavoritesProcessorService: ExportFavoritesProcessorService, private exportFollowingProcessorService: ExportFollowingProcessorService, private exportMutingProcessorService: ExportMutingProcessorService, @@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); case 'exportNotes': return this.exportNotesProcessorService.process(job); + case 'exportClips': return this.exportClipsProcessorService.process(job); case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); case 'exportFollowing': return this.exportFollowingProcessorService.process(job); case 'exportMuting': return this.exportMutingProcessorService.process(job); diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts new file mode 100644 index 0000000000..5221497bd3 --- /dev/null +++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as fs from 'node:fs'; +import { Writable } from 'node:stream'; +import { Inject, Injectable, StreamableFile } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { format as dateFormat } from 'date-fns'; +import { DI } from '@/di-symbols.js'; +import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { createTemp } from '@/misc/create-temp.js'; +import type { MiPoll } from '@/models/Poll.js'; +import type { MiNote } from '@/models/Note.js'; +import { bindThis } from '@/decorators.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { Packed } from '@/misc/json-schema.js'; +import { IdService } from '@/core/IdService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbJobDataWithUser } from '../types.js'; + +@Injectable() +export class ExportClipsProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + private idService: IdService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('export-clips'); + } + + @bindThis + public async process(job: Bull.Job): Promise { + this.logger.info(`Exporting clips of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + return; + } + + // Create temp file + const [path, cleanup] = await createTemp(); + + this.logger.info(`Temp file is ${path}`); + + try { + const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' })); + const writer = stream.getWriter(); + writer.closed.catch(this.logger.error); + + await writer.write('['); + + await this.processClips(writer, user, job); + + await writer.write(']'); + await writer.close(); + + this.logger.succ(`Exported to: ${path}`); + + const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; + const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); + + this.logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } + } + + async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job) { + let exportedClipsCount = 0; + let cursor: MiClip['id'] | null = null; + + while (true) { + const clips = await this.clipsRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); + + if (clips.length === 0) { + job.updateProgress(100); + break; + } + + cursor = clips.at(-1)?.id ?? null; + + for (const clip of clips) { + // Stringify but remove the last `]}` + const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2); + const isFirst = exportedClipsCount === 0; + await writer.write(isFirst ? content : ',\n' + content); + + await this.processClipNotes(writer, clip.id); + + await writer.write(']}'); + exportedClipsCount++; + } + + const total = await this.clipsRepository.countBy({ + userId: user.id, + }); + + job.updateProgress(exportedClipsCount / total); + } + } + + async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise { + let exportedClipNotesCount = 0; + let cursor: MiClipNote['id'] | null = null; + + while (true) { + const clipNotes = await this.clipNotesRepository.find({ + where: { + clipId, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + relations: ['note', 'note.user'], + }) as (MiClipNote & { note: MiNote & { user: MiUser } })[]; + + if (clipNotes.length === 0) { + break; + } + + cursor = clipNotes.at(-1)?.id ?? null; + + for (const clipNote of clipNotes) { + let poll: MiPoll | undefined; + if (clipNote.note.hasPoll) { + poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id }); + } + const content = JSON.stringify(this.serializeClipNote(clipNote, poll)); + const isFirst = exportedClipNotesCount === 0; + await writer.write(isFirst ? content : ',\n' + content); + + exportedClipNotesCount++; + } + } + } + + private serializeClip(clip: MiClip): Record { + return { + id: clip.id, + name: clip.name, + description: clip.description, + lastClippedAt: clip.lastClippedAt?.toISOString(), + clipNotes: [], + }; + } + + private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record { + return { + id: clip.id, + createdAt: this.idService.parse(clip.id).date.toISOString(), + note: { + id: clip.note.id, + text: clip.note.text, + createdAt: this.idService.parse(clip.note.id).date.toISOString(), + fileIds: clip.note.fileIds, + replyId: clip.note.replyId, + renoteId: clip.note.renoteId, + poll: poll, + cw: clip.note.cw, + visibility: clip.note.visibility, + visibleUserIds: clip.note.visibleUserIds, + localOnly: clip.note.localOnly, + reactionAcceptance: clip.note.reactionAcceptance, + uri: clip.note.uri, + url: clip.note.url, + user: { + id: clip.note.user.id, + name: clip.note.user.name, + username: clip.note.user.username, + host: clip.note.user.host, + uri: clip.note.user.uri, + }, + }, + }; + } +} diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 86a64d7121..a3a9805444 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -208,6 +208,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportClips from './endpoints/i/export-clips.js'; import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; @@ -569,6 +570,7 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default }; const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default }; const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default }; +const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default }; const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default }; const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default }; const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default }; @@ -934,6 +936,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, @@ -1293,6 +1296,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 41232091c6..bd8aa4af72 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Schema } from '@/misc/json-schema.js'; import { permissions } from 'misskey-js'; +import type { Schema } from '@/misc/json-schema.js'; import { RolePolicies } from '@/core/RoleService.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; @@ -209,6 +209,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportClips from './endpoints/i/export-clips.js'; import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; @@ -568,6 +569,7 @@ const eps = [ ['i/export-following', ep___i_exportFollowing], ['i/export-mute', ep___i_exportMute], ['i/export-notes', ep___i_exportNotes], + ['i/export-clips', ep___i_exportClips], ['i/export-favorites', ep___i_exportFavorites], ['i/export-user-lists', ep___i_exportUserLists], ['i/export-antennas', ep___i_exportAntennas], diff --git a/packages/backend/src/server/api/endpoints/i/export-clips.ts b/packages/backend/src/server/api/endpoints/i/export-clips.ts new file mode 100644 index 0000000000..9435a2b23c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/export-clips.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + secure: true, + requireCredential: true, + limit: { + duration: ms('1day'), + max: 1, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportClipsJob(me); + }); + } +} diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts new file mode 100644 index 0000000000..9686f2b7fd --- /dev/null +++ b/packages/backend/test/e2e/exports.ts @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, startServer, startJobQueue, port, post } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; + +describe('export-clips', () => { + let app: INestApplicationContext; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + + // XXX: Any better way to get the result? + async function pollFirstDriveFile() { + while (true) { + const files = (await api('/drive/files', {}, alice)).body; + if (!files.length) { + await new Promise(r => setTimeout(r, 100)); + continue; + } + if (files.length > 1) { + throw new Error('Too many files?'); + } + const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body; + const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`)); + return await res.json(); + } + } + + beforeAll(async () => { + app = await startServer(); + await startJobQueue(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + // Clean all clips and files of alice + const clips = (await api('/clips/list', {}, alice)).body; + for (const clip of clips) { + const res = await api('/clips/delete', { clipId: clip.id }, alice); + if (res.status !== 204) { + throw new Error('Failed to delete clip'); + } + } + const files = (await api('/drive/files', {}, alice)).body; + for (const file of files) { + const res = await api('/drive/files/delete', { fileId: file.id }, alice); + if (res.status !== 204) { + throw new Error('Failed to delete file'); + } + } + }); + + test('basic export', async () => { + let res = await api('/clips/create', { + name: 'foo', + description: 'bar', + }, alice); + assert.strictEqual(res.status, 200); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'foo'); + assert.strictEqual(exported[0].description, 'bar'); + assert.strictEqual(exported[0].clipNotes.length, 0); + }); + + test('export with notes', async () => { + let res = await api('/clips/create', { + name: 'foo', + description: 'bar', + }, alice); + assert.strictEqual(res.status, 200); + const clip = res.body; + + const note1 = await post(alice, { + text: 'baz1', + }); + + const note2 = await post(alice, { + text: 'baz2', + poll: { + choices: ['sakura', 'izumi', 'ako'], + }, + }); + + for (const note of [note1, note2]) { + res = await api('/clips/add-note', { + clipId: clip.id, + noteId: note.id, + }, alice); + assert.strictEqual(res.status, 204); + } + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'foo'); + assert.strictEqual(exported[0].description, 'bar'); + assert.strictEqual(exported[0].clipNotes.length, 2); + assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); + assert.strictEqual(exported[0].clipNotes[1].note.text, 'baz2'); + assert.deepStrictEqual(exported[0].clipNotes[1].note.poll.choices[0], 'sakura'); + }); + + test('multiple clips', async () => { + let res = await api('/clips/create', { + name: 'kawaii', + description: 'kawaii', + }, alice); + assert.strictEqual(res.status, 200); + const clip1 = res.body; + + res = await api('/clips/create', { + name: 'yuri', + description: 'yuri', + }, alice); + assert.strictEqual(res.status, 200); + const clip2 = res.body; + + const note1 = await post(alice, { + text: 'baz1', + }); + + const note2 = await post(alice, { + text: 'baz2', + }); + + res = await api('/clips/add-note', { + clipId: clip1.id, + noteId: note1.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/clips/add-note', { + clipId: clip2.id, + noteId: note2.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'kawaii'); + assert.strictEqual(exported[0].clipNotes.length, 1); + assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); + assert.strictEqual(exported[1].name, 'yuri'); + assert.strictEqual(exported[1].clipNotes.length, 1); + assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2'); + }); + + test('Clipping other user\'s note', async () => { + let res = await api('/clips/create', { + name: 'kawaii', + description: 'kawaii', + }, alice); + assert.strictEqual(res.status, 200); + const clip = res.body; + + const note = await post(bob, { + text: 'baz', + visibility: 'followers', + }); + + res = await api('/clips/add-note', { + clipId: clip.id, + noteId: note.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'kawaii'); + assert.strictEqual(exported[0].clipNotes.length, 1); + assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz'); + assert.strictEqual(exported[0].clipNotes[0].note.user.username, 'bob'); + }); +}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 46b8ea9cdd..7c9428d476 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -17,7 +17,7 @@ import { entities } from '../src/postgres.js'; import { loadConfig } from '../src/config.js'; import type * as misskey from 'misskey-js'; -export { server as startServer } from '@/boot/common.js'; +export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; interface UserToken { token: string; diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 990eff99c1..70d718f1ab 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -21,6 +21,14 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.export }} + + + + + + {{ i18n.ts.export }} + +
@@ -157,6 +165,10 @@ const exportFavorites = () => { misskeyApi('i/export-favorites', {}).then(onExportSuccess).catch(onError); }; +const exportClips = () => { + misskeyApi('i/export-clips', {}).then(onExportSuccess).catch(onError); +}; + const exportFollowing = () => { misskeyApi('i/export-following', { excludeMuting: excludeMutingUsers.value, From 00e195f50bcc29ee28b6ae11f39b7661a10f2b16 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 7 Jan 2024 13:19:10 +0900 Subject: [PATCH 78/99] tweak game --- locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + .../assets/drop-and-fusion/keycap_1.png | Bin 0 -> 29193 bytes .../assets/drop-and-fusion/keycap_10.png | Bin 0 -> 33717 bytes .../assets/drop-and-fusion/keycap_2.png | Bin 0 -> 32324 bytes .../assets/drop-and-fusion/keycap_3.png | Bin 0 -> 33127 bytes .../assets/drop-and-fusion/keycap_4.png | Bin 0 -> 31182 bytes .../assets/drop-and-fusion/keycap_5.png | Bin 0 -> 32745 bytes .../assets/drop-and-fusion/keycap_6.png | Bin 0 -> 32100 bytes .../assets/drop-and-fusion/keycap_7.png | Bin 0 -> 31318 bytes .../assets/drop-and-fusion/keycap_8.png | Bin 0 -> 32886 bytes .../assets/drop-and-fusion/keycap_9.png | Bin 0 -> 32483 bytes .../frontend/src/pages/drop-and-fusion.vue | 291 +++++++++++++++--- packages/frontend/src/router.ts | 2 +- packages/frontend/src/ui/_common_/common.ts | 4 +- 15 files changed, 245 insertions(+), 54 deletions(-) create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_1.png create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_10.png create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_2.png create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_3.png create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_4.png create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_5.png create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_6.png create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_7.png create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_8.png create mode 100644 packages/frontend/assets/drop-and-fusion/keycap_9.png diff --git a/locales/index.d.ts b/locales/index.d.ts index 75517fa2ad..8dfb81790e 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1192,6 +1192,7 @@ export interface Locale { "decorate": string; "addMfmFunction": string; "enableQuickAddMfmFunction": string; + "bubbleGame": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8b6b119d7e..d92c5f9a14 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1189,6 +1189,7 @@ seasonalScreenEffect: "季節に応じた画面の演出" decorate: "デコる" addMfmFunction: "装飾を追加" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" +bubbleGame: "バブルゲーム" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/frontend/assets/drop-and-fusion/keycap_1.png b/packages/frontend/assets/drop-and-fusion/keycap_1.png new file mode 100644 index 0000000000000000000000000000000000000000..d672f2854a8e22451f7329a1603c3241615c5936 GIT binary patch literal 29193 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 literal 0 HcmV?d00001 diff --git a/packages/frontend/assets/drop-and-fusion/keycap_10.png b/packages/frontend/assets/drop-and-fusion/keycap_10.png new file mode 100644 index 0000000000000000000000000000000000000000..32cf19354065bc8b720e46553c4da1ba5cedf4ea GIT binary patch literal 33717 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
@@ -103,6 +104,7 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Matter from 'matter-js'; import { Ref, onMounted, ref, shallowRef } from 'vue'; import { EventEmitter } from 'eventemitter3'; +import * as Misskey from 'misskey-js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import * as sound from '@/scripts/sound.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; @@ -115,6 +117,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { useInterval } from '@/scripts/use-interval.js'; import MkSelect from '@/components/MkSelect.vue'; +import { apiUrl } from '@/config.js'; +import { $i } from '@/account.js'; type Mono = { id: string; @@ -788,6 +792,46 @@ async function start() { game.start(); } +function getGameImageDriveFile() { + return new Promise(res => { + canvasEl.value?.toBlob(blob => { + if (!blob) return res(null); + if ($i == null) return res(null); + const formData = new FormData(); + 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); + } + + window.fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: formData, + }) + .then(response => response.json()) + .then(f => { + res(f); + }); + }, 'image/png'); + }); +} + +async function share() { + const uploading = getGameImageDriveFile(); + os.promiseDialog(uploading); + const file = await uploading; + if (!file) return; + os.post({ + initialText: `#BubbleGame +MODE: ${gameMode.value} +SCORE: ${score.value}`, + initialFiles: [file], + }); +} + useInterval(() => { if (!canvasEl.value) return; const actualCanvasWidth = canvasEl.value.getBoundingClientRect().width; From c6a4caa8be576f9ac457bbb218eccb91455148aa Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 7 Jan 2024 14:32:57 +0900 Subject: [PATCH 81/99] refactor --- .../frontend/src/pages/drop-and-fusion.vue | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 71d3f06192..a3be442d21 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="$style.transition_stock_move" >
- +
@@ -65,10 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="$style.transition_picked_move" mode="out-in" > - +
@@ -369,8 +369,8 @@ const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高い let viewScaleX = 1; let viewScaleY = 1; -const currentPick = shallowRef<{ id: string; fruit: Mono } | null>(null); -const stock = shallowRef<{ id: string; fruit: Mono }[]>([]); +const currentPick = shallowRef<{ id: string; mono: Mono } | null>(null); +const stock = shallowRef<{ id: string; mono: Mono }[]>([]); const score = ref(0); const combo = ref(0); const comboPrev = ref(0); @@ -383,7 +383,7 @@ const highScore = ref(null); class Game extends EventEmitter<{ changeScore: (score: number) => void; changeCombo: (combo: number) => void; - changeStock: (stock: { id: string; fruit: Mono }[]) => void; + changeStock: (stock: { id: string; mono: Mono }[]) => void; dropped: () => void; fusioned: (x: number, y: number, score: number) => void; gameOver: () => void; @@ -409,7 +409,7 @@ class Game extends EventEmitter<{ private latestDroppedAt = 0; private latestFusionedAt = 0; - private stock: { id: string; fruit: Mono }[] = []; + private stock: { id: string; mono: Mono }[] = []; private _combo = 0; private get combo() { @@ -509,11 +509,11 @@ class Game extends EventEmitter<{ }); } - private createBody(fruit: Mono, x: number, y: number) { + private createBody(mono: Mono, x: number, y: number) { const options: Matter.IBodyDefinition = { - label: fruit.id, + label: mono.id, //density: 0.0005, - density: fruit.size / 1000, + density: mono.size / 1000, restitution: 0.2, frictionAir: 0.01, friction: 0.7, @@ -522,16 +522,16 @@ class Game extends EventEmitter<{ //mass: 0, render: { sprite: { - texture: fruit.img, - xScale: (fruit.size / fruit.imgSize) * fruit.spriteScale, - yScale: (fruit.size / fruit.imgSize) * fruit.spriteScale, + texture: mono.img, + xScale: (mono.size / mono.imgSize) * mono.spriteScale, + yScale: (mono.size / mono.imgSize) * mono.spriteScale, }, }, }; - if (fruit.shape === 'circle') { - return Matter.Bodies.circle(x, y, fruit.size / 2, options); - } else if (fruit.shape === 'rectangle') { - return Matter.Bodies.rectangle(x, y, fruit.size, fruit.size, options); + if (mono.shape === 'circle') { + return Matter.Bodies.circle(x, y, mono.size / 2, options); + } else if (mono.shape === 'rectangle') { + return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options); } else { throw new Error('unrecognized shape'); } @@ -553,11 +553,11 @@ class Game extends EventEmitter<{ Matter.Composite.remove(this.engine.world, [bodyA, bodyB]); this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id); - const currentFruit = this.monoDefinitions.find(y => y.id === bodyA.label)!; - const nextFruit = this.monoDefinitions.find(x => x.level === currentFruit.level + 1); + const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!; + const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1); - if (nextFruit) { - const body = this.createBody(nextFruit, newX, newY); + if (nextMono) { + const body = this.createBody(nextMono, newX, newY); Matter.Composite.add(this.engine.world, body); // 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする @@ -566,11 +566,11 @@ class Game extends EventEmitter<{ }, 100); const comboBonus = 1 + ((this.combo - 1) / 5); - const additionalScore = Math.round(currentFruit.score * comboBonus); + const additionalScore = Math.round(currentMono.score * comboBonus); this.score += additionalScore; const pan = ((newX / GAME_WIDTH) - 0.5) * 2; - sound.playRaw('syuilo/bubble2', 1, pan, nextFruit.sfxPitch); + sound.playRaw('syuilo/bubble2', 1, pan, nextMono.sfxPitch); this.emit('fusioned', newX, newY, additionalScore); } else { @@ -597,7 +597,7 @@ class Game extends EventEmitter<{ for (let i = 0; i < this.STOCK_MAX; i++) { this.stock.push({ id: Math.random().toString(), - fruit: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); } this.emit('changeStock', this.stock); @@ -658,12 +658,12 @@ class Game extends EventEmitter<{ const st = this.stock.shift()!; this.stock.push({ id: Math.random().toString(), - fruit: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); this.emit('changeStock', this.stock); - const x = Math.min(GAME_WIDTH - this.PLAYAREA_MARGIN - (st.fruit.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.fruit.size / 2), _x)); - const body = this.createBody(st.fruit, x, 50 + st.fruit.size / 2); + const x = Math.min(GAME_WIDTH - this.PLAYAREA_MARGIN - (st.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.mono.size / 2), _x)); + const body = this.createBody(st.mono, x, 50 + st.mono.size / 2); Matter.Composite.add(this.engine.world, body); this.activeBodyIds.push(body.id); this.latestDroppedBodyId = body.id; @@ -970,7 +970,7 @@ definePageMetadata({ user-select: none; } -.currentFruit { +.currentMono { position: absolute; margin-top: 80px; z-index: 2; @@ -991,11 +991,11 @@ definePageMetadata({ user-select: none; } -.currentFruitArrow { +.currentMonoArrow { position: absolute; margin-top: 100px; z-index: 3; - animation: currentFruitArrow 2s ease infinite; + animation: currentMonoArrow 2s ease infinite; pointer-events: none; user-select: none; } @@ -1030,7 +1030,7 @@ definePageMetadata({ } } -@keyframes currentFruitArrow { +@keyframes currentMonoArrow { 0% { transform: translateY(0); } 25% { transform: translateY(-8px); } 50% { transform: translateY(0); } From 5e71418d5caca1cea333ee1b8629987cc69c4fbc Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 7 Jan 2024 08:02:53 +0100 Subject: [PATCH 82/99] fix(frontend/emoji) restore U+FE0F for simple emojis (#12866) * fix(frontend/emoji) restore U+FE0F for simple emojis * Update CHANGELOG.md --------- Co-authored-by: syuilo --- CHANGELOG.md | 1 + .../src/components/global/MkEmoji.vue | 10 ++--- packages/frontend/src/scripts/emojilist.ts | 7 +++- packages/frontend/test/emoji.test.ts | 41 +++++++++++++++++++ packages/frontend/test/init.ts | 24 ++++++----- 5 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 packages/frontend/test/emoji.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2fb4ccd5..474fcad674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Client - Feat: 新しいゲームを追加 - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように +- Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Enhance: チャンネルノートのピン留めをノートのメニューからできるよ diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index 76ca8688d1..f6b21343b6 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -5,15 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only