Merge remote-tracking branch 'msky/develop' into refactor-mkselect

This commit is contained in:
kakkokari-gtyih 2025-08-29 11:45:50 +09:00
commit 7bc91955d3
211 changed files with 1234 additions and 984 deletions

View File

@ -12,10 +12,13 @@
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。 - 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。 - 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
- データベースサイズへの効果が見られない場合はautovacuumが有効になっているか確認してください - データベースサイズへの効果が見られない場合はautovacuumが有効になっているか確認してください
- ハイパーリンクによる参照は検知できないためリンク切れとなります。
- 現時点では、2023-10-01以前にクリップされたリモートのートは検知しないため削除対象となります。
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました - サーバーの初期設定が完了するまでは連合がオンにならないようになりました
- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました - 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました
- 実際の動作に即した名称になり、馴染みのない人でも理解しやすくなりました - 実際の動作に即した名称になり、馴染みのない人でも理解しやすくなりました
- 他サービスにおける「ダイレクトメッセージ」に相当するMisskeyの機能は「チャット」ですが、「ダイレクト投稿」という名称の機能が存在するとそちらがダイレクトメッセージ機能であるような誤解を生んでいました - 他サービスにおける「ダイレクトメッセージ」に相当するMisskeyの機能は「チャット」ですが(過去のバージョンのMisskeyでも、当該機能は「チャット」ではなく「ダイレクトメッセージ」でした)、「ダイレクト投稿」という名称の機能が存在するとそちらがダイレクトメッセージ機能であるような誤解を生んでいました
- 今後、「チャット」の名称を「ダイレクトメッセージ」に戻す可能性があります
- mfm.jsをアップデートしました - mfm.jsをアップデートしました
- Enhance: Unicode 15.1 および 16.0 に収録されている絵文字に対応 - Enhance: Unicode 15.1 および 16.0 に収録されている絵文字に対応
- Enhance: acctに `.` が入っているユーザーのメンションに対応 - Enhance: acctに `.` が入っているユーザーのメンションに対応
@ -27,6 +30,7 @@
- プラグインは1.xに対応したものが必要です - プラグインは1.xに対応したものが必要です
- Playはそのまま動作しますが、新規に作られるプリセットは1.xになります - Playはそのまま動作しますが、新規に作られるプリセットは1.xになります
- 以前のバージョンから無効化されていた note_view_interruptor が有効になりました - 以前のバージョンから無効化されていた note_view_interruptor が有効になりました
- ハンドラは同期的である必要があります
- Feat: セーフモード - Feat: セーフモード
- プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます - プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます
- 以下の方法でセーフモードを起動できます - 以下の方法でセーフモードを起動できます
@ -54,6 +58,10 @@
- Fix: 複数のメンションを1行に記述した場合に、サジェストが正しく表示されない問題を修正 - Fix: 複数のメンションを1行に記述した場合に、サジェストが正しく表示されない問題を修正
- Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正 - Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正
- Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正 - Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正
- Fix: 照会ダイアログでap/showでローカルユーザーを解決した際@username@nullに飛ばされる問題を修正
- Fix: アイコンのデコレーションを付ける際にデコレーションが表示されなくなる問題を修正
- Fix: 管理中アカウント一覧で正しい表示が行われない問題を修正
- Fix: lookupページでリモートURLを指定した際に正しく動作しない問題を修正
### Server ### Server
- Feat: サーバー管理コマンド - Feat: サーバー管理コマンド

View File

@ -4,8 +4,8 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js'; import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js';
import { commonHandlers } from '../packages/frontend/.storybook/mocks.js'; import { commonHandlers } from '../packages/frontend/.storybook/mocks.js';

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Algunes configuracions actuals seran restablertes." restartServerSetupWizardConfirm_text: "Algunes configuracions actuals seran restablertes."
entrancePageStyle: "Estil de la pàgina d'inici" entrancePageStyle: "Estil de la pàgina d'inici"
showTimelineForVisitor: "Mostrar la línia de temps" showTimelineForVisitor: "Mostrar la línia de temps"
showActivityiesForVisitor: "Mostrar les activitats" showActivitiesForVisitor: "Mostrar activitat"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "Tot obert al públic " all: "Tot obert al públic "
localOnly: "Només es publiquen els continguts locals, el contingut remot es manté privat" localOnly: "Només es publiquen els continguts locals, el contingut remot es manté privat"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Les configuracions avançades, com especificar els servidors amb els quals es pot federar, es poden fer més tard." youCanConfigureMoreFederationSettingsLater: "Les configuracions avançades, com especificar els servidors amb els quals es pot federar, es poden fer més tard."
remoteContentsCleaning: "Neteja automàtica del contingut rebut" remoteContentsCleaning: "Neteja automàtica del contingut rebut"
remoteContentsCleaning_description: "Quan es comença a federar es rep un munt de contingut, quan s'activa la neteja automàtica el contingut antic que no es consulta serà eliminat del servidor, el que permet estalviar espai d'emmagatzematge." remoteContentsCleaning_description: "Quan es comença a federar es rep un munt de contingut, quan s'activa la neteja automàtica el contingut antic que no es consulta serà eliminat del servidor, el que permet estalviar espai d'emmagatzematge."
remoteContentsCleaning_description2: "Alguns mètodes de referència, com els enllaços, no poden ser detectats pel sistema."
adminInfo: "Informació de l'administrador " adminInfo: "Informació de l'administrador "
adminInfo_description: "Estableix la informació de l'administrador que es farà servir per rebre consultes." adminInfo_description: "Estableix la informació de l'administrador que es farà servir per rebre consultes."
adminInfo_mustBeFilled: "Aquesta informació ha de ser omplerta si el servidor té els registres oberts o la federació es troba activada." adminInfo_mustBeFilled: "Aquesta informació ha de ser omplerta si el servidor té els registres oberts o la federació es troba activada."

View File

@ -1218,8 +1218,8 @@ showRepliesToOthersInTimeline: "Show replies to others in timeline"
hideRepliesToOthersInTimeline: "Hide replies to others from timeline" hideRepliesToOthersInTimeline: "Hide replies to others from timeline"
showRepliesToOthersInTimelineAll: "Show replies to others from everyone you follow in timeline" showRepliesToOthersInTimelineAll: "Show replies to others from everyone you follow in timeline"
hideRepliesToOthersInTimelineAll: "Hide replies to others from everyone you follow in timeline" hideRepliesToOthersInTimelineAll: "Hide replies to others from everyone you follow in timeline"
confirmShowRepliesAll: "This operation is irreversible. Would you really like to show replies to others from everyone you follow in your timeline?" confirmShowRepliesAll: "Are you sure you want to show replies from everyone you follow in your timeline? This action is irreversible."
confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?" confirmHideRepliesAll: "Are you sure you want to hide replies from everyone you follow in your timeline? This action is irreversible."
externalServices: "External Services" externalServices: "External Services"
sourceCode: "Source code" sourceCode: "Source code"
sourceCodeIsNotYetProvided: "Source code is not yet available. Contact the administrator to fix this problem." sourceCodeIsNotYetProvided: "Source code is not yet available. Contact the administrator to fix this problem."
@ -1668,7 +1668,6 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Some current settings will be reset." restartServerSetupWizardConfirm_text: "Some current settings will be reset."
entrancePageStyle: "Entrance page style" entrancePageStyle: "Entrance page style"
showTimelineForVisitor: "Show timeline" showTimelineForVisitor: "Show timeline"
showActivityiesForVisitor: "Show activities"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "Everything is public" all: "Everything is public"
localOnly: "Only local content is published, remote content is kept private" localOnly: "Only local content is published, remote content is kept private"

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Algunas configuraciones actuales se restablecerán" restartServerSetupWizardConfirm_text: "Algunas configuraciones actuales se restablecerán"
entrancePageStyle: "Estilo de la página de inicio" entrancePageStyle: "Estilo de la página de inicio"
showTimelineForVisitor: "Mostrar la línea de tiempo" showTimelineForVisitor: "Mostrar la línea de tiempo"
showActivityiesForVisitor: "Mostrar actividades" showActivitiesForVisitor: "Mostrar actividades"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "Todo es público." all: "Todo es público."
localOnly: "Sólo se publica el contenido local, el remoto se mantiene privado" localOnly: "Sólo se publica el contenido local, el remoto se mantiene privado"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Los ajustes avanzados, como la especificación de servidores federados, pueden configurarse más adelante." youCanConfigureMoreFederationSettingsLater: "Los ajustes avanzados, como la especificación de servidores federados, pueden configurarse más adelante."
remoteContentsCleaning: "Limpieza automática de los contenidos recibidos" remoteContentsCleaning: "Limpieza automática de los contenidos recibidos"
remoteContentsCleaning_description: "La federación puede dar lugar a un flujo continuo de contenido. Al habilitar la limpieza automática, se eliminará del servidor el contenido obsoleto y sin referencias para ahorrar espacio de almacenamiento." remoteContentsCleaning_description: "La federación puede dar lugar a un flujo continuo de contenido. Al habilitar la limpieza automática, se eliminará del servidor el contenido obsoleto y sin referencias para ahorrar espacio de almacenamiento."
remoteContentsCleaning_description2: "Ciertos métodos de referencia, como los hipervínculos, no pueden ser detectados por el sistema."
adminInfo: "Información del administrador" adminInfo: "Información del administrador"
adminInfo_description: "Establece la información del administrador para recibir consultas." adminInfo_description: "Establece la información del administrador para recibir consultas."
adminInfo_mustBeFilled: "Esta información debe ser introducida en el caso de registros abiertos o la federación esté activada." adminInfo_mustBeFilled: "Esta información debe ser introducida en el caso de registros abiertos o la federación esté activada."

8
locales/index.d.ts vendored
View File

@ -12032,13 +12032,17 @@ export interface Locale extends ILocale {
*/ */
"youCanConfigureMoreFederationSettingsLater": string; "youCanConfigureMoreFederationSettingsLater": string;
/** /**
* *
*/ */
"remoteContentsCleaning": string; "remoteContentsCleaning": string;
/** /**
* *
*/ */
"remoteContentsCleaning_description": string; "remoteContentsCleaning_description": string;
/**
*
*/
"remoteContentsCleaning_description2": string;
/** /**
* *
*/ */

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Verranno ripristinate alcune tue impostazioni personalizzate." restartServerSetupWizardConfirm_text: "Verranno ripristinate alcune tue impostazioni personalizzate."
entrancePageStyle: "Stile della pagina di ingresso" entrancePageStyle: "Stile della pagina di ingresso"
showTimelineForVisitor: "Mostra la Timeline a visitatori non autenticati" showTimelineForVisitor: "Mostra la Timeline a visitatori non autenticati"
showActivityiesForVisitor: "Mostra le attività a visitatori non autenticati" showActivitiesForVisitor: "Mostrare la propria attività"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "Tutto pubblico" all: "Tutto pubblico"
localOnly: "Pubblica solo contenuti locali, mantieni privati i contenuti remoti" localOnly: "Pubblica solo contenuti locali, mantieni privati i contenuti remoti"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi." youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi."
remoteContentsCleaning: "Pulizia automatica dei contenuti in arrivo" remoteContentsCleaning: "Pulizia automatica dei contenuti in arrivo"
remoteContentsCleaning_description: "Con la federazione funzionante, riceverai sempre più contenuti. Abilitando la pulizia automatica, i contenuti non referenziati e obsoleti verranno rimossi automaticamente dai tuoi server, risparmiando spazio di archiviazione." remoteContentsCleaning_description: "Con la federazione funzionante, riceverai sempre più contenuti. Abilitando la pulizia automatica, i contenuti non referenziati e obsoleti verranno rimossi automaticamente dai tuoi server, risparmiando spazio di archiviazione."
remoteContentsCleaning_description2: "Alcuni metodi di riferimento, come i collegamenti ipertestuali, non possono essere rilevati sul sistema."
adminInfo: "Informazioni sull'amministratore" adminInfo: "Informazioni sull'amministratore"
adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste." adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste."
adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione." adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione."

View File

@ -3216,8 +3216,9 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。" doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。"
doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。" doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。"
youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。" youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。"
remoteContentsCleaning: "受信コンテンツの自動クリーニング" remoteContentsCleaning: "リモートコンテンツの自動クリーニング"
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。" remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
remoteContentsCleaning_description2: "ローカル内リモートコンテンツへのハイパーリンクはリンク切れとなります。"
adminInfo: "管理者情報" adminInfo: "管理者情報"
adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。" adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。"
adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。" adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。"

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "현재 일부 설정은 리셋됩니다." restartServerSetupWizardConfirm_text: "현재 일부 설정은 리셋됩니다."
entrancePageStyle: "입구 페이지의 스타일" entrancePageStyle: "입구 페이지의 스타일"
showTimelineForVisitor: "타임라인 표시" showTimelineForVisitor: "타임라인 표시"
showActivityiesForVisitor: "활동 표시" showActivitiesForVisitor: "액티비티 표시하기"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "모두 공개" all: "모두 공개"
localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개" localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다." youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다."
remoteContentsCleaning: "리모트 콘텐츠 자동 정리" remoteContentsCleaning: "리모트 콘텐츠 자동 정리"
remoteContentsCleaning_description: "연합 중인 서버가 있는 경우, 리모트 서버에서 대단히 많은 콘텐츠를 받아오게 됩니다. 자동 정리 기능을 활성화하면, 오래되고 서버에서 더 이상 조회되지 않는 콘텐츠를 자동으로 서버에서 삭제하여, 스토리지를 절약할 수 있습니다." remoteContentsCleaning_description: "연합 중인 서버가 있는 경우, 리모트 서버에서 대단히 많은 콘텐츠를 받아오게 됩니다. 자동 정리 기능을 활성화하면, 오래되고 서버에서 더 이상 조회되지 않는 콘텐츠를 자동으로 서버에서 삭제하여, 스토리지를 절약할 수 있습니다."
remoteContentsCleaning_description2: "로컬 내 원격 콘텐츠로의 하이퍼링크는 깨진 링크로 됩니다."
adminInfo: "관리자 정보" adminInfo: "관리자 정보"
adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다." adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다."
adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다." adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다."

View File

@ -1668,7 +1668,6 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır." restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır."
entrancePageStyle: "Giriş sayfası stili" entrancePageStyle: "Giriş sayfası stili"
showTimelineForVisitor: "Panoyu göster" showTimelineForVisitor: "Panoyu göster"
showActivityiesForVisitor: "Aktiviteleri göster"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "Her şey halka açıktır." all: "Her şey halka açıktır."
localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur." localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur."

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "现有的部分设定将重置。" restartServerSetupWizardConfirm_text: "现有的部分设定将重置。"
entrancePageStyle: "入口页面样式" entrancePageStyle: "入口页面样式"
showTimelineForVisitor: "显示时间线" showTimelineForVisitor: "显示时间线"
showActivityiesForVisitor: "显示活动" showActivitiesForVisitor: "显示活动"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "全部公开" all: "全部公开"
localOnly: "仅公开本地内容,隐藏远程内容" localOnly: "仅公开本地内容,隐藏远程内容"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。" youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。"
remoteContentsCleaning: "自动清理传入内容" remoteContentsCleaning: "自动清理传入内容"
remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。" remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
remoteContentsCleaning_description2: "如超链接之类的某些引用方法无法被系统检测到。"
adminInfo: "管理员信息" adminInfo: "管理员信息"
adminInfo_description: "设置用于接受询问的管理员信息。" adminInfo_description: "设置用于接受询问的管理员信息。"
adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。" adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。"

View File

@ -1668,7 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "當前的部分設定將會被重設。" restartServerSetupWizardConfirm_text: "當前的部分設定將會被重設。"
entrancePageStyle: "入口頁面的樣式" entrancePageStyle: "入口頁面的樣式"
showTimelineForVisitor: "顯示時間軸" showTimelineForVisitor: "顯示時間軸"
showActivityiesForVisitor: "顯示活動" showActivitiesForVisitor: "顯示活動"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "全部公開\n" all: "全部公開\n"
localOnly: "僅公開本地內容,遠端內容則不公開\n" localOnly: "僅公開本地內容,遠端內容則不公開\n"
@ -3120,6 +3120,7 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "您可以在稍後進行更高級的設定,例如指定可以聯繫的伺服器等。\n" youCanConfigureMoreFederationSettingsLater: "您可以在稍後進行更高級的設定,例如指定可以聯繫的伺服器等。\n"
remoteContentsCleaning: "自動清理接收的內容" remoteContentsCleaning: "自動清理接收的內容"
remoteContentsCleaning_description: "進行聯邦後,會持續接收大量內容。啟用自動清理功能後,系統會自動從伺服器中刪除未被參照的過時內容,以節省儲存空間。" remoteContentsCleaning_description: "進行聯邦後,會持續接收大量內容。啟用自動清理功能後,系統會自動從伺服器中刪除未被參照的過時內容,以節省儲存空間。"
remoteContentsCleaning_description2: "有些引用方式系統上無法檢測到,例如超連結。"
adminInfo: "管理員資訊" adminInfo: "管理員資訊"
adminInfo_description: "設定用於接收查詢的管理者資訊。\n" adminInfo_description: "設定用於接收查詢的管理者資訊。\n"
adminInfo_mustBeFilled: "當設置為開放伺服器或啟用聯邦時,必須填寫此資訊。\n" adminInfo_mustBeFilled: "當設置為開放伺服器或啟用聯邦時,必須填寫此資訊。\n"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.8.0-beta.4", "version": "2025.8.0-beta.5",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -244,7 +244,6 @@ export class WebhookTestService {
case 'reaction': case 'reaction':
return; return;
default: { default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type; const _exhaustiveAssertion: never = params.type;
return; return;
} }
@ -327,7 +326,6 @@ export class WebhookTestService {
break; break;
} }
default: { default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type; const _exhaustiveAssertion: never = params.type;
return; return;
} }
@ -412,7 +410,7 @@ export class WebhookTestService {
name: user.name, name: user.name,
username: user.username, username: user.username,
host: user.host, host: user.host,
avatarUrl: user.avatarId == null ? null : user.avatarUrl, avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? '',
avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash, avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.map(it => ({ avatarDecorations: user.avatarDecorations.map(it => ({
id: it.id, id: it.id,

View File

@ -49,15 +49,12 @@ export class NoteReactionEntityService implements OnModuleInit {
public async pack( public async pack(
src: MiNoteReaction['id'] | MiNoteReaction, src: MiNoteReaction['id'] | MiNoteReaction,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
options?: { options?: object,
withNote: boolean;
},
hints?: { hints?: {
packedUser?: Packed<'UserLite'> packedUser?: Packed<'UserLite'>
}, },
): Promise<Packed<'NoteReaction'>> { ): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({ const opts = Object.assign({
withNote: false,
}, options); }, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src }); const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
@ -67,9 +64,6 @@ export class NoteReactionEntityService implements OnModuleInit {
createdAt: this.idService.parse(reaction.id).date.toISOString(), createdAt: this.idService.parse(reaction.id).date.toISOString(),
user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me), user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction), type: this.reactionService.convertLegacyReaction(reaction.reaction),
...(opts.withNote ? {
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
} : {}),
}; };
} }
@ -77,16 +71,50 @@ export class NoteReactionEntityService implements OnModuleInit {
public async packMany( public async packMany(
reactions: MiNoteReaction[], reactions: MiNoteReaction[],
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
options?: { options?: object,
withNote: boolean;
},
): Promise<Packed<'NoteReaction'>[]> { ): Promise<Packed<'NoteReaction'>[]> {
const opts = Object.assign({ const opts = Object.assign({
withNote: false,
}, options); }, options);
const _users = reactions.map(({ user, userId }) => user ?? userId); const _users = reactions.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me) const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u]))); .then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) }))); return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
} }
@bindThis
public async packWithNote(
src: MiNoteReaction['id'] | MiNoteReaction,
me?: { id: MiUser['id'] } | null | undefined,
options?: object,
hints?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'NoteReactionWithNote'>> {
const opts = Object.assign({
}, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
return {
id: reaction.id,
createdAt: this.idService.parse(reaction.id).date.toISOString(),
user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction),
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
};
}
@bindThis
public async packManyWithNote(
reactions: MiNoteReaction[],
me?: { id: MiUser['id'] } | null | undefined,
options?: object,
): Promise<Packed<'NoteReactionWithNote'>[]> {
const opts = Object.assign({
}, options);
const _users = reactions.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reactions.map(reaction => this.packWithNote(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
}
} }

View File

@ -471,8 +471,8 @@ export class UserEntityService implements OnModuleInit {
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null; null;
const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null; const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : undefined;
const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null; const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : undefined;
const unreadAnnouncements = isMe && isDetailed ? const unreadAnnouncements = isMe && isDetailed ?
(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({ (await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
createdAt: this.idService.parse(announcement.id).date.toISOString(), createdAt: this.idService.parse(announcement.id).date.toISOString(),
@ -481,6 +481,7 @@ export class UserEntityService implements OnModuleInit {
const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null; const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
// TODO: 例えば avatarUrl: true など間違った型を設定しても型エラーにならないのをどうにかする(ジェネリクス使わない方法で実装するしかなさそう?)
const packed = { const packed = {
id: user.id, id: user.id,
name: user.name, name: user.name,

View File

@ -22,7 +22,7 @@ import { packedFollowingSchema } from '@/models/json-schema/following.js';
import { packedMutingSchema } from '@/models/json-schema/muting.js'; import { packedMutingSchema } from '@/models/json-schema/muting.js';
import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'; import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js';
import { packedBlockingSchema } from '@/models/json-schema/blocking.js'; import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; import { packedNoteReactionSchema, packedNoteReactionWithNoteSchema } from '@/models/json-schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js'; import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js'; import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
@ -65,6 +65,7 @@ import {
packedMetaDetailedSchema, packedMetaDetailedSchema,
packedMetaLiteSchema, packedMetaLiteSchema,
} from '@/models/json-schema/meta.js'; } from '@/models/json-schema/meta.js';
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js'; import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js';
@ -92,6 +93,7 @@ export const refs = {
Note: packedNoteSchema, Note: packedNoteSchema,
NoteDraft: packedNoteDraftSchema, NoteDraft: packedNoteDraftSchema,
NoteReaction: packedNoteReactionSchema, NoteReaction: packedNoteReactionSchema,
NoteReactionWithNote: packedNoteReactionWithNoteSchema,
NoteFavorite: packedNoteFavoriteSchema, NoteFavorite: packedNoteFavoriteSchema,
Notification: packedNotificationSchema, Notification: packedNotificationSchema,
DriveFile: packedDriveFileSchema, DriveFile: packedDriveFileSchema,
@ -133,6 +135,7 @@ export const refs = {
MetaLite: packedMetaLiteSchema, MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema, MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema, MetaDetailed: packedMetaDetailedSchema,
UserWebhook: packedUserWebhookSchema,
SystemWebhook: packedSystemWebhookSchema, SystemWebhook: packedSystemWebhookSchema,
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
ChatMessage: packedChatMessageSchema, ChatMessage: packedChatMessageSchema,

View File

@ -10,7 +10,6 @@ export const packedNoteReactionSchema = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
format: 'id', format: 'id',
example: 'xxxxxxxxxx',
}, },
createdAt: { createdAt: {
type: 'string', type: 'string',
@ -28,3 +27,33 @@ export const packedNoteReactionSchema = {
}, },
}, },
} as const; } as const;
export const packedNoteReactionWithNoteSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
user: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
type: {
type: 'string',
optional: false, nullable: false,
},
note: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},
} as const;

View File

@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { webhookEventTypes } from '@/models/Webhook.js';
export const packedUserWebhookSchema = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
optional: false, nullable: false,
},
userId: {
type: 'string',
format: 'id',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: false,
},
on: {
type: 'array',
items: {
type: 'string',
optional: false, nullable: false,
enum: webhookEventTypes,
},
},
url: {
type: 'string',
optional: false, nullable: false,
},
secret: {
type: 'string',
optional: false, nullable: false,
},
active: {
type: 'boolean',
optional: false, nullable: false,
},
latestSentAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: true,
},
latestStatus: {
type: 'integer',
optional: false, nullable: true,
},
},
} as const;

View File

@ -65,7 +65,7 @@ export const packedUserLiteSchema = {
avatarUrl: { avatarUrl: {
type: 'string', type: 'string',
format: 'url', format: 'url',
nullable: true, optional: false, nullable: false, optional: false,
}, },
avatarBlurhash: { avatarBlurhash: {
type: 'string', type: 'string',
@ -465,11 +465,11 @@ export const packedMeDetailedOnlySchema = {
}, },
isModerator: { isModerator: {
type: 'boolean', type: 'boolean',
nullable: true, optional: false, nullable: false, optional: false,
}, },
isAdmin: { isAdmin: {
type: 'boolean', type: 'boolean',
nullable: true, optional: false, nullable: false, optional: false,
}, },
injectFeaturedNote: { injectFeaturedNote: {
type: 'boolean', type: 'boolean',
@ -591,7 +591,7 @@ export const packedMeDetailedOnlySchema = {
}, },
mutedInstances: { mutedInstances: {
type: 'array', type: 'array',
nullable: true, optional: false, nullable: false, optional: false,
items: { items: {
type: 'string', type: 'string',
nullable: false, optional: false, nullable: false, optional: false,

View File

@ -49,6 +49,34 @@ export const meta = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
icon: {
type: 'string',
optional: false, nullable: true,
},
display: {
type: 'string',
optional: false, nullable: false,
},
isActive: {
type: 'boolean',
optional: false, nullable: false,
},
forExistingUsers: {
type: 'boolean',
optional: false, nullable: false,
},
silence: {
type: 'boolean',
optional: false, nullable: false,
},
needConfirmationToRead: {
type: 'boolean',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: true,
},
imageUrl: { imageUrl: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,

View File

@ -157,6 +157,22 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
maybeSensitive: {
type: 'boolean',
optional: false, nullable: false,
},
maybePorn: {
type: 'boolean',
optional: false, nullable: false,
},
requestIp: {
type: 'string',
optional: false, nullable: true,
},
requestHeaders: {
type: 'object',
optional: false, nullable: true,
},
}, },
}, },
} as const; } as const;

View File

@ -223,10 +223,12 @@ export const meta = {
sensitiveMediaDetection: { sensitiveMediaDetection: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
enum: ['none', 'all', 'local', 'remote'],
}, },
sensitiveMediaDetectionSensitivity: { sensitiveMediaDetectionSensitivity: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'],
}, },
setSensitiveFlagAutomatically: { setSensitiveFlagAutomatically: {
type: 'boolean', type: 'boolean',
@ -473,6 +475,10 @@ export const meta = {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
}, },
feedbackUrl: {
type: 'string',
optional: false, nullable: true,
},
summalyProxy: { summalyProxy: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,

View File

@ -73,8 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updatedAt: new Date(), updatedAt: new Date(),
...Object.fromEntries( ...Object.fromEntries(
Object.entries(ps).filter( Object.entries(ps).filter(
([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key) ([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key),
) ),
), ),
}); });
}); });

View File

@ -46,6 +46,14 @@ export const meta = {
type: 'string', type: 'string',
}, },
}, },
iconUrl: {
type: 'string',
optional: true, nullable: true,
},
description: {
type: 'string',
optional: true, nullable: true,
},
}, },
}, },
}, },
@ -88,6 +96,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
createdAt: this.idService.parse(token.id).date.toISOString(), createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(), lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.app ? token.app.permission : token.permission, permission: token.app ? token.app.permission : token.permission,
iconUrl: token.iconUrl,
description: token.description ?? token.app?.description ?? null,
}))); })));
}); });
} }

View File

@ -21,29 +21,7 @@ export const meta = {
type: 'array', type: 'array',
items: { items: {
type: 'object', type: 'object',
properties: { ref: 'UserWebhook',
id: {
type: 'string',
format: 'misskey:id',
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
},
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
}, },
}, },
} as const; } as const;
@ -65,19 +43,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: me.id, userId: me.id,
}); });
return webhooks.map(webhook => ( return webhooks.map(webhook => ({
{ id: webhook.id,
id: webhook.id, userId: webhook.userId,
userId: webhook.userId, name: webhook.name,
name: webhook.name, on: webhook.on,
on: webhook.on, url: webhook.url,
url: webhook.url, secret: webhook.secret,
secret: webhook.secret, active: webhook.active,
active: webhook.active, latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null,
latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, latestStatus: webhook.latestStatus,
latestStatus: webhook.latestStatus, }));
}
));
}); });
} }
} }

View File

@ -28,29 +28,7 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
properties: { ref: 'UserWebhook',
id: {
type: 'string',
format: 'misskey:id',
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
},
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
}, },
} as const; } as const;

View File

@ -23,6 +23,16 @@ export const meta = {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'UserList', ref: 'UserList',
properties: {
likedCount: {
type: 'number',
optional: true, nullable: false,
},
isLiked: {
type: 'boolean',
optional: true, nullable: false,
},
},
}, },
errors: { errors: {

View File

@ -28,7 +28,7 @@ export const meta = {
items: { items: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'NoteReaction', ref: 'NoteReactionWithNote',
}, },
}, },
@ -120,7 +120,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return true; return true;
}); });
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true }); return await this.noteReactionEntityService.packManyWithNote(reactions, me);
}); });
} }
} }

View File

@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
// TODO: (可能な部分を)sharedに抽出して frontend と共通化
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import lightTheme from '@@/themes/_light.json5'; import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5'; import darkTheme from '@@/themes/_dark.json5';

View File

@ -6,7 +6,7 @@
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import type { DefaultBodyType, HttpResponseResolver, JsonBodyType, PathParams } from 'msw'; import type { DefaultBodyType, HttpResponseResolver, JsonBodyType, PathParams } from 'msw';
import seedrandom from 'seedrandom'; import seedrandom from 'seedrandom';
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
function getChartArray(seed: string, limit: number, option?: { accumulate?: boolean, mul?: number }): number[] { function getChartArray(seed: string, limit: number, option?: { accumulate?: boolean, mul?: number }): number[] {
const rng = seedrandom(seed); const rng = seedrandom(seed);

View File

@ -42,7 +42,7 @@
"prefix": "storyimplevent", "prefix": "storyimplevent",
"body": [ "body": [
"/* eslint-disable @typescript-eslint/explicit-function-return-type */", "/* eslint-disable @typescript-eslint/explicit-function-return-type */",
"import { action } from '@storybook/addon-actions';", "import { action } from 'storybook/actions';",
"import { StoryObj } from '@storybook/vue3';", "import { StoryObj } from '@storybook/vue3';",
"import $1 from './$1.vue';", "import $1 from './$1.vue';",
"export const Default = {", "export const Default = {",

View File

@ -8,7 +8,7 @@
import { parse as vueSfcParse } from 'vue/compiler-sfc'; import { parse as vueSfcParse } from 'vue/compiler-sfc';
import { import {
createLogger, createLogger,
EnvironmentModuleGraph, type EnvironmentModuleGraph,
type LogErrorOptions, type LogErrorOptions,
type LogOptions, type LogOptions,
normalizePath, normalizePath,

View File

@ -83,7 +83,6 @@
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/summaly": "5.2.3", "@misskey-dev/summaly": "5.2.3",
"@storybook/addon-actions": "9.0.8",
"@storybook/addon-essentials": "8.6.14", "@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14", "@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "9.1.3", "@storybook/addon-links": "9.1.3",

View File

@ -23,7 +23,7 @@ export async function getAccounts(): Promise<{
host: string; host: string;
id: Misskey.entities.User['id']; id: Misskey.entities.User['id'];
username: Misskey.entities.User['username']; username: Misskey.entities.User['username'];
user?: Misskey.entities.User | null; user?: Misskey.entities.MeDetailed | null;
token: string | null; token: string | null;
}[]> { }[]> {
const tokens = store.s.accountTokens; const tokens = store.s.accountTokens;
@ -38,7 +38,7 @@ export async function getAccounts(): Promise<{
})); }));
} }
async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) { async function addAccount(host: string, user: Misskey.entities.MeDetailed, token: AccountWithToken['token']) {
if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) { if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) {
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token }); store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
store.set('accountInfos', { ...store.s.accountInfos, [host + '/' + user.id]: user }); store.set('accountInfos', { ...store.s.accountInfos, [host + '/' + user.id]: user });
@ -149,9 +149,10 @@ export function updateCurrentAccountPartial(accountData: Partial<Misskey.entitie
export async function refreshCurrentAccount() { export async function refreshCurrentAccount() {
if (!$i) return; if (!$i) return;
const me = $i;
return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => { return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => {
if (reason === isAccountDeleted) { if (reason === isAccountDeleted) {
removeAccount(host, $i.id); removeAccount(host, me.id);
if (Object.keys(store.s.accountTokens).length > 0) { if (Object.keys(store.s.accountTokens).length > 0) {
login(Object.values(store.s.accountTokens)[0]); login(Object.values(store.s.accountTokens)[0]);
} else { } else {
@ -214,19 +215,37 @@ export async function openAccountMenu(opts: {
includeCurrentAccount?: boolean; includeCurrentAccount?: boolean;
withExtraOperation: boolean; withExtraOperation: boolean;
active?: Misskey.entities.User['id']; active?: Misskey.entities.User['id'];
onChoose?: (account: Misskey.entities.User) => void; onChoose?: (account: Misskey.entities.MeDetailed) => void;
}, ev: MouseEvent) { }, ev: MouseEvent) {
if (!$i) return; if (!$i) return;
const me = $i;
function createItem(host: string, id: Misskey.entities.User['id'], username: Misskey.entities.User['username'], account: Misskey.entities.User | null | undefined, token: string): MenuItem { const callback = opts.onChoose;
function createItem(host: string, id: Misskey.entities.User['id'], username: Misskey.entities.User['username'], account: Misskey.entities.MeDetailed | null | undefined, token: string | null): MenuItem {
if (account) { if (account) {
return { return {
type: 'user' as const, type: 'user' as const,
user: account, user: account,
active: opts.active != null ? opts.active === id : false, active: opts.active != null ? opts.active === id : false,
action: async () => { action: async () => {
if (opts.onChoose) { if (callback) {
opts.onChoose(account); callback(account);
} else {
switchAccount(host, id);
}
},
};
} else if (token != null) {
return {
type: 'button' as const,
text: username,
active: opts.active != null ? opts.active === id : false,
action: async () => {
if (callback) {
fetchAccount(token, id).then(account => {
callback(account);
});
} else { } else {
switchAccount(host, id); switchAccount(host, id);
} }
@ -238,13 +257,7 @@ export async function openAccountMenu(opts: {
text: username, text: username,
active: opts.active != null ? opts.active === id : false, active: opts.active != null ? opts.active === id : false,
action: async () => { action: async () => {
if (opts.onChoose) { // TODO
fetchAccount(token, id).then(account => {
opts.onChoose(account);
});
} else {
switchAccount(host, id);
}
}, },
}; };
} }
@ -253,7 +266,7 @@ export async function openAccountMenu(opts: {
const menuItems: MenuItem[] = []; const menuItems: MenuItem[] = [];
// TODO: $iのホストも比較したいけど通常null // TODO: $iのホストも比較したいけど通常null
const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id))).map(a => createItem(a.host, a.id, a.username, a.user, a.token)); const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.id !== me.id))).map(a => createItem(a.host, a.id, a.username, a.user, a.token));
if (opts.withExtraOperation) { if (opts.withExtraOperation) {
menuItems.push({ menuItems.push({

View File

@ -4,11 +4,11 @@
*/ */
import { utils, values } from '@syuilo/aiscript'; import { utils, values } from '@syuilo/aiscript';
import { genId } from '@/utility/id.js';
import { ref } from 'vue'; import { ref } from 'vue';
import type { Ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { assertStringAndIsIn } from './common.js'; import { assertStringAndIsIn } from './common.js';
import type { Ref } from 'vue';
import { genId } from '@/utility/id.js';
const ALIGNS = ['left', 'center', 'right'] as const; const ALIGNS = ['left', 'center', 'right'] as const;
const FONTS = ['serif', 'sans-serif', 'monospace'] as const; const FONTS = ['serif', 'sans-serif', 'monospace'] as const;
@ -21,16 +21,15 @@ type BorderStyle = (typeof BORDER_STYLES)[number];
export type AsUiComponentBase = { export type AsUiComponentBase = {
id: string; id: string;
hidden?: boolean; hidden?: boolean;
children?: AsUiComponent['id'][];
}; };
export type AsUiRoot = AsUiComponentBase & { export type AsUiRoot = AsUiComponentBase & {
type: 'root'; type: 'root';
children: AsUiComponent['id'][];
}; };
export type AsUiContainer = AsUiComponentBase & { export type AsUiContainer = AsUiComponentBase & {
type: 'container'; type: 'container';
children?: AsUiComponent['id'][];
align?: Align; align?: Align;
bgColor?: string; bgColor?: string;
fgColor?: string; fgColor?: string;
@ -123,7 +122,6 @@ export type AsUiSelect = AsUiComponentBase & {
export type AsUiFolder = AsUiComponentBase & { export type AsUiFolder = AsUiComponentBase & {
type: 'folder'; type: 'folder';
children?: AsUiComponent['id'][];
title?: string; title?: string;
opened?: boolean; opened?: boolean;
}; };

View File

@ -368,11 +368,6 @@ export async function mainBoot() {
}); });
}); });
main.on('unreadAntenna', () => {
updateCurrentAccountPartial({ hasUnreadAntenna: true });
sound.playMisskeySfx('antenna');
});
main.on('newChatMessage', () => { main.on('newChatMessage', () => {
updateCurrentAccountPartial({ hasUnreadChatMessages: true }); updateCurrentAccountPartial({ hasUnreadChatMessages: true });
sound.playMisskeySfx('chatMessage'); sound.playMisskeySfx('chatMessage');

View File

@ -2,14 +2,13 @@
* SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { action } from 'storybook/actions';
import { action } from '@storybook/addon-actions';
import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js'; import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReportWindow from './MkAbuseReportWindow.vue'; import MkAbuseReportWindow from './MkAbuseReportWindow.vue';
import type { StoryObj } from '@storybook/vue3';
export const Default = { export const Default = {
render(args) { render(args) {
return { return {

View File

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum', [$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum',
}]" }]"
> >
<div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }"> <div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg ?? '' }">
<img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img"> <img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div> </div>
</div> </div>
@ -61,8 +61,8 @@ import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utili
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
user: Misskey.entities.User; user: Misskey.entities.User;
withLocked: boolean; withLocked?: boolean;
withDescription: boolean; withDescription?: boolean;
}>(), { }>(), {
withLocked: true, withLocked: true,
withDescription: true, withDescription: true,
@ -71,7 +71,7 @@ const props = withDefaults(defineProps<{
const achievements = ref<Misskey.entities.UsersAchievementsResponse | null>(null); const achievements = ref<Misskey.entities.UsersAchievementsResponse | null>(null);
const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x))); const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x)));
function fetch() { function _fetch_() {
misskeyApi('users/achievements', { userId: props.user.id }).then(res => { misskeyApi('users/achievements', { userId: props.user.id }).then(res => {
achievements.value = []; achievements.value = [];
for (const t of ACHIEVEMENT_TYPES) { for (const t of ACHIEVEMENT_TYPES) {
@ -84,11 +84,11 @@ function fetch() {
function clickHere() { function clickHere() {
claimAchievement('clickedClickHere'); claimAchievement('clickedClickHere');
fetch(); _fetch_();
} }
onMounted(() => { onMounted(() => {
fetch(); _fetch_();
}); });
</script> </script>

View File

@ -265,6 +265,8 @@ onUnmounted(() => {
if (handle) { if (handle) {
window.cancelAnimationFrame(handle); window.cancelAnimationFrame(handle);
} }
// TODO: WebGL
}); });
</script> </script>

View File

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';

View File

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';

View File

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';

View File

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { expect, userEvent, waitFor, within } from '@storybook/test'; import { expect, userEvent, waitFor, within } from '@storybook/test';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';

View File

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */ /* eslint-disable import/no-default-export */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
export const Default = { export const Default = {

View File

@ -2,9 +2,9 @@
* SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { expect, userEvent, within } from '@storybook/test'; import { expect, userEvent, within } from '@storybook/test';
import { channel } from '../../.storybook/fakes.js'; import { channel } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';

View File

@ -3,14 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */
import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { channel } from '../../.storybook/fakes.js'; import { channel } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';
import MkChannelList from './MkChannelList.vue'; import MkChannelList from './MkChannelList.vue';
import type { StoryObj } from '@storybook/vue3';
import { Paginator } from '@/utility/paginator.js';
export const Default = { export const Default = {
render(args) { render(args) {
return { return {
@ -33,10 +32,7 @@ export const Default = {
}; };
}, },
args: { args: {
pagination: { paginator: new Paginator('channels/search', {}),
endpoint: 'channels/search',
limit: 10,
},
}, },
parameters: { parameters: {
chromatic: { chromatic: {

View File

@ -589,7 +589,10 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
}; };
const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => { const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span }); const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'In', name: 'In',
@ -611,7 +614,10 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
}; };
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => { const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span }); const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'Users', name: 'Users',
@ -626,7 +632,10 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
}; };
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => { const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span }); const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'Notes', name: 'Notes',
@ -641,7 +650,10 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
}; };
const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => { const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span }); const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'Following', name: 'Following',
@ -664,7 +676,10 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
}; };
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => { const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span }); const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return { return {
bytes: true, bytes: true,
series: [{ series: [{
@ -680,7 +695,10 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
}; };
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => { const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span }); const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'Drive files', name: 'Drive files',
@ -695,7 +713,10 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
}; };
const fetchPerUserNotesChart = async (): Promise<typeof chartData> => { const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/notes', { userId: props.args?.user?.id, limit: props.limit, span: props.span }); const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/notes', { userId: userId, limit: props.limit, span: props.span });
return { return {
series: [...(props.args?.withoutAll ? [] : [{ series: [...(props.args?.withoutAll ? [] : [{
name: 'All', name: 'All',
@ -727,7 +748,10 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
}; };
const fetchPerUserPvChart = async (): Promise<typeof chartData> => { const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/pv', { userId: props.args?.user?.id, limit: props.limit, span: props.span }); const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/pv', { userId: userId, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'Unique PV (user)', name: 'Unique PV (user)',
@ -754,7 +778,10 @@ const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
}; };
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => { const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span }); const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/following', { userId: userId, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'Local', name: 'Local',
@ -769,7 +796,10 @@ const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
}; };
const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => { const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span }); const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/following', { userId: userId, limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'Local', name: 'Local',
@ -784,7 +814,10 @@ const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
}; };
const fetchPerUserDriveChart = async (): Promise<typeof chartData> => { const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/drive', { userId: props.args?.user?.id, limit: props.limit, span: props.span }); const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/drive', { userId: userId, limit: props.limit, span: props.span });
return { return {
bytes: true, bytes: true,
series: [{ series: [{

View File

@ -4,7 +4,7 @@
*/ */
import { http, HttpResponse } from 'msw'; import { http, HttpResponse } from 'msw';
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { chatMessage } from '../../.storybook/fakes'; import { chatMessage } from '../../.storybook/fakes';
import MkChatHistories from './MkChatHistories.vue'; import MkChatHistories from './MkChatHistories.vue';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';

View File

@ -2,9 +2,9 @@
* SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { expect, userEvent, within } from '@storybook/test'; import { expect, userEvent, within } from '@storybook/test';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';
import MkClickerGame from './MkClickerGame.vue'; import MkClickerGame from './MkClickerGame.vue';

View File

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */ /* eslint-disable import/no-default-export */
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import MkCodeEditor from './MkCodeEditor.vue'; import MkCodeEditor from './MkCodeEditor.vue';
const code = `for (let i, 100) { const code = `for (let i, 100) {
<: if (i % 15 == 0) "FizzBuzz" <: if (i % 15 == 0) "FizzBuzz"

View File

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */ /* eslint-disable import/no-default-export */
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import MkColorInput from './MkColorInput.vue'; import MkColorInput from './MkColorInput.vue';
export const Default = { export const Default = {
render(args) { render(args) {

View File

@ -4,7 +4,7 @@
*/ */
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { file } from '../../.storybook/fakes.js'; import { file } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';
import MkCropperDialog from './MkCropperDialog.vue'; import MkCropperDialog from './MkCropperDialog.vue';

View File

@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, useTemplateRef, ref } from 'vue'; import { onMounted, useTemplateRef, ref, onUnmounted } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs'; import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
@ -55,17 +55,19 @@ const imgEl = useTemplateRef('imgEl');
let cropper: Cropper | null = null; let cropper: Cropper | null = null;
const loading = ref(true); const loading = ref(true);
const ok = async () => { async function ok() {
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => { const promise = new Promise<Blob>(async (res) => {
const croppedImage = await cropper?.getCropperImage(); if (cropper == null) throw new Error('Cropper is not initialized');
const croppedSection = await cropper?.getCropperSelection();
const croppedImage = await cropper.getCropperImage()!;
const croppedSection = await cropper.getCropperSelection()!;
// () // ()
const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth; const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth;
const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate; const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate;
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender }); const croppedCanvas = await croppedSection.$toCanvas({ width: widthToRender });
croppedCanvas?.toBlob(blob => { croppedCanvas.toBlob(blob => {
if (!blob) return; if (!blob) return;
res(blob); res(blob);
}); });
@ -74,25 +76,27 @@ const ok = async () => {
const f = await promise; const f = await promise;
emit('ok', f); emit('ok', f);
dialogEl.value!.close(); if (dialogEl.value != null) dialogEl.value.close();
}; }
const cancel = () => { function cancel() {
emit('cancel'); emit('cancel');
dialogEl.value!.close(); if (dialogEl.value != null) dialogEl.value.close();
}; }
const onImageLoad = () => { function onImageLoad() {
loading.value = false; loading.value = false;
if (cropper) { if (cropper) {
cropper.getCropperImage()!.$center('contain'); cropper.getCropperImage()!.$center('contain');
cropper.getCropperSelection()!.$center(); cropper.getCropperSelection()!.$center();
} }
}; }
onMounted(() => { onMounted(() => {
cropper = new Cropper(imgEl.value!, { if (imgEl.value == null) return; // TS
cropper = new Cropper(imgEl.value, {
}); });
const computedStyle = getComputedStyle(window.document.documentElement); const computedStyle = getComputedStyle(window.document.documentElement);
@ -104,16 +108,22 @@ onMounted(() => {
selection.outlined = true; selection.outlined = true;
window.setTimeout(() => { window.setTimeout(() => {
cropper!.getCropperImage()!.$center('contain'); if (cropper == null) return;
cropper.getCropperImage()!.$center('contain');
selection.$center(); selection.$center();
}, 100); }, 100);
// 調 // 調
window.setTimeout(() => { window.setTimeout(() => {
cropper!.getCropperImage()!.$center('contain'); if (cropper == null) return;
cropper.getCropperImage()!.$center('contain');
selection.$center(); selection.$center();
}, 500); }, 500);
}); });
onUnmounted(() => {
URL.revokeObjectURL(imgUrl);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */ /* eslint-disable import/no-default-export */
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { expect, userEvent, within } from '@storybook/test'; import { expect, userEvent, within } from '@storybook/test';
import { file } from '../../.storybook/fakes.js'; import { file } from '../../.storybook/fakes.js';
import MkCwButton from './MkCwButton.vue'; import MkCwButton from './MkCwButton.vue';

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { expect, userEvent, waitFor, within } from '@storybook/test'; import { expect, userEvent, waitFor, within } from '@storybook/test';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { onBeforeUnmount } from 'vue'; import { onBeforeUnmount } from 'vue';
import MkDonation from './MkDonation.vue'; import MkDonation from './MkDonation.vue';

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import MkDrive_file from './MkDrive.file.vue'; import MkDrive_file from './MkDrive.file.vue';
import { file } from '../../.storybook/fakes.js'; import { file } from '../../.storybook/fakes.js';

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { http, HttpResponse } from 'msw'; import { http, HttpResponse } from 'msw';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { http, HttpResponse } from 'msw'; import { http, HttpResponse } from 'msw';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';

View File

@ -152,11 +152,12 @@ import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { Paginator } from '@/utility/paginator.js'; import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder['id'] | null; initialFolder?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id'] | null;
type?: string; type?: string;
multiple?: boolean; multiple?: boolean;
select?: 'file' | 'folder' | null; select?: 'file' | 'folder' | null;
}>(), { }>(), {
initialFolder: null,
multiple: false, multiple: false,
select: null, select: null,
}); });
@ -293,7 +294,7 @@ function onDragleave() {
draghover.value = false; draghover.value = false;
} }
function onDrop(ev: DragEvent) { function onDrop(ev: DragEvent): void | boolean {
draghover.value = false; draghover.value = false;
if (!ev.dataTransfer) return; if (!ev.dataTransfer) return;

View File

@ -39,13 +39,13 @@ withDefaults(defineProps<{
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'done', r?: Misskey.entities.DriveFolder[]): void; (ev: 'done', r?: (Misskey.entities.DriveFolder | null)[]): void;
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const dialog = useTemplateRef('dialog'); const dialog = useTemplateRef('dialog');
const selected = ref<Misskey.entities.DriveFolder[]>([]); const selected = ref<(Misskey.entities.DriveFolder | null)[]>([]);
function ok() { function ok() {
emit('done', selected.value); emit('done', selected.value);
@ -57,7 +57,7 @@ function cancel() {
dialog.value?.close(); dialog.value?.close();
} }
function onChangeSelection(v: Misskey.entities.DriveFolder[]) { function onChangeSelection(v: (Misskey.entities.DriveFolder | null)[]) {
selected.value = v; selected.value = v;
} }
</script> </script>

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { expect, userEvent, waitFor, within } from '@storybook/test'; import { expect, userEvent, waitFor, within } from '@storybook/test';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View File

@ -152,7 +152,7 @@ const props = withDefaults(defineProps<{
asDrawer?: boolean; asDrawer?: boolean;
asWindow?: boolean; asWindow?: boolean;
asReactionPicker?: boolean; // 使使 asReactionPicker?: boolean; // 使使
targetNote?: Misskey.entities.Note; targetNote?: Misskey.entities.Note | null;
}>(), { }>(), {
showPinned: true, showPinned: true,
}); });

View File

@ -44,11 +44,11 @@ import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
manualShowing?: boolean | null; manualShowing?: boolean | null;
anchorElement?: HTMLElement; anchorElement?: HTMLElement | null;
showPinned?: boolean; showPinned?: boolean;
pinnedEmojis?: string[], pinnedEmojis?: string[],
asReactionPicker?: boolean; asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note; targetNote?: Misskey.entities.Note | null;
choseAndClose?: boolean; choseAndClose?: boolean;
}>(), { }>(), {
manualShowing: null, manualShowing: null,

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm class="summaryMfm" :text="flash.summary" :plain="true" :nowrap="true"/> <Mfm class="summaryMfm" :text="flash.summary" :plain="true" :nowrap="true"/>
</p> </p>
<footer> <footer>
<img v-if="flash.user.avatarUrl != null" class="icon" :src="flash.user.avatarUrl"/> <img class="icon" :src="flash.user.avatarUrl"/>
<p>{{ userName(flash.user) }}</p> <p>{{ userName(flash.user) }}</p>
</footer> </footer>
</article> </article>

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { file } from '../../.storybook/fakes.js'; import { file } from '../../.storybook/fakes.js';
import MkImgPreviewDialog from './MkImgPreviewDialog.vue'; import MkImgPreviewDialog from './MkImgPreviewDialog.vue';
export const Default = { export const Default = {

View File

@ -34,9 +34,10 @@ import { deviceKind } from '@/utility/device-kind.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
anchorElement?: HTMLElement; anchorElement?: HTMLElement | null;
anchor?: { x: string; y: string; }; anchor?: { x: string; y: string; };
}>(), { }>(), {
anchorElement: null,
anchor: () => ({ x: 'right', y: 'center' }), anchor: () => ({ x: 'right', y: 'center' }),
}); });

View File

@ -39,10 +39,12 @@ const el = ref<HTMLElement | { $el: HTMLElement }>();
if (isEnabledUrlPreview.value) { if (isEnabledUrlPreview.value) {
useTooltip(el, (showing) => { useTooltip(el, (showing) => {
const anchorElement = el.value instanceof HTMLElement ? el.value : el.value?.$el;
if (anchorElement == null) return;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing, showing,
url: props.url, url: props.url,
anchorElement: el.value instanceof HTMLElement ? el.value : el.value?.$el, anchorElement: anchorElement,
}, { }, {
closed: () => dispose(), closed: () => dispose(),
}); });

View File

@ -94,6 +94,8 @@ async function calcAspectRatio() {
onMounted(() => { onMounted(() => {
calcAspectRatio(); calcAspectRatio();
if (gallery.value == null) return; // TS
lightbox = new PhotoSwipeLightbox({ lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList dataSource: props.mediaList
.filter(media => { .filter(media => {

View File

@ -91,7 +91,7 @@ const emit = defineEmits<{
(ev: 'opened'): void; (ev: 'opened'): void;
(ev: 'click'): void; (ev: 'click'): void;
(ev: 'esc'): void; (ev: 'esc'): void;
(ev: 'close'): void; (ev: 'close'): void; // TODO: (refactor) closing
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
@ -148,7 +148,6 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
useSendAnime.value = true; useSendAnime.value = true;
} }
// eslint-disable-next-line vue/no-mutating-props
if (props.anchorElement) props.anchorElement.style.pointerEvents = 'auto'; if (props.anchorElement) props.anchorElement.style.pointerEvents = 'auto';
showing.value = false; showing.value = false;
emit('close'); emit('close');
@ -319,7 +318,6 @@ const alignObserver = new ResizeObserver((entries, observer) => {
onMounted(() => { onMounted(() => {
watch(() => props.anchorElement, async () => { watch(() => props.anchorElement, async () => {
if (props.anchorElement) { if (props.anchorElement) {
// eslint-disable-next-line vue/no-mutating-props
props.anchorElement.style.pointerEvents = 'none'; props.anchorElement.style.pointerEvents = 'none';
} }
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.anchorElement) != null); fixed.value = (type.value === 'drawer') || (getFixedContainer(props.anchorElement) != null);

View File

@ -654,7 +654,7 @@ function showRenoteMenu(): void {
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
{ type: 'divider' }, { type: 'divider' },
getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote), getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined, ...(($i?.isModerator || $i?.isAdmin) ? [getUnrenote()] : []),
], renoteTime.value); ], renoteTime.value);
} }
} }

View File

@ -392,6 +392,9 @@ const reactionsPaginator = markRaw(new Paginator('notes/reactions', {
})); }));
useTooltip(renoteButton, async (showing) => { useTooltip(renoteButton, async (showing) => {
const anchorElement = renoteButton.value;
if (anchorElement == null) return;
const renotes = await misskeyApi('notes/renotes', { const renotes = await misskeyApi('notes/renotes', {
noteId: appearNote.id, noteId: appearNote.id,
limit: 11, limit: 11,
@ -405,7 +408,7 @@ useTooltip(renoteButton, async (showing) => {
showing, showing,
users, users,
count: appearNote.renoteCount, count: appearNote.renoteCount,
anchorElement: renoteButton.value, anchorElement: anchorElement,
}, { }, {
closed: () => dispose(), closed: () => dispose(),
}); });

View File

@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<div :class="$style.root" class="_forceShrinkSpacer"> <div :class="$style.root" class="_forceShrinkSpacer">
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :key="reloadCount" :router="windowRouter"/> <StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :key="reloadCount.toString() + ':stacking'" :router="windowRouter"/>
<RouterView v-else :key="reloadCount" :router="windowRouter"/> <RouterView v-else :key="reloadCount.toString() + ':non-stacking'" :router="windowRouter"/>
</div> </div>
</MkWindow> </MkWindow>
</template> </template>
@ -58,20 +58,15 @@ const windowRouter = createRouter(props.initialPath);
const pageMetadata = ref<null | PageMetadata>(null); const pageMetadata = ref<null | PageMetadata>(null);
const windowEl = useTemplateRef('windowEl'); const windowEl = useTemplateRef('windowEl');
const history = ref<{ path: string; }[]>([{ const _history_ = ref<{ path: string; }[]>([{
path: windowRouter.getCurrentFullPath(), path: windowRouter.getCurrentFullPath(),
}]); }]);
const buttonsLeft = computed(() => { const buttonsLeft = computed(() => {
const buttons: Record<string, unknown>[] = []; return _history_.value.length > 1 ? [{
icon: 'ti ti-arrow-left',
if (history.value.length > 1) { title: i18n.ts.goBack,
buttons.push({ onClick: back,
icon: 'ti ti-arrow-left', }] : [];
onClick: back,
});
}
return buttons;
}); });
const buttonsRight = computed(() => { const buttonsRight = computed(() => {
const buttons = [{ const buttons = [{
@ -97,12 +92,12 @@ function getSearchMarker(path: string) {
const searchMarkerId = ref<string | null>(getSearchMarker(props.initialPath)); const searchMarkerId = ref<string | null>(getSearchMarker(props.initialPath));
windowRouter.addListener('push', ctx => { windowRouter.addListener('push', ctx => {
history.value.push({ path: ctx.fullPath }); _history_.value.push({ path: ctx.fullPath });
}); });
windowRouter.addListener('replace', ctx => { windowRouter.addListener('replace', ctx => {
history.value.pop(); _history_.value.pop();
history.value.push({ path: ctx.fullPath }); _history_.value.push({ path: ctx.fullPath });
}); });
windowRouter.addListener('change', ctx => { windowRouter.addListener('change', ctx => {
@ -150,8 +145,8 @@ const contextmenu = computed(() => ([{
}])); }]));
function back() { function back() {
history.value.pop(); _history_.value.pop();
windowRouter.replaceByPath(history.value.at(-1)!.path); windowRouter.replaceByPath(_history_.value.at(-1)!.path);
} }
function reload() { function reload() {

View File

@ -57,7 +57,7 @@ async function _close() {
modal.value?.close(); modal.value?.close();
} }
function onEsc(ev: KeyboardEvent) { function onEsc() {
_close(); _close();
} }

View File

@ -78,7 +78,7 @@ function subscribe() {
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
return promiseDialog(registration.value.pushManager.subscribe({ return promiseDialog(registration.value.pushManager.subscribe({
userVisibleOnly: true, userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey), applicationServerKey: urlBase64ToBase64(instance.swPublickey),
}) })
.then(async subscription => { .then(async subscription => {
pushSubscription.value = subscription; pushSubscription.value = subscription;
@ -131,22 +131,16 @@ function encode(buffer: ArrayBuffer | null) {
} }
/** /**
* Convert the URL safe base64 string to a Uint8Array * Convert the URL safe base64 string to a base64 string
* @param base64String base64 string * @param base64String base64 string
*/ */
function urlBase64ToUint8Array(base64String: string): Uint8Array { function urlBase64ToBase64(base64String: string): string {
const padding = '='.repeat((4 - base64String.length % 4) % 4); const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding) const base64 = (base64String + padding)
.replace(/-/g, '+') .replace(/-/g, '+')
.replace(/_/g, '/'); .replace(/_/g, '/');
const rawData = window.atob(base64); return base64;
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
} }
if (navigator.serviceWorker == null) { if (navigator.serviceWorker == null) {

View File

@ -58,18 +58,22 @@ const emit = defineEmits<{
const buttonEl = useTemplateRef('buttonEl'); const buttonEl = useTemplateRef('buttonEl');
const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, '')); const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
const canToggle = computed(() => { const canToggle = computed(() => {
const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction);
// TODO // TODO
//return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value); //return !props.reaction.match(/@\w/) && $i && emoji && checkReactionPermissions($i, props.note, emoji);
return !props.reaction.match(/@\w/) && $i && emoji.value; return !props.reaction.match(/@\w/) && $i && emoji;
}); });
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.'); const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.');
async function toggleReaction() { async function toggleReaction() {
if (!canToggle.value) return; if (!canToggle.value) return;
if ($i == null) return;
const me = $i;
const oldReaction = props.myReaction; const oldReaction = props.myReaction;
if (oldReaction) { if (oldReaction) {
@ -93,7 +97,7 @@ async function toggleReaction() {
noteId: props.noteId, noteId: props.noteId,
}).then(() => { }).then(() => {
noteEvents.emit(`unreacted:${props.noteId}`, { noteEvents.emit(`unreacted:${props.noteId}`, {
userId: $i!.id, userId: me.id,
reaction: oldReaction, reaction: oldReaction,
}); });
if (oldReaction !== props.reaction) { if (oldReaction !== props.reaction) {
@ -101,10 +105,12 @@ async function toggleReaction() {
noteId: props.noteId, noteId: props.noteId,
reaction: props.reaction, reaction: props.reaction,
}).then(() => { }).then(() => {
const emoji = customEmojisMap.get(emojiName.value);
if (emoji == null) return;
noteEvents.emit(`reacted:${props.noteId}`, { noteEvents.emit(`reacted:${props.noteId}`, {
userId: $i!.id, userId: me.id,
reaction: props.reaction, reaction: props.reaction,
emoji: emoji.value, emoji: emoji,
}); });
}); });
} }
@ -131,10 +137,13 @@ async function toggleReaction() {
noteId: props.noteId, noteId: props.noteId,
reaction: props.reaction, reaction: props.reaction,
}).then(() => { }).then(() => {
const emoji = customEmojisMap.get(emojiName.value);
if (emoji == null) return;
noteEvents.emit(`reacted:${props.noteId}`, { noteEvents.emit(`reacted:${props.noteId}`, {
userId: $i!.id, userId: me.id,
reaction: props.reaction, reaction: props.reaction,
emoji: emoji.value, emoji: emoji,
}); });
}); });
// TODO: // TODO:
@ -217,6 +226,8 @@ onMounted(() => {
if (!mock) { if (!mock) {
useTooltip(buttonEl, async (showing) => { useTooltip(buttonEl, async (showing) => {
if (buttonEl.value == null) return;
const reactions = await misskeyApiGet('notes/reactions', { const reactions = await misskeyApiGet('notes/reactions', {
noteId: props.noteId, noteId: props.noteId,
type: props.reaction, type: props.reaction,

View File

@ -105,9 +105,7 @@ async function addRole() {
.map(r => ({ text: r.name, value: r })); .map(r => ({ text: r.name, value: r }));
const { canceled, result: role } = await os.select({ items }); const { canceled, result: role } = await os.select({ items });
if (canceled) { if (canceled || role == null) return;
return;
}
selectedRoleIds.value.push(role.id); selectedRoleIds.value.push(role.id);
} }

View File

@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-if="q_federation === 'yes'" v-model="q_remoteContentsCleaning"> <MkSwitch v-if="q_federation === 'yes'" v-model="q_remoteContentsCleaning">
<template #label>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning }}</template> <template #label>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning }}</template>
<template #caption>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning_description }}</template> <template #caption>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning_description }} ({{ i18n.ts._serverSetupWizard.remoteContentsCleaning_description2 }})</template>
</MkSwitch> </MkSwitch>
</div> </div>
</MkFolder> </MkFolder>

View File

@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/> <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha"/> <MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha" :sitekey="null"/>
</div> </div>
<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> <MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>

View File

@ -297,76 +297,97 @@ function prepend(note: Misskey.entities.Note & MisskeyEntity) {
} }
} }
let connection: Misskey.IChannelConnection | null = null;
let connection2: Misskey.IChannelConnection | null = null;
const stream = store.s.realtimeMode ? useStream() : null; const stream = store.s.realtimeMode ? useStream() : null;
const connections = {
antenna: null as Misskey.IChannelConnection<Misskey.Channels['antenna']> | null,
homeTimeline: null as Misskey.IChannelConnection<Misskey.Channels['homeTimeline']> | null,
localTimeline: null as Misskey.IChannelConnection<Misskey.Channels['localTimeline']> | null,
hybridTimeline: null as Misskey.IChannelConnection<Misskey.Channels['hybridTimeline']> | null,
globalTimeline: null as Misskey.IChannelConnection<Misskey.Channels['globalTimeline']> | null,
main: null as Misskey.IChannelConnection<Misskey.Channels['main']> | null,
userList: null as Misskey.IChannelConnection<Misskey.Channels['userList']> | null,
channel: null as Misskey.IChannelConnection<Misskey.Channels['channel']> | null,
roleTimeline: null as Misskey.IChannelConnection<Misskey.Channels['roleTimeline']> | null,
};
function connectChannel() { function connectChannel() {
if (stream == null) return; if (stream == null) return;
if (props.src === 'antenna') { if (props.src === 'antenna') {
if (props.antenna == null) return; if (props.antenna == null) return;
connection = stream.useChannel('antenna', { connections.antenna = stream.useChannel('antenna', {
antennaId: props.antenna, antennaId: props.antenna,
}); });
connections.antenna.on('note', prepend);
} else if (props.src === 'home') { } else if (props.src === 'home') {
connection = stream.useChannel('homeTimeline', { connections.homeTimeline = stream.useChannel('homeTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connection2 = stream.useChannel('main'); connections.main = stream.useChannel('main');
connections.homeTimeline.on('note', prepend);
} else if (props.src === 'local') { } else if (props.src === 'local') {
connection = stream.useChannel('localTimeline', { connections.localTimeline = stream.useChannel('localTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connections.localTimeline.on('note', prepend);
} else if (props.src === 'social') { } else if (props.src === 'social') {
connection = stream.useChannel('hybridTimeline', { connections.hybridTimeline = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connections.hybridTimeline.on('note', prepend);
} else if (props.src === 'global') { } else if (props.src === 'global') {
connection = stream.useChannel('globalTimeline', { connections.globalTimeline = stream.useChannel('globalTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connections.globalTimeline.on('note', prepend);
} else if (props.src === 'mentions') { } else if (props.src === 'mentions') {
connection = stream.useChannel('main'); connections.main = stream.useChannel('main');
connection.on('mention', prepend); connections.main.on('mention', prepend);
} else if (props.src === 'directs') { } else if (props.src === 'directs') {
const onNote = note => { const onNote = note => {
if (note.visibility === 'specified') { if (note.visibility === 'specified') {
prepend(note); prepend(note);
} }
}; };
connection = stream.useChannel('main'); connections.main = stream.useChannel('main');
connection.on('mention', onNote); connections.main.on('mention', onNote);
} else if (props.src === 'list') { } else if (props.src === 'list') {
if (props.list == null) return; if (props.list == null) return;
connection = stream.useChannel('userList', { connections.userList = stream.useChannel('userList', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
listId: props.list, listId: props.list,
}); });
connections.userList.on('note', prepend);
} else if (props.src === 'channel') { } else if (props.src === 'channel') {
if (props.channel == null) return; if (props.channel == null) return;
connection = stream.useChannel('channel', { connections.channel = stream.useChannel('channel', {
channelId: props.channel, channelId: props.channel,
}); });
connections.channel.on('note', prepend);
} else if (props.src === 'role') { } else if (props.src === 'role') {
if (props.role == null) return; if (props.role == null) return;
connection = stream.useChannel('roleTimeline', { connections.roleTimeline = stream.useChannel('roleTimeline', {
roleId: props.role, roleId: props.role,
}); });
connections.roleTimeline.on('note', prepend);
} }
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
} }
function disconnectChannel() { function disconnectChannel() {
if (connection) connection.dispose(); for (const key in connections) {
if (connection2) connection2.dispose(); const conn = connections[key as keyof typeof connections];
if (conn != null) {
conn.dispose();
connections[key as keyof typeof connections] = null;
}
}
} }
if (store.s.realtimeMode) { if (store.s.realtimeMode) {

View File

@ -59,7 +59,7 @@ import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-sep
import { Paginator } from '@/utility/paginator.js'; import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{ const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][]; excludeTypes?: typeof notificationTypes[number][] | null;
}>(); }>();
const rootEl = useTemplateRef('rootEl'); const rootEl = useTemplateRef('rootEl');

View File

@ -39,17 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only
export type Tab = { export type Tab = {
key: string; key: string;
onClick?: (ev: MouseEvent) => void; onClick?: (ev: MouseEvent) => void;
} & ( iconOnly?: boolean;
| { title: string;
iconOnly?: false; icon?: string;
title: string; };
icon?: string;
}
| {
iconOnly: true;
icon: string;
}
);
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */ /* eslint-disable import/no-default-export */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import MkTagItem from './MkTagItem.vue'; import MkTagItem from './MkTagItem.vue';

View File

@ -50,6 +50,7 @@ export type DefaultStoredWidget = {
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, ref, computed } from 'vue'; import { defineAsyncComponent, ref, computed } from 'vue';
import { isLink } from '@@/js/is-link.js';
import { genId } from '@/utility/id.js'; import { genId } from '@/utility/id.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -57,8 +58,7 @@ import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { isLink } from '@@/js/is-link.js'; \import { useMkSelect } from '@/composables/use-mkselect';
import { useMkSelect } from '@/composables/use-mkselect';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@ -81,7 +81,7 @@ const emit = defineEmits<{
(ev: 'updateWidgets', widgets: Widget[]): void; (ev: 'updateWidgets', widgets: Widget[]): void;
(ev: 'addWidget', widget: Widget): void; (ev: 'addWidget', widget: Widget): void;
(ev: 'removeWidget', widget: Widget): void; (ev: 'removeWidget', widget: Widget): void;
(ev: 'updateWidget', widget: Partial<Widget>): void; (ev: 'updateWidget', widget: { id: Widget['id']; data: Widget['data']; }): void;
(ev: 'exit'): void; (ev: 'exit'): void;
}>(); }>();
@ -112,7 +112,7 @@ const addWidget = () => {
const removeWidget = (widget) => { const removeWidget = (widget) => {
emit('removeWidget', widget); emit('removeWidget', widget);
}; };
const updateWidget = (id, data) => { const updateWidget = (id: Widget['id'], data: Widget['data']) => {
emit('updateWidget', { id, data }); emit('updateWidget', { id, data });
}; };

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading/> <MkLoading/>
</div> </div>
<div v-else-if="resolved"> <div v-else-if="resolved">
<slot :result="result"></slot> <slot :result="result as T"></slot>
</div> </div>
<div v-else> <div v-else>
<div :class="$style.error"> <div :class="$style.error">

View File

@ -2,11 +2,10 @@
* SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect, userEvent, waitFor, within } from '@storybook/test'; import { expect, userEvent, waitFor, within } from '@storybook/test';
import type { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue'; import MkAd from './MkAd.vue';
import type { StoryObj } from '@storybook/vue3';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const common = { const common = {
@ -68,7 +67,7 @@ const common = {
await expect(imgAgain).toBeInTheDocument(); await expect(imgAgain).toBeInTheDocument();
}, },
args: { args: {
prefer: [], preferForms: [],
specify: { specify: {
id: 'someadid', id: 'someadid',
ratio: 1, ratio: 1,

View File

@ -52,7 +52,7 @@ import { prefer } from '@/preferences.js';
type Ad = (typeof instance)['ads'][number]; type Ad = (typeof instance)['ads'][number];
const props = defineProps<{ const props = defineProps<{
preferForms: string[]; preferForms?: string[];
specify?: Ad; specify?: Ad;
}>(); }>();
@ -71,7 +71,7 @@ const choseAd = (): Ad | null => {
ratio: 0, ratio: 0,
} : ad); } : ad);
let ads = allAds.filter(ad => props.preferForms.includes(ad.place)); let ads = props.preferForms ? allAds.filter(ad => props.preferForms!.includes(ad.place)) : allAds;
if (ads.length === 0) { if (ads.length === 0) {
ads = allAds.filter(ad => ad.place === 'square'); ads = allAds.filter(ad => ad.place === 'square');

View File

@ -84,7 +84,6 @@ const bound = computed(() => props.link
: {}); : {});
const url = computed(() => { const url = computed(() => {
if (props.user.avatarUrl == null) return null;
if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl); if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
return props.user.avatarUrl; return props.user.avatarUrl;
}); });

View File

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import { expect, waitFor } from '@storybook/test'; import { expect, waitFor } from '@storybook/test';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import MkError from './MkError.vue'; import MkError from './MkError.vue';

View File

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { waitFor } from '@storybook/test'; import { waitFor } from '@storybook/test';
import MkPageHeader from './MkPageHeader.vue'; import MkPageHeader from './MkPageHeader.vue';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
@ -59,6 +59,7 @@ export const Icon = {
{ {
...OneTab.args.tabs[0], ...OneTab.args.tabs[0],
icon: 'ti ti-home', icon: 'ti ti-home',
title: 'Home',
}, },
], ],
}, },
@ -71,6 +72,7 @@ export const IconOnly = {
{ {
key: Icon.args.tabs[0].key, key: Icon.args.tabs[0].key,
icon: Icon.args.tabs[0].icon, icon: Icon.args.tabs[0].icon,
title: Icon.args.tabs[0].title,
iconOnly: true, iconOnly: true,
}, },
], ],

View File

@ -39,17 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only
export type Tab = { export type Tab = {
key: string; key: string;
onClick?: (ev: MouseEvent) => void; onClick?: (ev: MouseEvent) => void;
} & ( iconOnly?: boolean;
| { title: string;
iconOnly?: false; icon?: string;
title: string; };
icon?: string;
}
| {
iconOnly: true;
icon: string;
}
);
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
@ -59,7 +52,7 @@ import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
tabs?: Tab[]; tabs?: Tab[];
tab?: string; tab?: string;
rootEl?: HTMLElement; rootEl?: HTMLElement | null;
}>(), { }>(), {
tabs: () => ([] as Tab[]), tabs: () => ([] as Tab[]),
}); });

View File

@ -30,19 +30,21 @@ const props = defineProps<{
router?: Router; router?: Router;
}>(); }>();
const router = props.router ?? inject(DI.router); const _router = props.router ?? inject(DI.router);
if (router == null) { if (_router == null) {
throw new Error('no router provided'); throw new Error('no router provided');
} }
const router = _router;
const viewId = randomId(); const viewId = randomId();
provide(DI.viewId, viewId); provide(DI.viewId, viewId);
const currentDepth = inject(DI.routerCurrentDepth, 0); const currentDepth = inject(DI.routerCurrentDepth, 0);
provide(DI.routerCurrentDepth, currentDepth + 1); provide(DI.routerCurrentDepth, currentDepth + 1);
const current = router.current!; const current = router.current;
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
const currentPageProps = ref(current.props); const currentPageProps = ref(current.props);
let currentRoutePath = current.route.path; let currentRoutePath = current.route.path;
@ -52,14 +54,10 @@ router.useListener('change', ({ resolved }) => {
if (resolved == null || 'redirect' in resolved.route) return; if (resolved == null || 'redirect' in resolved.route) return;
if (resolved.route.path === currentRoutePath && deepEqual(resolved.props, currentPageProps.value)) return; if (resolved.route.path === currentRoutePath && deepEqual(resolved.props, currentPageProps.value)) return;
function _() { currentPageComponent.value = resolved.route.component;
currentPageComponent.value = resolved.route.component; currentPageProps.value = resolved.props;
currentPageProps.value = resolved.props; key.value = router.getCurrentFullPath();
key.value = router.getCurrentFullPath(); currentRoutePath = resolved.route.path;
currentRoutePath = resolved.route.path;
}
_();
}); });
</script> </script>

View File

@ -1,14 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<slot></slot>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" module>
</style>

View File

@ -87,7 +87,7 @@ router.useListener('change', ({ resolved }) => {
const fullPath = router.getCurrentFullPath(); const fullPath = router.getCurrentFullPath();
if (tabs.value.some(tab => tab.routePath === routePath && deepEqual(resolved.props, tab.props))) { if (tabs.value.some(tab => tab.routePath === routePath && deepEqual(resolved.props, tab.props))) {
const newTabs = []; const newTabs = [] as typeof tabs.value;
for (const tab of tabs.value) { for (const tab of tabs.value) {
newTabs.push(tab); newTabs.push(tab);

View File

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3'; import type { StoryObj } from '@storybook/vue3';
import { ref } from 'vue'; import { ref } from 'vue';
import { commonHandlers } from '../../../.storybook/mocks.js'; import { commonHandlers } from '../../../.storybook/mocks.js';

View File

@ -9,7 +9,7 @@ import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import type { Component, Ref } from 'vue'; import type { Component, Ref } from 'vue';
import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { ComponentEmit, ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js'; import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js'; import type { PostFormProps } from '@/types/post-form.js';
@ -157,28 +157,9 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
return zIndexes[priority]; return zIndexes[priority];
} }
// InstanceType<typeof Component>['$emit'] だとインターセクション型が返ってきて
// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
type ComponentEmit<T> = T extends new () => { $props: infer Props }
? [keyof Pick<T, Extract<keyof T, `on${string}`>>] extends [never]
? Record<string, unknown> // *.ts ファイルから型がうまく取れないとき用(これがないと {} になって型エラーがうるさい)
: EmitsExtractor<Props>
: T extends (...args: any) => any
? ReturnType<T> extends { [x: string]: any; __ctx?: { [x: string]: any; props: infer Props } }
? [keyof Pick<T, Extract<keyof T, `on${string}`>>] extends [never]
? Record<string, unknown>
: EmitsExtractor<Props>
: never
: never;
// props に ref を許可するようにする // props に ref を許可するようにする
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> }; type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> };
type EmitsExtractor<T> = {
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
};
export function popup<T extends Component>( export function popup<T extends Component>(
component: T, component: T,
props: ComponentProps<T>, props: ComponentProps<T>,
@ -703,7 +684,7 @@ export async function cropImageFile(imageFile: File | Blob, options: {
}); });
} }
export function popupMenu(items: MenuItem[], anchorElement?: HTMLElement | EventTarget | null, options?: { export function popupMenu(items: (MenuItem | null)[], anchorElement?: HTMLElement | EventTarget | null, options?: {
align?: string; align?: string;
width?: number; width?: number;
onClosing?: () => void; onClosing?: () => void;
@ -715,7 +696,7 @@ export function popupMenu(items: MenuItem[], anchorElement?: HTMLElement | Event
let returnFocusTo = getHTMLElementOrNull(anchorElement) ?? getHTMLElementOrNull(window.document.activeElement); let returnFocusTo = getHTMLElementOrNull(anchorElement) ?? getHTMLElementOrNull(window.document.activeElement);
return new Promise(resolve => nextTick(() => { return new Promise(resolve => nextTick(() => {
const { dispose } = popup(MkPopupMenu, { const { dispose } = popup(MkPopupMenu, {
items, items: items.filter(x => x != null),
anchorElement, anchorElement,
width: options?.width, width: options?.width,
align: options?.align, align: options?.align,

View File

@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFoldableSection> </MkFoldableSection>
<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category"> <MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category ?? '___root___'">
<template #header>{{ category || i18n.ts.other }}</template> <template #header>{{ category || i18n.ts.other }}</template>
<div :class="$style.emojis"> <div :class="$style.emojis">
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/> <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/>
@ -48,7 +48,7 @@ import { $i } from '@/i.js';
const customEmojiTags = getCustomEmojiTags(); const customEmojiTags = getCustomEmojiTags();
const q = ref(''); const q = ref('');
const searchEmojis = ref<Misskey.entities.EmojiSimple[]>(null); const searchEmojis = ref<Misskey.entities.EmojiSimple[] | null>(null);
const selectedTags = ref(new Set()); const selectedTags = ref(new Set());
function search() { function search() {

View File

@ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, markRaw, ref } from 'vue'; import { computed, markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';

Some files were not shown because too many files have changed in this diff Show More