Merge branch 'develop' into fix-13913
This commit is contained in:
commit
599d597094
|
@ -12,10 +12,13 @@
|
|||
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
|
||||
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
|
||||
- データベースサイズへの効果が見られない場合はautovacuumが有効になっているか確認してください
|
||||
- ハイパーリンクによる参照は検知できないためリンク切れとなります。
|
||||
- 現時点では、2023-10-01以前にクリップされたリモートのノートは検知しないため削除対象となります。
|
||||
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました
|
||||
- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました
|
||||
- 実際の動作に即した名称になり、馴染みのない人でも理解しやすくなりました
|
||||
- 他サービスにおける「ダイレクトメッセージ」に相当するMisskeyの機能は「チャット」ですが、「ダイレクト投稿」という名称の機能が存在するとそちらがダイレクトメッセージ機能であるような誤解を生んでいました
|
||||
- 他サービスにおける「ダイレクトメッセージ」に相当するMisskeyの機能は「チャット」ですが(過去のバージョンのMisskeyでも、当該機能は「チャット」ではなく「ダイレクトメッセージ」でした)、「ダイレクト投稿」という名称の機能が存在するとそちらがダイレクトメッセージ機能であるような誤解を生んでいました
|
||||
- 今後、「チャット」の名称を「ダイレクトメッセージ」に戻す可能性があります
|
||||
- mfm.jsをアップデートしました
|
||||
- Enhance: Unicode 15.1 および 16.0 に収録されている絵文字に対応
|
||||
- Enhance: acctに `.` が入っているユーザーのメンションに対応
|
||||
|
@ -27,6 +30,7 @@
|
|||
- プラグインは1.xに対応したものが必要です
|
||||
- Playはそのまま動作しますが、新規に作られるプリセットは1.xになります
|
||||
- 以前のバージョンから無効化されていた note_view_interruptor が有効になりました
|
||||
- ハンドラは同期的である必要があります
|
||||
- Feat: セーフモード
|
||||
- プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます
|
||||
- 以下の方法でセーフモードを起動できます
|
||||
|
@ -56,6 +60,8 @@
|
|||
- Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正
|
||||
- Fix: 照会ダイアログでap/showでローカルユーザーを解決した際@username@nullに飛ばされる問題を修正
|
||||
- Fix: アイコンのデコレーションを付ける際にデコレーションが表示されなくなる問題を修正
|
||||
- Fix: 管理中アカウント一覧で正しい表示が行われない問題を修正
|
||||
- Fix: lookupページでリモートURLを指定した際に正しく動作しない問題を修正
|
||||
- Fix: Ctrlキー(Commandキー)を押下しながらリンクをクリックすると新しいタブで開くように
|
||||
|
||||
### Server
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { action } from 'storybook/actions';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js';
|
||||
import { commonHandlers } from '../packages/frontend/.storybook/mocks.js';
|
||||
|
|
|
@ -1668,7 +1668,7 @@ _serverSettings:
|
|||
restartServerSetupWizardConfirm_text: "Algunes configuracions actuals seran restablertes."
|
||||
entrancePageStyle: "Estil de la pàgina d'inici"
|
||||
showTimelineForVisitor: "Mostrar la línia de temps"
|
||||
showActivityiesForVisitor: "Mostrar les activitats"
|
||||
showActivitiesForVisitor: "Mostrar activitat"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Tot obert al públic "
|
||||
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."
|
||||
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_description2: "Alguns mètodes de referència, com els enllaços, no poden ser detectats pel sistema."
|
||||
adminInfo: "Informació de l'administrador "
|
||||
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."
|
||||
|
|
|
@ -1218,8 +1218,8 @@ showRepliesToOthersInTimeline: "Show replies to others in timeline"
|
|||
hideRepliesToOthersInTimeline: "Hide replies to others from timeline"
|
||||
showRepliesToOthersInTimelineAll: "Show 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?"
|
||||
confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide 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: "Are you sure you want to hide replies from everyone you follow in your timeline? This action is irreversible."
|
||||
externalServices: "External Services"
|
||||
sourceCode: "Source code"
|
||||
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."
|
||||
entrancePageStyle: "Entrance page style"
|
||||
showTimelineForVisitor: "Show timeline"
|
||||
showActivityiesForVisitor: "Show activities"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Everything is public"
|
||||
localOnly: "Only local content is published, remote content is kept private"
|
||||
|
|
|
@ -1668,7 +1668,7 @@ _serverSettings:
|
|||
restartServerSetupWizardConfirm_text: "Algunas configuraciones actuales se restablecerán"
|
||||
entrancePageStyle: "Estilo de la página de inicio"
|
||||
showTimelineForVisitor: "Mostrar la línea de tiempo"
|
||||
showActivityiesForVisitor: "Mostrar actividades"
|
||||
showActivitiesForVisitor: "Mostrar actividades"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Todo es público."
|
||||
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."
|
||||
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_description2: "Ciertos métodos de referencia, como los hipervínculos, no pueden ser detectados por el sistema."
|
||||
adminInfo: "Información del administrador"
|
||||
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."
|
||||
|
|
|
@ -12032,13 +12032,17 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"youCanConfigureMoreFederationSettingsLater": string;
|
||||
/**
|
||||
* 受信コンテンツの自動クリーニング
|
||||
* リモートコンテンツの自動クリーニング
|
||||
*/
|
||||
"remoteContentsCleaning": string;
|
||||
/**
|
||||
* 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。
|
||||
* 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。
|
||||
*/
|
||||
"remoteContentsCleaning_description": string;
|
||||
/**
|
||||
* ローカル内リモートコンテンツへのハイパーリンクはリンク切れとなります。
|
||||
*/
|
||||
"remoteContentsCleaning_description2": string;
|
||||
/**
|
||||
* 管理者情報
|
||||
*/
|
||||
|
|
|
@ -1668,7 +1668,7 @@ _serverSettings:
|
|||
restartServerSetupWizardConfirm_text: "Verranno ripristinate alcune tue impostazioni personalizzate."
|
||||
entrancePageStyle: "Stile della pagina di ingresso"
|
||||
showTimelineForVisitor: "Mostra la Timeline a visitatori non autenticati"
|
||||
showActivityiesForVisitor: "Mostra le attività a visitatori non autenticati"
|
||||
showActivitiesForVisitor: "Mostrare la propria attività"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Tutto pubblico"
|
||||
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."
|
||||
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_description2: "Alcuni metodi di riferimento, come i collegamenti ipertestuali, non possono essere rilevati sul sistema."
|
||||
adminInfo: "Informazioni sull'amministratore"
|
||||
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."
|
||||
|
|
|
@ -3216,8 +3216,9 @@ _serverSetupWizard:
|
|||
doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。"
|
||||
doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。"
|
||||
youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。"
|
||||
remoteContentsCleaning: "受信コンテンツの自動クリーニング"
|
||||
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
|
||||
remoteContentsCleaning: "リモートコンテンツの自動クリーニング"
|
||||
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
|
||||
remoteContentsCleaning_description2: "ローカル内リモートコンテンツへのハイパーリンクはリンク切れとなります。"
|
||||
adminInfo: "管理者情報"
|
||||
adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。"
|
||||
adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。"
|
||||
|
|
|
@ -1668,7 +1668,7 @@ _serverSettings:
|
|||
restartServerSetupWizardConfirm_text: "현재 일부 설정은 리셋됩니다."
|
||||
entrancePageStyle: "입구 페이지의 스타일"
|
||||
showTimelineForVisitor: "타임라인 표시"
|
||||
showActivityiesForVisitor: "활동 표시"
|
||||
showActivitiesForVisitor: "액티비티 표시하기"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "모두 공개"
|
||||
localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개"
|
||||
|
@ -3120,6 +3120,7 @@ _serverSetupWizard:
|
|||
youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다."
|
||||
remoteContentsCleaning: "리모트 콘텐츠 자동 정리"
|
||||
remoteContentsCleaning_description: "연합 중인 서버가 있는 경우, 리모트 서버에서 대단히 많은 콘텐츠를 받아오게 됩니다. 자동 정리 기능을 활성화하면, 오래되고 서버에서 더 이상 조회되지 않는 콘텐츠를 자동으로 서버에서 삭제하여, 스토리지를 절약할 수 있습니다."
|
||||
remoteContentsCleaning_description2: "로컬 내 원격 콘텐츠로의 하이퍼링크는 깨진 링크로 됩니다."
|
||||
adminInfo: "관리자 정보"
|
||||
adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다."
|
||||
adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다."
|
||||
|
|
|
@ -1668,7 +1668,6 @@ _serverSettings:
|
|||
restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır."
|
||||
entrancePageStyle: "Giriş sayfası stili"
|
||||
showTimelineForVisitor: "Panoyu göster"
|
||||
showActivityiesForVisitor: "Aktiviteleri göster"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Her şey halka açıktır."
|
||||
localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur."
|
||||
|
|
|
@ -1668,7 +1668,7 @@ _serverSettings:
|
|||
restartServerSetupWizardConfirm_text: "现有的部分设定将重置。"
|
||||
entrancePageStyle: "入口页面样式"
|
||||
showTimelineForVisitor: "显示时间线"
|
||||
showActivityiesForVisitor: "显示活动"
|
||||
showActivitiesForVisitor: "显示活动"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "全部公开"
|
||||
localOnly: "仅公开本地内容,隐藏远程内容"
|
||||
|
@ -3120,6 +3120,7 @@ _serverSetupWizard:
|
|||
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。"
|
||||
remoteContentsCleaning: "自动清理传入内容"
|
||||
remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
|
||||
remoteContentsCleaning_description2: "如超链接之类的某些引用方法无法被系统检测到。"
|
||||
adminInfo: "管理员信息"
|
||||
adminInfo_description: "设置用于接受询问的管理员信息。"
|
||||
adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。"
|
||||
|
|
|
@ -1668,7 +1668,7 @@ _serverSettings:
|
|||
restartServerSetupWizardConfirm_text: "當前的部分設定將會被重設。"
|
||||
entrancePageStyle: "入口頁面的樣式"
|
||||
showTimelineForVisitor: "顯示時間軸"
|
||||
showActivityiesForVisitor: "顯示活動"
|
||||
showActivitiesForVisitor: "顯示活動"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "全部公開\n"
|
||||
localOnly: "僅公開本地內容,遠端內容則不公開\n"
|
||||
|
@ -3120,6 +3120,7 @@ _serverSetupWizard:
|
|||
youCanConfigureMoreFederationSettingsLater: "您可以在稍後進行更高級的設定,例如指定可以聯繫的伺服器等。\n"
|
||||
remoteContentsCleaning: "自動清理接收的內容"
|
||||
remoteContentsCleaning_description: "進行聯邦後,會持續接收大量內容。啟用自動清理功能後,系統會自動從伺服器中刪除未被參照的過時內容,以節省儲存空間。"
|
||||
remoteContentsCleaning_description2: "有些引用方式系統上無法檢測到,例如超連結。"
|
||||
adminInfo: "管理員資訊"
|
||||
adminInfo_description: "設定用於接收查詢的管理者資訊。\n"
|
||||
adminInfo_mustBeFilled: "當設置為開放伺服器或啟用聯邦時,必須填寫此資訊。\n"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.8.0-beta.4",
|
||||
"version": "2025.8.0-beta.5",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -244,7 +244,6 @@ export class WebhookTestService {
|
|||
case 'reaction':
|
||||
return;
|
||||
default: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _exhaustiveAssertion: never = params.type;
|
||||
return;
|
||||
}
|
||||
|
@ -327,7 +326,6 @@ export class WebhookTestService {
|
|||
break;
|
||||
}
|
||||
default: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _exhaustiveAssertion: never = params.type;
|
||||
return;
|
||||
}
|
||||
|
@ -412,7 +410,7 @@ export class WebhookTestService {
|
|||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: user.avatarId == null ? null : user.avatarUrl,
|
||||
avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? '',
|
||||
avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash,
|
||||
avatarDecorations: user.avatarDecorations.map(it => ({
|
||||
id: it.id,
|
||||
|
|
|
@ -49,15 +49,12 @@ export class NoteReactionEntityService implements OnModuleInit {
|
|||
public async pack(
|
||||
src: MiNoteReaction['id'] | MiNoteReaction,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
options?: {
|
||||
withNote: boolean;
|
||||
},
|
||||
options?: object,
|
||||
hints?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'NoteReaction'>> {
|
||||
const opts = Object.assign({
|
||||
withNote: false,
|
||||
}, options);
|
||||
|
||||
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(),
|
||||
user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
|
||||
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(
|
||||
reactions: MiNoteReaction[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
options?: {
|
||||
withNote: boolean;
|
||||
},
|
||||
options?: object,
|
||||
): Promise<Packed<'NoteReaction'>[]> {
|
||||
const opts = Object.assign({
|
||||
withNote: false,
|
||||
}, 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.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) })));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -471,8 +471,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
|
||||
null;
|
||||
|
||||
const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null;
|
||||
const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null;
|
||||
const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : undefined;
|
||||
const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : undefined;
|
||||
const unreadAnnouncements = isMe && isDetailed ?
|
||||
(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
|
||||
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;
|
||||
|
||||
// TODO: 例えば avatarUrl: true など間違った型を設定しても型エラーにならないのをどうにかする(ジェネリクス使わない方法で実装するしかなさそう?)
|
||||
const packed = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
|
|
|
@ -22,7 +22,7 @@ import { packedFollowingSchema } from '@/models/json-schema/following.js';
|
|||
import { packedMutingSchema } from '@/models/json-schema/muting.js';
|
||||
import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.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 { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
|
||||
import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
|
||||
|
@ -65,6 +65,7 @@ import {
|
|||
packedMetaDetailedSchema,
|
||||
packedMetaLiteSchema,
|
||||
} 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 { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
|
||||
import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js';
|
||||
|
@ -92,6 +93,7 @@ export const refs = {
|
|||
Note: packedNoteSchema,
|
||||
NoteDraft: packedNoteDraftSchema,
|
||||
NoteReaction: packedNoteReactionSchema,
|
||||
NoteReactionWithNote: packedNoteReactionWithNoteSchema,
|
||||
NoteFavorite: packedNoteFavoriteSchema,
|
||||
Notification: packedNotificationSchema,
|
||||
DriveFile: packedDriveFileSchema,
|
||||
|
@ -133,6 +135,7 @@ export const refs = {
|
|||
MetaLite: packedMetaLiteSchema,
|
||||
MetaDetailedOnly: packedMetaDetailedOnlySchema,
|
||||
MetaDetailed: packedMetaDetailedSchema,
|
||||
UserWebhook: packedUserWebhookSchema,
|
||||
SystemWebhook: packedSystemWebhookSchema,
|
||||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||
ChatMessage: packedChatMessageSchema,
|
||||
|
|
|
@ -10,7 +10,6 @@ export const packedNoteReactionSchema = {
|
|||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
|
@ -28,3 +27,33 @@ export const packedNoteReactionSchema = {
|
|||
},
|
||||
},
|
||||
} 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;
|
||||
|
|
|
@ -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;
|
|
@ -65,7 +65,7 @@ export const packedUserLiteSchema = {
|
|||
avatarUrl: {
|
||||
type: 'string',
|
||||
format: 'url',
|
||||
nullable: true, optional: false,
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
avatarBlurhash: {
|
||||
type: 'string',
|
||||
|
@ -465,11 +465,11 @@ export const packedMeDetailedOnlySchema = {
|
|||
},
|
||||
isModerator: {
|
||||
type: 'boolean',
|
||||
nullable: true, optional: false,
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
isAdmin: {
|
||||
type: 'boolean',
|
||||
nullable: true, optional: false,
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
injectFeaturedNote: {
|
||||
type: 'boolean',
|
||||
|
@ -591,7 +591,7 @@ export const packedMeDetailedOnlySchema = {
|
|||
},
|
||||
mutedInstances: {
|
||||
type: 'array',
|
||||
nullable: true, optional: false,
|
||||
nullable: false, optional: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
|
|
|
@ -49,6 +49,34 @@ export const meta = {
|
|||
type: 'string',
|
||||
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: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
|
|
@ -157,6 +157,22 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
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;
|
||||
|
|
|
@ -223,10 +223,12 @@ export const meta = {
|
|||
sensitiveMediaDetection: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['none', 'all', 'local', 'remote'],
|
||||
},
|
||||
sensitiveMediaDetectionSensitivity: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'],
|
||||
},
|
||||
setSensitiveFlagAutomatically: {
|
||||
type: 'boolean',
|
||||
|
@ -473,6 +475,10 @@ export const meta = {
|
|||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
feedbackUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
summalyProxy: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
|
|
@ -73,8 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
updatedAt: new Date(),
|
||||
...Object.fromEntries(
|
||||
Object.entries(ps).filter(
|
||||
([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key)
|
||||
)
|
||||
([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key),
|
||||
),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,6 +46,14 @@ export const meta = {
|
|||
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(),
|
||||
lastUsedAt: token.lastUsedAt?.toISOString(),
|
||||
permission: token.app ? token.app.permission : token.permission,
|
||||
iconUrl: token.iconUrl,
|
||||
description: token.description ?? token.app?.description ?? null,
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,29 +21,7 @@ export const meta = {
|
|||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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 },
|
||||
},
|
||||
ref: 'UserWebhook',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -65,8 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
userId: me.id,
|
||||
});
|
||||
|
||||
return webhooks.map(webhook => (
|
||||
{
|
||||
return webhooks.map(webhook => ({
|
||||
id: webhook.id,
|
||||
userId: webhook.userId,
|
||||
name: webhook.name,
|
||||
|
@ -76,8 +53,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
active: webhook.active,
|
||||
latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null,
|
||||
latestStatus: webhook.latestStatus,
|
||||
}
|
||||
));
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,29 +28,7 @@ export const meta = {
|
|||
|
||||
res: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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 },
|
||||
},
|
||||
ref: 'UserWebhook',
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -23,6 +23,16 @@ export const meta = {
|
|||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserList',
|
||||
properties: {
|
||||
likedCount: {
|
||||
type: 'number',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
isLiked: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -28,7 +28,7 @@ export const meta = {
|
|||
items: {
|
||||
type: 'object',
|
||||
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 await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
|
||||
return await this.noteReactionEntityService.packManyWithNote(reactions, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// TODO: (可能な部分を)sharedに抽出して frontend と共通化
|
||||
|
||||
import tinycolor from 'tinycolor2';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { HttpResponse, http } from 'msw';
|
||||
import type { DefaultBodyType, HttpResponseResolver, JsonBodyType, PathParams } from 'msw';
|
||||
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[] {
|
||||
const rng = seedrandom(seed);
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
"prefix": "storyimplevent",
|
||||
"body": [
|
||||
"/* 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 $1 from './$1.vue';",
|
||||
"export const Default = {",
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { parse as vueSfcParse } from 'vue/compiler-sfc';
|
||||
import {
|
||||
createLogger,
|
||||
EnvironmentModuleGraph,
|
||||
type EnvironmentModuleGraph,
|
||||
type LogErrorOptions,
|
||||
type LogOptions,
|
||||
normalizePath,
|
||||
|
|
|
@ -83,7 +83,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.3",
|
||||
"@storybook/addon-actions": "9.0.8",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/addon-interactions": "8.6.14",
|
||||
"@storybook/addon-links": "9.1.3",
|
||||
|
|
|
@ -23,7 +23,7 @@ export async function getAccounts(): Promise<{
|
|||
host: string;
|
||||
id: Misskey.entities.User['id'];
|
||||
username: Misskey.entities.User['username'];
|
||||
user?: Misskey.entities.User | null;
|
||||
user?: Misskey.entities.MeDetailed | null;
|
||||
token: string | null;
|
||||
}[]> {
|
||||
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)) {
|
||||
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
|
||||
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() {
|
||||
if (!$i) return;
|
||||
const me = $i;
|
||||
return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => {
|
||||
if (reason === isAccountDeleted) {
|
||||
removeAccount(host, $i.id);
|
||||
removeAccount(host, me.id);
|
||||
if (Object.keys(store.s.accountTokens).length > 0) {
|
||||
login(Object.values(store.s.accountTokens)[0]);
|
||||
} else {
|
||||
|
@ -214,19 +215,37 @@ export async function openAccountMenu(opts: {
|
|||
includeCurrentAccount?: boolean;
|
||||
withExtraOperation: boolean;
|
||||
active?: Misskey.entities.User['id'];
|
||||
onChoose?: (account: Misskey.entities.User) => void;
|
||||
onChoose?: (account: Misskey.entities.MeDetailed) => void;
|
||||
}, ev: MouseEvent) {
|
||||
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) {
|
||||
return {
|
||||
type: 'user' as const,
|
||||
user: account,
|
||||
active: opts.active != null ? opts.active === id : false,
|
||||
action: async () => {
|
||||
if (opts.onChoose) {
|
||||
opts.onChoose(account);
|
||||
if (callback) {
|
||||
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 {
|
||||
switchAccount(host, id);
|
||||
}
|
||||
|
@ -238,13 +257,7 @@ export async function openAccountMenu(opts: {
|
|||
text: username,
|
||||
active: opts.active != null ? opts.active === id : false,
|
||||
action: async () => {
|
||||
if (opts.onChoose) {
|
||||
fetchAccount(token, id).then(account => {
|
||||
opts.onChoose(account);
|
||||
});
|
||||
} else {
|
||||
switchAccount(host, id);
|
||||
}
|
||||
// TODO
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -253,7 +266,7 @@ export async function openAccountMenu(opts: {
|
|||
const menuItems: MenuItem[] = [];
|
||||
|
||||
// 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) {
|
||||
menuItems.push({
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
*/
|
||||
|
||||
import { utils, values } from '@syuilo/aiscript';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import * as Misskey from 'misskey-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 FONTS = ['serif', 'sans-serif', 'monospace'] as const;
|
||||
|
@ -21,16 +21,15 @@ type BorderStyle = (typeof BORDER_STYLES)[number];
|
|||
export type AsUiComponentBase = {
|
||||
id: string;
|
||||
hidden?: boolean;
|
||||
children?: AsUiComponent['id'][];
|
||||
};
|
||||
|
||||
export type AsUiRoot = AsUiComponentBase & {
|
||||
type: 'root';
|
||||
children: AsUiComponent['id'][];
|
||||
};
|
||||
|
||||
export type AsUiContainer = AsUiComponentBase & {
|
||||
type: 'container';
|
||||
children?: AsUiComponent['id'][];
|
||||
align?: Align;
|
||||
bgColor?: string;
|
||||
fgColor?: string;
|
||||
|
@ -123,7 +122,6 @@ export type AsUiSelect = AsUiComponentBase & {
|
|||
|
||||
export type AsUiFolder = AsUiComponentBase & {
|
||||
type: 'folder';
|
||||
children?: AsUiComponent['id'][];
|
||||
title?: string;
|
||||
opened?: boolean;
|
||||
};
|
||||
|
|
|
@ -368,11 +368,6 @@ export async function mainBoot() {
|
|||
});
|
||||
});
|
||||
|
||||
main.on('unreadAntenna', () => {
|
||||
updateCurrentAccountPartial({ hasUnreadAntenna: true });
|
||||
sound.playMisskeySfx('antenna');
|
||||
});
|
||||
|
||||
main.on('newChatMessage', () => {
|
||||
updateCurrentAccountPartial({ hasUnreadChatMessages: true });
|
||||
sound.playMisskeySfx('chatMessage');
|
||||
|
|
|
@ -3,13 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { action } from 'storybook/actions';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAbuseReportWindow from './MkAbuseReportWindow.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* 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 { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
|
|
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
[$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">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,8 +61,8 @@ import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utili
|
|||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: Misskey.entities.User;
|
||||
withLocked: boolean;
|
||||
withDescription: boolean;
|
||||
withLocked?: boolean;
|
||||
withDescription?: boolean;
|
||||
}>(), {
|
||||
withLocked: true,
|
||||
withDescription: true,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* 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 { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* 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 { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* 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 { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* 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 type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { action } from 'storybook/actions';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkButton from './MkButton.vue';
|
||||
export const Default = {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { action } from 'storybook/actions';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
import { channel } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
* 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 { action } from '@storybook/addon-actions';
|
||||
import { action } from 'storybook/actions';
|
||||
import { channel } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkChannelList from './MkChannelList.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
|
@ -33,10 +32,7 @@ export const Default = {
|
|||
};
|
||||
},
|
||||
args: {
|
||||
pagination: {
|
||||
endpoint: 'channels/search',
|
||||
limit: 10,
|
||||
},
|
||||
paginator: new Paginator('channels/search', {}),
|
||||
},
|
||||
parameters: {
|
||||
chromatic: {
|
||||
|
|
|
@ -589,7 +589,10 @@ const fetchDriveFilesChart = 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 {
|
||||
series: [{
|
||||
name: 'In',
|
||||
|
@ -611,7 +614,10 @@ const fetchInstanceRequestsChart = async (): 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 {
|
||||
series: [{
|
||||
name: 'Users',
|
||||
|
@ -626,7 +632,10 @@ const fetchInstanceUsersChart = 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 {
|
||||
series: [{
|
||||
name: 'Notes',
|
||||
|
@ -641,7 +650,10 @@ const fetchInstanceNotesChart = 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 {
|
||||
series: [{
|
||||
name: 'Following',
|
||||
|
@ -664,7 +676,10 @@ const fetchInstanceFfChart = 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 {
|
||||
bytes: true,
|
||||
series: [{
|
||||
|
@ -680,7 +695,10 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
|
|||
};
|
||||
|
||||
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 {
|
||||
series: [{
|
||||
name: 'Drive files',
|
||||
|
@ -695,7 +713,10 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
|
|||
};
|
||||
|
||||
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 {
|
||||
series: [...(props.args?.withoutAll ? [] : [{
|
||||
name: 'All',
|
||||
|
@ -727,7 +748,10 @@ const fetchPerUserNotesChart = 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 {
|
||||
series: [{
|
||||
name: 'Unique PV (user)',
|
||||
|
@ -754,7 +778,10 @@ const fetchPerUserPvChart = 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 {
|
||||
series: [{
|
||||
name: 'Local',
|
||||
|
@ -769,7 +796,10 @@ const fetchPerUserFollowingChart = 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 {
|
||||
series: [{
|
||||
name: 'Local',
|
||||
|
@ -784,7 +814,10 @@ const fetchPerUserFollowersChart = 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 {
|
||||
bytes: true,
|
||||
series: [{
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { action } from 'storybook/actions';
|
||||
import { chatMessage } from '../../.storybook/fakes';
|
||||
import MkChatHistories from './MkChatHistories.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { action } from 'storybook/actions';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkClickerGame from './MkClickerGame.vue';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { action } from 'storybook/actions';
|
||||
import MkCodeEditor from './MkCodeEditor.vue';
|
||||
const code = `for (let i, 100) {
|
||||
<: if (i % 15 == 0) "FizzBuzz"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { action } from 'storybook/actions';
|
||||
import MkColorInput from './MkColorInput.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { action } from 'storybook/actions';
|
||||
import { file } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkCropperDialog from './MkCropperDialog.vue';
|
||||
|
|
|
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef, ref } from 'vue';
|
||||
import { onMounted, useTemplateRef, ref, onUnmounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import Cropper from 'cropperjs';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
@ -55,17 +55,19 @@ const imgEl = useTemplateRef('imgEl');
|
|||
let cropper: Cropper | null = null;
|
||||
const loading = ref(true);
|
||||
|
||||
const ok = async () => {
|
||||
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
|
||||
const croppedImage = await cropper?.getCropperImage();
|
||||
const croppedSection = await cropper?.getCropperSelection();
|
||||
async function ok() {
|
||||
const promise = new Promise<Blob>(async (res) => {
|
||||
if (cropper == null) throw new Error('Cropper is not initialized');
|
||||
|
||||
const croppedImage = await cropper.getCropperImage()!;
|
||||
const croppedSection = await cropper.getCropperSelection()!;
|
||||
|
||||
// 拡大率を計算し、(ほぼ)元の大きさに戻す
|
||||
const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth;
|
||||
const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate;
|
||||
|
||||
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
|
||||
croppedCanvas?.toBlob(blob => {
|
||||
const croppedCanvas = await croppedSection.$toCanvas({ width: widthToRender });
|
||||
croppedCanvas.toBlob(blob => {
|
||||
if (!blob) return;
|
||||
res(blob);
|
||||
});
|
||||
|
@ -74,25 +76,27 @@ const ok = async () => {
|
|||
const f = await promise;
|
||||
|
||||
emit('ok', f);
|
||||
dialogEl.value!.close();
|
||||
};
|
||||
if (dialogEl.value != null) dialogEl.value.close();
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
dialogEl.value!.close();
|
||||
};
|
||||
if (dialogEl.value != null) dialogEl.value.close();
|
||||
}
|
||||
|
||||
const onImageLoad = () => {
|
||||
function onImageLoad() {
|
||||
loading.value = false;
|
||||
|
||||
if (cropper) {
|
||||
cropper.getCropperImage()!.$center('contain');
|
||||
cropper.getCropperSelection()!.$center();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
cropper = new Cropper(imgEl.value!, {
|
||||
if (imgEl.value == null) return; // TSを黙らすため
|
||||
|
||||
cropper = new Cropper(imgEl.value, {
|
||||
});
|
||||
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
|
@ -104,16 +108,22 @@ onMounted(() => {
|
|||
selection.outlined = true;
|
||||
|
||||
window.setTimeout(() => {
|
||||
cropper!.getCropperImage()!.$center('contain');
|
||||
if (cropper == null) return;
|
||||
cropper.getCropperImage()!.$center('contain');
|
||||
selection.$center();
|
||||
}, 100);
|
||||
|
||||
// モーダルオープンアニメーションが終わったあとで再度調整
|
||||
window.setTimeout(() => {
|
||||
cropper!.getCropperImage()!.$center('contain');
|
||||
if (cropper == null) return;
|
||||
cropper.getCropperImage()!.$center('contain');
|
||||
selection.$center();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
URL.revokeObjectURL(imgUrl);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
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 { file } from '../../.storybook/fakes.js';
|
||||
import MkCwButton from './MkCwButton.vue';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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 type { StoryObj } from '@storybook/vue3';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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 { onBeforeUnmount } from 'vue';
|
||||
import MkDonation from './MkDonation.vue';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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 MkDrive_file from './MkDrive.file.vue';
|
||||
import { file } from '../../.storybook/fakes.js';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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 { http, HttpResponse } from 'msw';
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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 { http, HttpResponse } from 'msw';
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
|
|
@ -152,11 +152,12 @@ import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
|
|||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
|
||||
initialFolder?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id'] | null;
|
||||
type?: string;
|
||||
multiple?: boolean;
|
||||
select?: 'file' | 'folder' | null;
|
||||
}>(), {
|
||||
initialFolder: null,
|
||||
multiple: false,
|
||||
select: null,
|
||||
});
|
||||
|
@ -293,7 +294,7 @@ function onDragleave() {
|
|||
draghover.value = false;
|
||||
}
|
||||
|
||||
function onDrop(ev: DragEvent) {
|
||||
function onDrop(ev: DragEvent): void | boolean {
|
||||
draghover.value = false;
|
||||
|
||||
if (!ev.dataTransfer) return;
|
||||
|
|
|
@ -39,13 +39,13 @@ withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', r?: Misskey.entities.DriveFolder[]): void;
|
||||
(ev: 'done', r?: (Misskey.entities.DriveFolder | null)[]): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const selected = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const selected = ref<(Misskey.entities.DriveFolder | null)[]>([]);
|
||||
|
||||
function ok() {
|
||||
emit('done', selected.value);
|
||||
|
@ -57,7 +57,7 @@ function cancel() {
|
|||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function onChangeSelection(v: Misskey.entities.DriveFolder[]) {
|
||||
function onChangeSelection(v: (Misskey.entities.DriveFolder | null)[]) {
|
||||
selected.value = v;
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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 type { StoryObj } from '@storybook/vue3';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
@ -152,7 +152,7 @@ const props = withDefaults(defineProps<{
|
|||
asDrawer?: boolean;
|
||||
asWindow?: boolean;
|
||||
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
||||
targetNote?: Misskey.entities.Note;
|
||||
targetNote?: Misskey.entities.Note | null;
|
||||
}>(), {
|
||||
showPinned: true,
|
||||
});
|
||||
|
|
|
@ -44,11 +44,11 @@ import { prefer } from '@/preferences.js';
|
|||
|
||||
const props = withDefaults(defineProps<{
|
||||
manualShowing?: boolean | null;
|
||||
anchorElement?: HTMLElement;
|
||||
anchorElement?: HTMLElement | null;
|
||||
showPinned?: boolean;
|
||||
pinnedEmojis?: string[],
|
||||
asReactionPicker?: boolean;
|
||||
targetNote?: Misskey.entities.Note;
|
||||
targetNote?: Misskey.entities.Note | null;
|
||||
choseAndClose?: boolean;
|
||||
}>(), {
|
||||
manualShowing: null,
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Mfm class="summaryMfm" :text="flash.summary" :plain="true" :nowrap="true"/>
|
||||
</p>
|
||||
<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>
|
||||
</footer>
|
||||
</article>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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 MkImgPreviewDialog from './MkImgPreviewDialog.vue';
|
||||
export const Default = {
|
||||
|
|
|
@ -34,9 +34,10 @@ import { deviceKind } from '@/utility/device-kind.js';
|
|||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
anchorElement?: HTMLElement;
|
||||
anchorElement?: HTMLElement | null;
|
||||
anchor?: { x: string; y: string; };
|
||||
}>(), {
|
||||
anchorElement: null,
|
||||
anchor: () => ({ x: 'right', y: 'center' }),
|
||||
});
|
||||
|
||||
|
|
|
@ -39,10 +39,12 @@ const el = ref<HTMLElement | { $el: HTMLElement }>();
|
|||
|
||||
if (isEnabledUrlPreview.value) {
|
||||
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')), {
|
||||
showing,
|
||||
url: props.url,
|
||||
anchorElement: el.value instanceof HTMLElement ? el.value : el.value?.$el,
|
||||
anchorElement: anchorElement,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
|
|
@ -94,6 +94,8 @@ async function calcAspectRatio() {
|
|||
onMounted(() => {
|
||||
calcAspectRatio();
|
||||
|
||||
if (gallery.value == null) return; // TSを黙らすため
|
||||
|
||||
lightbox = new PhotoSwipeLightbox({
|
||||
dataSource: props.mediaList
|
||||
.filter(media => {
|
||||
|
|
|
@ -91,7 +91,7 @@ const emit = defineEmits<{
|
|||
(ev: 'opened'): void;
|
||||
(ev: 'click'): void;
|
||||
(ev: 'esc'): void;
|
||||
(ev: 'close'): void;
|
||||
(ev: 'close'): void; // TODO: (refactor) closing に改名する
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
|
@ -148,7 +148,6 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
|
|||
useSendAnime.value = true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
if (props.anchorElement) props.anchorElement.style.pointerEvents = 'auto';
|
||||
showing.value = false;
|
||||
emit('close');
|
||||
|
@ -319,7 +318,6 @@ const alignObserver = new ResizeObserver((entries, observer) => {
|
|||
onMounted(() => {
|
||||
watch(() => props.anchorElement, async () => {
|
||||
if (props.anchorElement) {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
props.anchorElement.style.pointerEvents = 'none';
|
||||
}
|
||||
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.anchorElement) != null);
|
||||
|
|
|
@ -654,7 +654,7 @@ function showRenoteMenu(): void {
|
|||
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
|
||||
{ type: 'divider' },
|
||||
getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
|
||||
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
||||
...(($i?.isModerator || $i?.isAdmin) ? [getUnrenote()] : []),
|
||||
], renoteTime.value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -392,6 +392,9 @@ const reactionsPaginator = markRaw(new Paginator('notes/reactions', {
|
|||
}));
|
||||
|
||||
useTooltip(renoteButton, async (showing) => {
|
||||
const anchorElement = renoteButton.value;
|
||||
if (anchorElement == null) return;
|
||||
|
||||
const renotes = await misskeyApi('notes/renotes', {
|
||||
noteId: appearNote.id,
|
||||
limit: 11,
|
||||
|
@ -405,7 +408,7 @@ useTooltip(renoteButton, async (showing) => {
|
|||
showing,
|
||||
users,
|
||||
count: appearNote.renoteCount,
|
||||
anchorElement: renoteButton.value,
|
||||
anchorElement: anchorElement,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
|
|
@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<div :class="$style.root" class="_forceShrinkSpacer">
|
||||
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :key="reloadCount" :router="windowRouter"/>
|
||||
<RouterView v-else :key="reloadCount" :router="windowRouter"/>
|
||||
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :key="reloadCount.toString() + ':stacking'" :router="windowRouter"/>
|
||||
<RouterView v-else :key="reloadCount.toString() + ':non-stacking'" :router="windowRouter"/>
|
||||
</div>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
@ -58,20 +58,15 @@ const windowRouter = createRouter(props.initialPath);
|
|||
|
||||
const pageMetadata = ref<null | PageMetadata>(null);
|
||||
const windowEl = useTemplateRef('windowEl');
|
||||
const history = ref<{ path: string; }[]>([{
|
||||
const _history_ = ref<{ path: string; }[]>([{
|
||||
path: windowRouter.getCurrentFullPath(),
|
||||
}]);
|
||||
const buttonsLeft = computed(() => {
|
||||
const buttons: Record<string, unknown>[] = [];
|
||||
|
||||
if (history.value.length > 1) {
|
||||
buttons.push({
|
||||
return _history_.value.length > 1 ? [{
|
||||
icon: 'ti ti-arrow-left',
|
||||
title: i18n.ts.goBack,
|
||||
onClick: back,
|
||||
});
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}] : [];
|
||||
});
|
||||
const buttonsRight = computed(() => {
|
||||
const buttons = [{
|
||||
|
@ -97,12 +92,12 @@ function getSearchMarker(path: string) {
|
|||
const searchMarkerId = ref<string | null>(getSearchMarker(props.initialPath));
|
||||
|
||||
windowRouter.addListener('push', ctx => {
|
||||
history.value.push({ path: ctx.fullPath });
|
||||
_history_.value.push({ path: ctx.fullPath });
|
||||
});
|
||||
|
||||
windowRouter.addListener('replace', ctx => {
|
||||
history.value.pop();
|
||||
history.value.push({ path: ctx.fullPath });
|
||||
_history_.value.pop();
|
||||
_history_.value.push({ path: ctx.fullPath });
|
||||
});
|
||||
|
||||
windowRouter.addListener('change', ctx => {
|
||||
|
@ -150,8 +145,8 @@ const contextmenu = computed(() => ([{
|
|||
}]));
|
||||
|
||||
function back() {
|
||||
history.value.pop();
|
||||
windowRouter.replaceByPath(history.value.at(-1)!.path);
|
||||
_history_.value.pop();
|
||||
windowRouter.replaceByPath(_history_.value.at(-1)!.path);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
|
|
|
@ -57,7 +57,7 @@ async function _close() {
|
|||
modal.value?.close();
|
||||
}
|
||||
|
||||
function onEsc(ev: KeyboardEvent) {
|
||||
function onEsc() {
|
||||
_close();
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ function subscribe() {
|
|||
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
|
||||
return promiseDialog(registration.value.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
|
||||
applicationServerKey: urlBase64ToBase64(instance.swPublickey),
|
||||
})
|
||||
.then(async 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
|
||||
*/
|
||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
function urlBase64ToBase64(base64String: string): string {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
return base64;
|
||||
}
|
||||
|
||||
if (navigator.serviceWorker == null) {
|
||||
|
|
|
@ -58,18 +58,22 @@ const emit = defineEmits<{
|
|||
const buttonEl = useTemplateRef('buttonEl');
|
||||
|
||||
const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
|
||||
const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
|
||||
|
||||
const canToggle = computed(() => {
|
||||
const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction);
|
||||
|
||||
// TODO
|
||||
//return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
|
||||
return !props.reaction.match(/@\w/) && $i && emoji.value;
|
||||
//return !props.reaction.match(/@\w/) && $i && emoji && checkReactionPermissions($i, props.note, emoji);
|
||||
return !props.reaction.match(/@\w/) && $i && emoji;
|
||||
});
|
||||
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
||||
const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.');
|
||||
|
||||
async function toggleReaction() {
|
||||
if (!canToggle.value) return;
|
||||
if ($i == null) return;
|
||||
|
||||
const me = $i;
|
||||
|
||||
const oldReaction = props.myReaction;
|
||||
if (oldReaction) {
|
||||
|
@ -93,7 +97,7 @@ async function toggleReaction() {
|
|||
noteId: props.noteId,
|
||||
}).then(() => {
|
||||
noteEvents.emit(`unreacted:${props.noteId}`, {
|
||||
userId: $i!.id,
|
||||
userId: me.id,
|
||||
reaction: oldReaction,
|
||||
});
|
||||
if (oldReaction !== props.reaction) {
|
||||
|
@ -101,10 +105,12 @@ async function toggleReaction() {
|
|||
noteId: props.noteId,
|
||||
reaction: props.reaction,
|
||||
}).then(() => {
|
||||
const emoji = customEmojisMap.get(emojiName.value);
|
||||
if (emoji == null) return;
|
||||
noteEvents.emit(`reacted:${props.noteId}`, {
|
||||
userId: $i!.id,
|
||||
userId: me.id,
|
||||
reaction: props.reaction,
|
||||
emoji: emoji.value,
|
||||
emoji: emoji,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -131,10 +137,13 @@ async function toggleReaction() {
|
|||
noteId: props.noteId,
|
||||
reaction: props.reaction,
|
||||
}).then(() => {
|
||||
const emoji = customEmojisMap.get(emojiName.value);
|
||||
if (emoji == null) return;
|
||||
|
||||
noteEvents.emit(`reacted:${props.noteId}`, {
|
||||
userId: $i!.id,
|
||||
userId: me.id,
|
||||
reaction: props.reaction,
|
||||
emoji: emoji.value,
|
||||
emoji: emoji,
|
||||
});
|
||||
});
|
||||
// TODO: 上位コンポーネントでやる
|
||||
|
@ -217,6 +226,8 @@ onMounted(() => {
|
|||
|
||||
if (!mock) {
|
||||
useTooltip(buttonEl, async (showing) => {
|
||||
if (buttonEl.value == null) return;
|
||||
|
||||
const reactions = await misskeyApiGet('notes/reactions', {
|
||||
noteId: props.noteId,
|
||||
type: props.reaction,
|
||||
|
|
|
@ -105,9 +105,7 @@ async function addRole() {
|
|||
.map(r => ({ text: r.name, value: r }));
|
||||
|
||||
const { canceled, result: role } = await os.select({ items });
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
if (canceled || role == null) return;
|
||||
|
||||
selectedRoleIds.value.push(role.id);
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkSwitch v-if="q_federation === 'yes'" v-model="q_remoteContentsCleaning">
|
||||
<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>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
|
|
@ -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.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.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha"/>
|
||||
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha" :sitekey="null"/>
|
||||
</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>
|
||||
|
|
|
@ -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 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() {
|
||||
if (stream == null) return;
|
||||
if (props.src === 'antenna') {
|
||||
if (props.antenna == null) return;
|
||||
connection = stream.useChannel('antenna', {
|
||||
connections.antenna = stream.useChannel('antenna', {
|
||||
antennaId: props.antenna,
|
||||
});
|
||||
connections.antenna.on('note', prepend);
|
||||
} else if (props.src === 'home') {
|
||||
connection = stream.useChannel('homeTimeline', {
|
||||
connections.homeTimeline = stream.useChannel('homeTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
connection2 = stream.useChannel('main');
|
||||
connections.main = stream.useChannel('main');
|
||||
connections.homeTimeline.on('note', prepend);
|
||||
} else if (props.src === 'local') {
|
||||
connection = stream.useChannel('localTimeline', {
|
||||
connections.localTimeline = stream.useChannel('localTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
connections.localTimeline.on('note', prepend);
|
||||
} else if (props.src === 'social') {
|
||||
connection = stream.useChannel('hybridTimeline', {
|
||||
connections.hybridTimeline = stream.useChannel('hybridTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
connections.hybridTimeline.on('note', prepend);
|
||||
} else if (props.src === 'global') {
|
||||
connection = stream.useChannel('globalTimeline', {
|
||||
connections.globalTimeline = stream.useChannel('globalTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
connections.globalTimeline.on('note', prepend);
|
||||
} else if (props.src === 'mentions') {
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', prepend);
|
||||
connections.main = stream.useChannel('main');
|
||||
connections.main.on('mention', prepend);
|
||||
} else if (props.src === 'directs') {
|
||||
const onNote = note => {
|
||||
if (note.visibility === 'specified') {
|
||||
prepend(note);
|
||||
}
|
||||
};
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', onNote);
|
||||
connections.main = stream.useChannel('main');
|
||||
connections.main.on('mention', onNote);
|
||||
} else if (props.src === 'list') {
|
||||
if (props.list == null) return;
|
||||
connection = stream.useChannel('userList', {
|
||||
connections.userList = stream.useChannel('userList', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
});
|
||||
connections.userList.on('note', prepend);
|
||||
} else if (props.src === 'channel') {
|
||||
if (props.channel == null) return;
|
||||
connection = stream.useChannel('channel', {
|
||||
connections.channel = stream.useChannel('channel', {
|
||||
channelId: props.channel,
|
||||
});
|
||||
connections.channel.on('note', prepend);
|
||||
} else if (props.src === 'role') {
|
||||
if (props.role == null) return;
|
||||
connection = stream.useChannel('roleTimeline', {
|
||||
connections.roleTimeline = stream.useChannel('roleTimeline', {
|
||||
roleId: props.role,
|
||||
});
|
||||
connections.roleTimeline.on('note', prepend);
|
||||
}
|
||||
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
|
||||
}
|
||||
|
||||
function disconnectChannel() {
|
||||
if (connection) connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
for (const key in connections) {
|
||||
const conn = connections[key as keyof typeof connections];
|
||||
if (conn != null) {
|
||||
conn.dispose();
|
||||
connections[key as keyof typeof connections] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (store.s.realtimeMode) {
|
||||
|
|
|
@ -59,7 +59,7 @@ import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-sep
|
|||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const props = defineProps<{
|
||||
excludeTypes?: typeof notificationTypes[number][];
|
||||
excludeTypes?: typeof notificationTypes[number][] | null;
|
||||
}>();
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
|
|
|
@ -39,17 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
export type Tab = {
|
||||
key: string;
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
} & (
|
||||
| {
|
||||
iconOnly?: false;
|
||||
iconOnly?: boolean;
|
||||
title: string;
|
||||
icon?: string;
|
||||
}
|
||||
| {
|
||||
iconOnly: true;
|
||||
icon: string;
|
||||
}
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { action } from 'storybook/actions';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkTagItem from './MkTagItem.vue';
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ export type DefaultStoredWidget = {
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
@ -58,7 +59,6 @@ import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
|
|||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
@ -81,7 +81,7 @@ const emit = defineEmits<{
|
|||
(ev: 'updateWidgets', widgets: Widget[]): void;
|
||||
(ev: 'addWidget', 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;
|
||||
}>();
|
||||
|
||||
|
@ -104,7 +104,7 @@ const addWidget = () => {
|
|||
const removeWidget = (widget) => {
|
||||
emit('removeWidget', widget);
|
||||
};
|
||||
const updateWidget = (id, data) => {
|
||||
const updateWidget = (id: Widget['id'], data: Widget['data']) => {
|
||||
emit('updateWidget', { id, data });
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading/>
|
||||
</div>
|
||||
<div v-else-if="resolved">
|
||||
<slot :result="result"></slot>
|
||||
<slot :result="result as T"></slot>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div :class="$style.error">
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkAd from './MkAd.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const common = {
|
||||
|
@ -68,7 +67,7 @@ const common = {
|
|||
await expect(imgAgain).toBeInTheDocument();
|
||||
},
|
||||
args: {
|
||||
prefer: [],
|
||||
preferForms: [],
|
||||
specify: {
|
||||
id: 'someadid',
|
||||
ratio: 1,
|
||||
|
|
|
@ -52,7 +52,7 @@ import { prefer } from '@/preferences.js';
|
|||
type Ad = (typeof instance)['ads'][number];
|
||||
|
||||
const props = defineProps<{
|
||||
preferForms: string[];
|
||||
preferForms?: string[];
|
||||
specify?: Ad;
|
||||
}>();
|
||||
|
||||
|
@ -71,7 +71,7 @@ const choseAd = (): Ad | null => {
|
|||
ratio: 0,
|
||||
} : 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) {
|
||||
ads = allAds.filter(ad => ad.place === 'square');
|
||||
|
|
|
@ -84,7 +84,6 @@ const bound = computed(() => props.link
|
|||
: {});
|
||||
|
||||
const url = computed(() => {
|
||||
if (props.user.avatarUrl == null) return null;
|
||||
if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
|
||||
return props.user.avatarUrl;
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* 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 type { StoryObj } from '@storybook/vue3';
|
||||
import MkError from './MkError.vue';
|
||||
|
|
|
@ -59,6 +59,7 @@ export const Icon = {
|
|||
{
|
||||
...OneTab.args.tabs[0],
|
||||
icon: 'ti ti-home',
|
||||
title: 'Home',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -71,6 +72,7 @@ export const IconOnly = {
|
|||
{
|
||||
key: Icon.args.tabs[0].key,
|
||||
icon: Icon.args.tabs[0].icon,
|
||||
title: Icon.args.tabs[0].title,
|
||||
iconOnly: true,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -39,17 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
export type Tab = {
|
||||
key: string;
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
} & (
|
||||
| {
|
||||
iconOnly?: false;
|
||||
iconOnly?: boolean;
|
||||
title: string;
|
||||
icon?: string;
|
||||
}
|
||||
| {
|
||||
iconOnly: true;
|
||||
icon: string;
|
||||
}
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -59,7 +52,7 @@ import { prefer } from '@/preferences.js';
|
|||
const props = withDefaults(defineProps<{
|
||||
tabs?: Tab[];
|
||||
tab?: string;
|
||||
rootEl?: HTMLElement;
|
||||
rootEl?: HTMLElement | null;
|
||||
}>(), {
|
||||
tabs: () => ([] as Tab[]),
|
||||
});
|
||||
|
|
|
@ -30,19 +30,21 @@ const props = defineProps<{
|
|||
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');
|
||||
}
|
||||
|
||||
const router = _router;
|
||||
|
||||
const viewId = randomId();
|
||||
provide(DI.viewId, viewId);
|
||||
|
||||
const currentDepth = inject(DI.routerCurrentDepth, 0);
|
||||
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 currentPageProps = ref(current.props);
|
||||
let currentRoutePath = current.route.path;
|
||||
|
@ -52,14 +54,10 @@ router.useListener('change', ({ resolved }) => {
|
|||
if (resolved == null || 'redirect' in resolved.route) return;
|
||||
if (resolved.route.path === currentRoutePath && deepEqual(resolved.props, currentPageProps.value)) return;
|
||||
|
||||
function _() {
|
||||
currentPageComponent.value = resolved.route.component;
|
||||
currentPageProps.value = resolved.props;
|
||||
key.value = router.getCurrentFullPath();
|
||||
currentRoutePath = resolved.route.path;
|
||||
}
|
||||
|
||||
_();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ router.useListener('change', ({ resolved }) => {
|
|||
const fullPath = router.getCurrentFullPath();
|
||||
|
||||
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) {
|
||||
newTabs.push(tab);
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* 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 { ref } from 'vue';
|
||||
import { commonHandlers } from '../../../.storybook/mocks.js';
|
||||
|
|
|
@ -684,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;
|
||||
width?: number;
|
||||
onClosing?: () => void;
|
||||
|
@ -696,7 +696,7 @@ export function popupMenu(items: MenuItem[], anchorElement?: HTMLElement | Event
|
|||
let returnFocusTo = getHTMLElementOrNull(anchorElement) ?? getHTMLElementOrNull(window.document.activeElement);
|
||||
return new Promise(resolve => nextTick(() => {
|
||||
const { dispose } = popup(MkPopupMenu, {
|
||||
items,
|
||||
items: items.filter(x => x != null),
|
||||
anchorElement,
|
||||
width: options?.width,
|
||||
align: options?.align,
|
||||
|
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</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>
|
||||
<div :class="$style.emojis">
|
||||
<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 q = ref('');
|
||||
const searchEmojis = ref<Misskey.entities.EmojiSimple[]>(null);
|
||||
const searchEmojis = ref<Misskey.entities.EmojiSimple[] | null>(null);
|
||||
const selectedTags = ref(new Set());
|
||||
|
||||
function search() {
|
||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_m">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
|
||||
<div style="overflow: clip;">
|
||||
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
|
||||
<img :src="instance.iconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
|
||||
<div :class="$style.bannerName">
|
||||
<b>{{ instance.name ?? host }}</b>
|
||||
</div>
|
||||
|
|
|
@ -16,9 +16,11 @@ import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
|
|||
import MkAchievements from '@/components/MkAchievements.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
let timer: number | null;
|
||||
|
||||
function viewAchievements3min() {
|
||||
|
|
|
@ -172,7 +172,7 @@ const headerTabs = computed(() => [{
|
|||
key: 'raw',
|
||||
title: 'Raw data',
|
||||
icon: 'ti ti-code',
|
||||
}]);
|
||||
}].filter(x => x != null));
|
||||
|
||||
definePage(() => ({
|
||||
title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue