Merge branch 'develop' into fix-13913

This commit is contained in:
かっこかり 2025-08-26 13:34:14 +09:00 committed by GitHub
commit b5392d4731
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
121 changed files with 2549 additions and 2016 deletions

View File

@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0

View File

@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout head
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
- name: Setup Node.js
uses: actions/setup-node@v4.4.0
with:

View File

@ -18,7 +18,7 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps:
- name: checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
with:
submodules: true
persist-credentials: false
@ -66,7 +66,7 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps:
- name: checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
with:
submodules: true
persist-credentials: false

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
- name: Check version
run: |
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
- name: Check
run: |
counter=0

View File

@ -10,7 +10,7 @@ jobs:
check_copyright_year:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
- run: |
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
echo "Please change copyright year!"

View File

@ -28,7 +28,7 @@ jobs:
wait_time: ${{ steps.get-wait-time.outputs.wait_time }}
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
- name: Check allowed users
id: check-allowed-users

View File

@ -27,7 +27,7 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub

View File

@ -32,7 +32,7 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta

View File

@ -15,7 +15,7 @@ jobs:
DOCKER_CONTENT_TRUST: 1
DOCKLE_VERSION: 0.4.14
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"

View File

@ -25,7 +25,7 @@ jobs:
ref: refs/pull/${{ github.event.number }}/merge
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
ref: ${{ matrix.ref }}
submodules: true

View File

@ -36,7 +36,7 @@ jobs:
pnpm_install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
fetch-depth: 0
submodules: true
@ -69,7 +69,7 @@ jobs:
eslint-cache-version: v1
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
fetch-depth: 0
submodules: true
@ -81,7 +81,7 @@ jobs:
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Restore eslint cache
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.4
with:
path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
@ -99,7 +99,7 @@ jobs:
- sw
- misskey-js
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
fetch-depth: 0
submodules: true

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
fetch-depth: 0
submodules: true

View File

@ -16,7 +16,7 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
submodules: true
- name: Setup pnpm

View File

@ -22,12 +22,12 @@ jobs:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
if: github.event_name != 'pull_request_target'
with:
fetch-depth: 0
submodules: true
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
if: github.event_name == 'pull_request_target'
with:
fetch-depth: 0

View File

@ -50,7 +50,7 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
submodules: true
- name: Setup pnpm
@ -129,7 +129,7 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
submodules: true
- name: Setup pnpm
@ -173,7 +173,7 @@ jobs:
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
submodules: true
- name: Setup pnpm

View File

@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
submodules: true
- name: Setup pnpm
@ -76,7 +76,7 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150

View File

@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.3.0
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
submodules: true
- name: Setup pnpm

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.3.0
with:
submodules: true
- name: Setup pnpm

View File

@ -54,6 +54,8 @@
- Fix: 複数のメンションを1行に記述した場合に、サジェストが正しく表示されない問題を修正
- Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正
- Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正
- Fix: 照会ダイアログでap/showでローカルユーザーを解決した際@username@nullに飛ばされる問題を修正
- Fix: アイコンのデコレーションを付ける際にデコレーションが表示されなくなる問題を修正
- Fix: CtrlキーCommandキーを押下しながらリンクをクリックすると新しいタブで開くように
### Server
@ -68,6 +70,7 @@
- Fix: `notes/mentions` で場合によっては並び順が正しく返されない問題を修正
- Fix: SystemWebhook設定でsecretを空に出来ない問題を修正
- Fix: 削除されたユーザーがチャットメッセージにリアクションしている場合`chat/history`などでエラーになる問題を修正
- Fix: Pageのアイキャッチ画像をドライブから消してもPageごと消えないように
## 2025.7.0

View File

@ -1245,6 +1245,7 @@ releaseToRefresh: "Release to refresh"
refreshing: "Refreshing..."
pullDownToRefresh: "Pull down to refresh"
useGroupedNotifications: "Display grouped notifications"
emailVerificationFailedError: "A problem occurred while verifying your email address. The link may have expired."
cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided."
doReaction: "Add reaction"
code: "Code"
@ -1375,6 +1376,7 @@ safeModeEnabled: "Safe mode is enabled"
pluginsAreDisabledBecauseSafeMode: "All plugins are disabled because safe mode is enabled."
customCssIsDisabledBecauseSafeMode: "Custom CSS is not applied because safe mode is enabled."
themeIsDefaultBecauseSafeMode: "While safe mode is active, the default theme is used. Disabling safe mode will revert these changes."
thankYouForTestingBeta: "Thank you for helping us test the beta version!"
_order:
newest: "Newest First"
oldest: "Oldest First"
@ -1664,6 +1666,9 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor_description2: "Unconditionally publishing all content on the server to the Internet, including remote content received by the server is risky. This is especially important for guests who are unaware of the distributed nature of the content, as they may mistakenly believe that even remote content is content created by users on the server."
restartServerSetupWizardConfirm_title: "Restart server setup wizard?"
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"

View File

@ -1054,6 +1054,7 @@ permissionDeniedError: "Operación denegada"
permissionDeniedErrorDescription: "Esta cuenta no tiene permisos para hacer esa acción."
preset: "Predefinido"
selectFromPresets: "Escoger desde predefinidos"
custom: "Personalizado"
achievements: "Logros"
gotInvalidResponseError: "Respuesta del servidor inválida"
gotInvalidResponseErrorDescription: "Puede que el servidor esté caído o en mantenimiento. Favor de intentar más tarde"
@ -1244,6 +1245,7 @@ releaseToRefresh: "Soltar para recargar"
refreshing: "Recargando..."
pullDownToRefresh: "Tira hacia abajo para recargar"
useGroupedNotifications: "Mostrar notificaciones agrupadas"
emailVerificationFailedError: "Se ha producido un error al confirmar tu dirección de correo electrónico. Es posible que el enlace haya caducado."
cwNotationRequired: "Si se ha activado \"ocultar contenido\", es necesario proporcionar una descripción."
doReaction: "Añadir reacción"
code: "Código"
@ -1374,6 +1376,7 @@ safeModeEnabled: "El modo seguro está activado"
pluginsAreDisabledBecauseSafeMode: "El modo seguro está activado, por lo que todos los plugins están desactivados."
customCssIsDisabledBecauseSafeMode: "El modo seguro está activado, por lo que no se aplica el CSS personalizado."
themeIsDefaultBecauseSafeMode: "Mientras el modo seguro esté activado, se utilizará el tema predeterminado. Cuando se desactive el modo seguro, se volverá al tema original."
thankYouForTestingBeta: "¡Gracias por tu colaboración en la prueba de la versión beta!"
_order:
newest: "Los más recientes primero"
oldest: "Los más antiguos primero"
@ -1663,6 +1666,9 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor_description2: "Publicar incondicionalmente todo el contenido del servidor en Internet, incluido el contenido remoto recibido por el servidor, es arriesgado. Esto es especialmente importante para los invitados que desconocen la naturaleza distribuida del contenido, ya que pueden creer erróneamente que incluso el contenido remoto es contenido creado por usuarios en el servidor."
restartServerSetupWizardConfirm_title: "¿Reiniciar el asistente de configuración del servidor?"
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"
_userGeneratedContentsVisibilityForVisitor:
all: "Todo es público."
localOnly: "Sólo se publica el contenido local, el remoto se mantiene privado"
@ -2272,6 +2278,7 @@ _time:
minute: "Minutos"
hour: "Horas"
day: "Días"
month: "Mes(es)"
_2fa:
alreadyRegistered: "Ya has completado la configuración."
registerTOTP: "Registrar aplicación autenticadora"

2
locales/index.d.ts vendored
View File

@ -6633,7 +6633,7 @@ export interface Locale extends ILocale {
/**
*
*/
"showActivityiesForVisitor": string;
"showActivitiesForVisitor": string;
"_userGeneratedContentsVisibilityForVisitor": {
/**
*

View File

@ -1685,7 +1685,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "現在の一部の設定はリセットされます。"
entrancePageStyle: "エントランスページのスタイル"
showTimelineForVisitor: "タイムラインを表示する"
showActivityiesForVisitor: "アクティビティを表示する"
showActivitiesForVisitor: "アクティビティを表示する"
_userGeneratedContentsVisibilityForVisitor:
all: "全て公開"

View File

@ -31,7 +31,7 @@ openInWindow: "Pencerede aç"
profile: "Profil"
timeline: "Pano"
noAccountDescription: "Bu kullanıcı henüz biyografisini yazmamış."
login: "Giriş Yap"
login: "Oturum Aç"
loggingIn: "Giriş Yapılıyor..."
logout: ıkış Yap"
signup: "Kaydol"
@ -310,7 +310,7 @@ agreeBelow: "Aşağıdakileri kabul ediyorum"
basicNotesBeforeCreateAccount: "Önemli notlar"
termsOfService: "Hizmet Şartları"
start: "Başla"
home: "Ana sayfa"
home: "Pano"
remoteUserCaution: "Bu kullanıcı uzak bir sunucudan geldiği için, gösterilen bilgiler eksik olabilir."
activity: "Etkinlik"
images: "Görseller"
@ -1054,6 +1054,7 @@ permissionDeniedError: "İşlem reddedildi"
permissionDeniedErrorDescription: "Bu hesap bu işlemi gerçekleştirmek için gerekli izne sahip değildir."
preset: "Ön ayar"
selectFromPresets: "Ön ayarlardan seçim yapın"
custom: "Özel"
achievements: "Başarılar"
gotInvalidResponseError: "Geçersiz sunucu yanıtı"
gotInvalidResponseErrorDescription: "Sunucu erişilemez durumda olabilir veya bakım çalışması yapılmaktadır. Lütfen daha sonra tekrar dene."
@ -1066,8 +1067,8 @@ collapseRenotesDescription: "Zaten yanıtladığın veya renote aldığın notla
internalServerError: "İç Sunucu Hatası"
internalServerErrorDescription: "Sunucu beklenmedik bir hatayla karşılaştı."
copyErrorInfo: "Hata ayrıntılarını kopyala"
joinThisServer: "Bu sunucuda kaydolun"
exploreOtherServers: "Başka bir sunucu arayın"
joinThisServer: "Kaydol"
exploreOtherServers: "Diğer sunucuları keşfet"
letsLookAtTimeline: "Pano'ya bir göz atın"
disableFederationConfirm: "Federasyonu cidden devre dışı bırakmak istiyor musun?"
disableFederationConfirmWarn: "Federasyondan ayrılsa bile, aksi belirtilmedikçe gönderiler herkese açık olmaya devam edecek. Genellikle bunu yapmanız gerekmez."
@ -1244,6 +1245,7 @@ releaseToRefresh: "Yenilemek için serbest bırak"
refreshing: "Yenileniyor..."
pullDownToRefresh: "Yenilemek için aşağı çekin"
useGroupedNotifications: "Gruplandırılmış bildirimleri göster"
emailVerificationFailedError: "E-posta adresi doğrulanırken bir sorun oluştu. Bağlantının geçerlilik süresi dolmuş olabilir."
cwNotationRequired: "“İçeriği gizle” seçeneği etkinleştirilirse, bir açıklama sağlanmalı."
doReaction: "Tepki ekle"
code: "Kod"
@ -1374,6 +1376,7 @@ safeModeEnabled: "Güvenli mod etkinleştirildi"
pluginsAreDisabledBecauseSafeMode: "Güvenli mod etkinleştirildiği için tüm eklentiler devre dışı bırakılmıştır."
customCssIsDisabledBecauseSafeMode: "Güvenli mod etkin olduğu için özel CSS uygulanmıyor."
themeIsDefaultBecauseSafeMode: "Güvenli mod etkinken, varsayılan tema kullanılır. Güvenli modu devre dışı bırakmak bu değişiklikleri geri alır."
thankYouForTestingBeta: "Beta sürümünü test ettiğin için teşekkür ederiz!"
_order:
newest: "Önce yeni"
oldest: "Önce eski"
@ -1622,7 +1625,7 @@ _initialTutorial:
_timelineDescription:
home: "Ana Pano'da, takip ettiğin hesapların notlarını görebilirsin."
local: "Yerel Pano'da, bu sunucudaki tüm kullanıcıların notlarını görebilirsin."
social: "Sosyal Pano, Ana Sayfa ve Yerel Pano'dan gelen notları görüntüler."
social: "Pano, Sosyal Pano ve Yerel Pano'dan gelen notları görüntüler."
global: "Global Pano'da, bağlı tüm sunuculardan gelen notları görebilirsin."
_serverRules:
description: "Kayıt öncesinde gösterilecek bir dizi kural. Hizmet Şartlarının özetini belirlemen önerilir."
@ -1663,6 +1666,9 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor_description2: "Sunucu tarafından alınan uzak içerik dahil olmak üzere sunucudaki tüm içeriği koşulsuz olarak İnternet'e yayınlamak risklidir. Bu, içeriğin dağıtılmış yapısından haberdar olmayan misafirler için özellikle önemlidir, çünkü onlar yanlışlıkla uzak içeriğin bile sunucudaki kullanıcılar tarafından oluşturulan içerik olduğunu düşünebilirler."
restartServerSetupWizardConfirm_title: "Sunucu kurulum sihirbazını yeniden başlatmak ister misin?"
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."
@ -2272,6 +2278,7 @@ _time:
minute: "Dakika(lar)"
hour: "Saat(ler)"
day: "Gün(ler)"
month: "Ay"
_2fa:
alreadyRegistered: "2fa kimlik doğrulama cihazını zaten kaydettin."
registerTOTP: "Kimlik doğrulama uygulamasını kaydet"
@ -2477,7 +2484,7 @@ _poll:
_visibility:
public: "Halka açık"
publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır."
home: "Ana sayfa"
home: "Pano"
homeDescription: "Yalnızca ana zaman çizelgesine gönder"
followers: "Takipçiler"
followersDescription: "Sadece takipçilerine görünür hale getir"
@ -2553,7 +2560,7 @@ _instanceCharts:
files: "Dosya sayısındaki fark"
filesTotal: "Toplam dosya sayısı"
_timelines:
home: "Ana Sayfa"
home: "Pano"
local: "Yerel"
social: "Sosyal"
global: "Global"
@ -2671,7 +2678,7 @@ _notification:
chatRoomInvitationReceived: "Sohbet odasına davet edildi"
achievementEarned: "Başarı kilidi açıldı"
exportCompleted: "İhracat işlemi tamamlandı."
login: "Giriş Yap"
login: "Oturum Aç"
createToken: "Erişim jetonu oluştur"
test: "Bildirim testi"
app: "Bağlı uygulamalardan gelen bildirimler"
@ -2708,7 +2715,7 @@ _deck:
main: "Ana"
widgets: "Widget'lar"
notifications: "Bildirimler"
tl: "Ana Sayfa"
tl: "Pano"
antenna: "Antenler"
list: "Liste"
channel: "Kanal"

View File

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2025.8.0-beta.3",
"version": "2025.8.0-beta.4",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.14.0",
"packageManager": "pnpm@10.15.0",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@ -53,8 +53,8 @@
"lodash": "4.17.21"
},
"dependencies": {
"cssnano": "7.1.0",
"esbuild": "0.25.8",
"cssnano": "7.1.1",
"esbuild": "0.25.9",
"execa": "9.6.0",
"fast-glob": "3.3.3",
"glob": "11.0.3",
@ -67,15 +67,15 @@
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/node": "22.17.1",
"@typescript-eslint/eslint-plugin": "8.39.0",
"@typescript-eslint/parser": "8.39.0",
"@types/node": "22.17.2",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"cross-env": "7.0.3",
"cypress": "14.5.4",
"eslint": "9.33.0",
"eslint": "9.34.0",
"globals": "16.3.0",
"ncp": "2.0.0",
"pnpm": "10.14.0",
"pnpm": "10.15.0",
"start-server-and-test": "2.0.13"
},
"optionalDependencies": {

View File

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NonCascadingPageEyeCatching1756062689648 {
name = 'NonCascadingPageEyeCatching1756062689648'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa"`);
await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa"`);
await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
}

View File

@ -39,17 +39,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.13.3",
"@swc/core-darwin-x64": "1.13.3",
"@swc/core-darwin-arm64": "1.13.4",
"@swc/core-darwin-x64": "1.13.4",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.13.3",
"@swc/core-linux-arm64-gnu": "1.13.3",
"@swc/core-linux-arm64-musl": "1.13.3",
"@swc/core-linux-x64-gnu": "1.13.3",
"@swc/core-linux-x64-musl": "1.13.3",
"@swc/core-win32-arm64-msvc": "1.13.3",
"@swc/core-win32-ia32-msvc": "1.13.3",
"@swc/core-win32-x64-msvc": "1.13.3",
"@swc/core-linux-arm-gnueabihf": "1.13.4",
"@swc/core-linux-arm64-gnu": "1.13.4",
"@swc/core-linux-arm64-musl": "1.13.4",
"@swc/core-linux-x64-gnu": "1.13.4",
"@swc/core-linux-x64-musl": "1.13.4",
"@swc/core-win32-arm64-msvc": "1.13.4",
"@swc/core-win32-ia32-msvc": "1.13.4",
"@swc/core-win32-x64-msvc": "1.13.4",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
@ -69,8 +69,8 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@aws-sdk/client-s3": "3.864.0",
"@aws-sdk/lib-storage": "3.864.0",
"@aws-sdk/client-s3": "3.873.0",
"@aws-sdk/lib-storage": "3.873.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
@ -93,7 +93,7 @@
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.8",
"@swc/core": "1.13.3",
"@swc/core": "1.13.4",
"@twemoji/parser": "16.0.0",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
@ -103,10 +103,10 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.56.9",
"bullmq": "5.58.1",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.5.0",
"chalk": "5.6.0",
"chalk-template": "1.1.0",
"chokidar": "4.0.3",
"cli-highlight": "2.1.11",
@ -114,7 +114,7 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "5.4.0",
"fastify": "5.5.0",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "19.6.0",
@ -135,7 +135,7 @@
"jsonld": "8.3.3",
"jsrsasign": "11.1.0",
"juice": "11.0.1",
"meilisearch": "0.51.0",
"meilisearch": "0.52.0",
"mfm-js": "0.25.0",
"microformats-parser": "2.0.4",
"mime-types": "2.1.35",
@ -151,7 +151,7 @@
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.4.0",
"otpauth": "9.4.1",
"parse5": "7.3.0",
"pg": "8.16.3",
"pkce-challenge": "4.1.0",
@ -177,10 +177,10 @@
"stringz": "2.1.0",
"systeminformation": "5.27.7",
"tinycolor2": "1.6.0",
"tmp": "0.2.4",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.25",
"typeorm": "0.3.26",
"typescript": "5.9.2",
"ulid": "2.4.0",
"vary": "1.1.2",
@ -191,7 +191,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.20",
"@sentry/vue": "9.45.0",
"@sentry/vue": "9.46.0",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.39",
"@types/accepts": "1.3.7",
@ -210,8 +210,8 @@
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.17.1",
"@types/nodemailer": "6.4.17",
"@types/node": "22.17.2",
"@types/nodemailer": "6.4.19",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
@ -231,8 +231,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.39.0",
"@typescript-eslint/parser": "8.39.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0",

View File

@ -69,7 +69,7 @@ export class MiPage {
public eyeCatchingImageId: MiDriveFile['id'] | null;
@ManyToOne(type => MiDriveFile, {
onDelete: 'CASCADE',
onDelete: 'SET NULL',
})
@JoinColumn()
public eyeCatchingImage: MiDriveFile | null;

View File

@ -16,7 +16,7 @@
"@rollup/pluginutils": "5.2.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.18",
"@vue/compiler-sfc": "3.5.19",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@ -26,16 +26,16 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.46.2",
"sass": "1.89.2",
"shiki": "3.9.1",
"rollup": "4.48.0",
"sass": "1.90.0",
"shiki": "3.11.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"uuid": "11.1.0",
"vite": "7.0.6",
"vue": "3.5.18"
"vite": "7.1.3",
"vue": "3.5.19"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.3",
@ -43,14 +43,14 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.17.0",
"@types/node": "22.17.2",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.18",
"@vue/runtime-core": "3.5.19",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"eslint-plugin-import": "2.32.0",
@ -59,14 +59,14 @@
"happy-dom": "18.0.1",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.10.4",
"msw": "2.10.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"start-server-and-test": "2.0.12",
"tsx": "4.20.3",
"start-server-and-test": "2.0.13",
"tsx": "4.20.4",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.0.5",
"vue-component-type-helpers": "3.0.6",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.5"
"vue-tsc": "3.0.6"
}
}

View File

@ -21,10 +21,10 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.17.0",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"esbuild": "0.25.8",
"@types/node": "22.17.2",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"esbuild": "0.25.9",
"eslint-plugin-vue": "10.4.0",
"nodemon": "3.1.10",
"typescript": "5.9.2",
@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.5.18"
"vue": "3.5.19"
}
}

View File

@ -24,14 +24,14 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0",
"@sentry/vue": "10.0.0",
"@sentry/vue": "10.5.0",
"@syuilo/aiscript": "1.1.0",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.18",
"@vue/compiler-sfc": "3.5.19",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.16",
"analytics": "0.8.19",
"astring": "1.9.0",
"broadcast-channel": "7.1.0",
"buraha": "0.0.1",
@ -55,7 +55,7 @@
"ios-haptics": "0.1.0",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"magic-string": "0.30.17",
"magic-string": "0.30.18",
"matter-js": "0.20.0",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
@ -63,10 +63,10 @@
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
"rollup": "4.46.2",
"rollup": "4.48.0",
"sanitize-html": "2.17.0",
"sass": "1.89.2",
"shiki": "3.9.1",
"sass": "1.90.0",
"shiki": "3.11.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.179.1",
@ -76,8 +76,8 @@
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"v-code-diff": "1.13.1",
"vite": "7.0.6",
"vue": "3.5.18",
"vite": "7.1.3",
"vue": "3.5.19",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
@ -86,7 +86,7 @@
"@storybook/addon-actions": "9.0.8",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "9.1.0",
"@storybook/addon-links": "9.1.3",
"@storybook/addon-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14",
@ -94,34 +94,34 @@
"@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14",
"@storybook/react": "9.1.0",
"@storybook/react-vite": "9.1.0",
"@storybook/react": "9.1.3",
"@storybook/react-vite": "9.1.3",
"@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14",
"@storybook/vue3": "9.1.0",
"@storybook/vue3-vite": "9.1.0",
"@storybook/vue3": "9.1.3",
"@storybook/vue3-vite": "9.1.3",
"@tabler/icons-webfont": "3.34.1",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8",
"@types/matter-js": "0.19.8",
"@types/matter-js": "0.20.0",
"@types/micromatch": "4.0.9",
"@types/node": "22.17.0",
"@types/node": "22.17.2",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.18",
"@vue/runtime-core": "3.5.18",
"@vue/compiler-core": "3.5.19",
"@vue/runtime-core": "3.5.19",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"cypress": "14.5.3",
"cypress": "14.5.4",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.4.0",
"fast-glob": "3.3.3",
@ -129,22 +129,22 @@
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.3",
"msw": "2.10.4",
"msw": "2.10.5",
"msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.12",
"storybook": "9.1.0",
"start-server-and-test": "2.0.13",
"storybook": "9.1.3",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.20.3",
"tsx": "4.20.4",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.0.5",
"vue-component-type-helpers": "3.0.6",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.5"
"vue-tsc": "3.0.6"
}
}

View File

@ -71,7 +71,7 @@ const props = withDefaults(defineProps<{
const achievements = ref<Misskey.entities.UsersAchievementsResponse | null>(null);
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 => {
achievements.value = [];
for (const t of ACHIEVEMENT_TYPES) {
@ -84,11 +84,11 @@ function fetch() {
function clickHere() {
claimAchievement('clickedClickHere');
fetch();
_fetch_();
}
onMounted(() => {
fetch();
_fetch_();
});
</script>

View File

@ -44,7 +44,7 @@ function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
if (shaderProgram == null || vertexShader == null || fragmentShader == null) return null;
if (vertexShader == null || fragmentShader == null) return null;
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
@ -71,8 +71,10 @@ onMounted(() => {
canvas.width = width;
canvas.height = height;
const gl = canvas.getContext('webgl', { premultipliedAlpha: true });
if (gl == null) return;
const maybeGl = canvas.getContext('webgl', { premultipliedAlpha: true });
if (maybeGl == null) return;
const gl = maybeGl;
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT);
@ -229,8 +231,8 @@ onMounted(() => {
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);
if (isChromatic()) {
gl!.uniform1f(u_time, 0);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
gl.uniform1f(u_time, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
} else {
function render(timeStamp: number) {
let sizeChanged = false;
@ -249,8 +251,8 @@ onMounted(() => {
gl.viewport(0, 0, width, height);
}
gl!.uniform1f(u_time, timeStamp);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
gl.uniform1f(u_time, timeStamp);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
handle = window.requestAnimationFrame(render);
}
@ -263,6 +265,8 @@ onUnmounted(() => {
if (handle) {
window.cancelAnimationFrame(handle);
}
// TODO: WebGL
});
</script>

View File

@ -363,7 +363,7 @@ function onDrop(ev: DragEvent) {
//#endregion
}
function onUploadRequested(files: File[], folder: Misskey.entities.DriveFolder | null) {
function onUploadRequested(files: File[], folder?: Misskey.entities.DriveFolder | null) {
os.launchUploader(files, {
folderId: folder?.id ?? null,
});

View File

@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:forceBlurhash="forceBlurhash"
/>
<img
v-else-if="isThumbnailAvailable"
v-else-if="isThumbnailAvailable && file.thumbnailUrl != null"
:src="file.thumbnailUrl"
:alt="file.name"
:title="file.name"

View File

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

View File

@ -17,11 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { url as local } from '@@/js/config.js';
import { maybeMakeRelative } from '@@/js/url.js';
import type { MkABehavior } from '@/components/global/MkA.vue';
import { useTooltip } from '@/composables/use-tooltip.js';
import * as os from '@/os.js';
import { isEnabledUrlPreview } from '@/utility/url-preview.js';
import type { MkABehavior } from '@/components/global/MkA.vue';
import { maybeMakeRelative } from '@@/js/url.js';
const props = withDefaults(defineProps<{
url: string;
@ -42,7 +42,7 @@ if (isEnabledUrlPreview.value) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing,
url: props.url,
source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
anchorElement: el.value instanceof HTMLElement ? el.value : el.value?.$el,
}, {
closed: () => dispose(),
});

View File

@ -16,7 +16,7 @@ import type { MenuItem } from '@/types/menu.js';
const props = defineProps<{
items: MenuItem[];
targetElement: HTMLElement;
anchorElement: HTMLElement;
rootElement: HTMLElement;
width?: number;
}>();
@ -36,10 +36,10 @@ const SCROLLBAR_THICKNESS = 16;
function setPosition() {
if (el.value == null) return;
const rootRect = props.rootElement.getBoundingClientRect();
const parentRect = props.targetElement.getBoundingClientRect();
const parentRect = props.anchorElement.getBoundingClientRect();
const myRect = el.value.getBoundingClientRect();
let left = props.targetElement.offsetWidth;
let left = props.anchorElement.offsetWidth;
let top = (parentRect.top - rootRect.top) - 8;
if (rootRect.left + left + myRect.width >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = -myRect.width;
@ -59,7 +59,7 @@ function onChildClosed(actioned?: boolean) {
}
}
watch(() => props.targetElement, () => {
watch(() => props.anchorElement, () => {
setPosition();
});

View File

@ -208,7 +208,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</span>
</div>
<div v-if="childMenu">
<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/>
<XChild ref="child" :items="childMenu" :anchorElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/>
</div>
</div>
</template>

View File

@ -429,7 +429,7 @@ if (!props.mock) {
showing,
users,
count: appearNote.renoteCount,
targetElement: renoteButton.value,
anchorElement: renoteButton.value,
}, {
closed: () => dispose(),
});
@ -452,7 +452,7 @@ if (!props.mock) {
reaction: '❤️',
users,
count: $appearNote.reactionCount,
targetElement: reactButton.value!,
anchorElement: reactButton.value!,
}, {
closed: () => dispose(),
});
@ -460,14 +460,12 @@ if (!props.mock) {
}
}
function renote(viaKeyboard = false) {
function renote() {
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock });
os.popupMenu(menu, renoteButton.value, {
viaKeyboard,
});
os.popupMenu(menu, renoteButton.value);
subscribeManuallyToNoteCapture();
}

View File

@ -302,7 +302,7 @@ if (noteViewInterruptors.length > 0) {
}
const isRenote = Misskey.note.isPureRenote(note);
const appearNote = getAppearNote(note);
const appearNote = getAppearNote(note) ?? note;
const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({
note: appearNote,
parentNote: note,
@ -405,7 +405,7 @@ useTooltip(renoteButton, async (showing) => {
showing,
users,
count: appearNote.renoteCount,
targetElement: renoteButton.value,
anchorElement: renoteButton.value,
}, {
closed: () => dispose(),
});
@ -428,7 +428,7 @@ if (appearNote.reactionAcceptance === 'likeOnly') {
reaction: '❤️',
users,
count: $appearNote.reactionCount,
targetElement: reactButton.value!,
anchorElement: reactButton.value!,
}, {
closed: () => dispose(),
});

View File

@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</section>
<section v-else-if="expiration === 'after'">
<MkInput v-model="after" small type="number" min="1" class="input">
<MkInput v-model="after" small type="number" :min="1" class="input">
<template #label>{{ i18n.ts._poll.duration }}</template>
</MkInput>
<MkSelect v-model="unit" small>

View File

@ -959,7 +959,16 @@ async function post(ev?: MouseEvent) {
if (postAccount.value) {
const storedAccounts = await getAccounts();
token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token;
const storedAccount = storedAccounts.find(x => x.id === postAccount.value?.id);
if (storedAccount && storedAccount.token != null) {
token = storedAccount.token;
} else {
await os.alert({
type: 'error',
text: 'cannot find the token of the selected account.',
});
return;
}
}
posting.value = true;

View File

@ -167,7 +167,7 @@ function onMouseenter() {
text: computed(() => {
return props.textConverter(finalValue.value);
}),
targetElement: thumbEl.value ?? undefined,
anchorElement: thumbEl.value ?? undefined,
}, {
closed: () => dispose(),
});
@ -191,7 +191,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
text: computed(() => {
return props.textConverter(finalValue.value);
}),
targetElement: thumbEl.value ?? undefined,
anchorElement: thumbEl.value ?? undefined,
}, {
closed: () => dispose(),
});

View File

@ -28,7 +28,7 @@ if (props.withTooltip) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), {
showing,
reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'),
targetElement: elRef.value.$el,
anchorElement: elRef.value.$el,
}, {
closed: () => dispose(),
});

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')">
<MkTooltip ref="tooltip" :showing="showing" :anchorElement="anchorElement" :maxWidth="340" @closed="emit('closed')">
<div :class="$style.root">
<MkReactionIcon :reaction="reaction" :class="$style.icon" :noStyle="true"/>
<div :class="$style.name">{{ reaction.replace('@.', '') }}</div>
@ -20,7 +20,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
defineProps<{
showing: boolean;
reaction: string;
targetElement: HTMLElement;
anchorElement: HTMLElement;
}>();
const emit = defineEmits<{

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')">
<MkTooltip ref="tooltip" :showing="showing" :anchorElement="anchorElement" :maxWidth="340" @closed="emit('closed')">
<div :class="$style.root">
<div :class="$style.reaction">
<MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :noStyle="true"/>
@ -33,7 +33,7 @@ defineProps<{
reaction: string;
users: Misskey.entities.UserLite[];
count: number;
targetElement: HTMLElement;
anchorElement: HTMLElement;
}>();
const emit = defineEmits<{

View File

@ -231,7 +231,7 @@ if (!mock) {
reaction: props.reaction,
users,
count: props.count,
targetElement: buttonEl.value,
anchorElement: buttonEl.value,
}, {
closed: () => dispose(),
});

View File

@ -39,13 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
<script lang="ts" setup>
import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
import { useInterval } from '@@/js/use-interval.js';
import type { VNode, VNodeChild } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
<script lang="ts">
type ItemOption = {
type?: 'option';
value: string | number | null;
@ -60,11 +54,32 @@ type ItemGroup = {
export type MkSelectItem = ItemOption | ItemGroup;
type ValuesOfItems<T> = T extends (infer U)[]
? U extends { type: 'group'; items: infer V }
? V extends (infer W)[]
? W extends { value: infer X }
? X
: never
: never
: U extends { value: infer Y }
? Y
: never
: never;
</script>
<script lang="ts" setup generic="T extends MkSelectItem[]">
import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
import { useInterval } from '@@/js/use-interval.js';
import type { VNode, VNodeChild } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
// TODO: itemsslotoption(props.items)
// see: https://github.com/misskey-dev/misskey/issues/15558
//
const props = defineProps<{
modelValue: string | number | null;
modelValue: ValuesOfItems<T>;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
@ -73,11 +88,11 @@ const props = defineProps<{
inline?: boolean;
small?: boolean;
large?: boolean;
items?: MkSelectItem[];
items?: T;
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', value: string | number | null): void;
(ev: 'update:modelValue', value: ValuesOfItems<T>): void;
}>();
const slots = useSlots();

View File

@ -31,7 +31,7 @@ import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
showing: boolean;
targetElement?: HTMLElement;
anchorElement?: HTMLElement;
x?: number;
y?: number;
text?: string;
@ -58,7 +58,7 @@ const zIndex = os.claimZIndex('high');
function setPosition() {
if (el.value == null) return;
const data = calcPopupPosition(el.value, {
anchorElement: props.targetElement,
anchorElement: props.anchorElement,
direction: props.direction,
align: 'center',
innerMargin: props.innerMargin,

View File

@ -20,7 +20,7 @@ import { prefer } from '@/preferences.js';
const props = defineProps<{
showing: boolean;
url: string;
source: HTMLElement;
anchorElement: HTMLElement;
}>();
const emit = defineEmits<{
@ -32,9 +32,9 @@ const top = ref(0);
const left = ref(0);
onMounted(() => {
const rect = props.source.getBoundingClientRect();
const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.scrollX;
const y = rect.top + props.source.offsetHeight + window.scrollY;
const rect = props.anchorElement.getBoundingClientRect();
const x = Math.max((rect.left + (props.anchorElement.offsetWidth / 2)) - (300 / 2), 6) + window.scrollX;
const y = rect.top + props.anchorElement.offsetHeight + window.scrollY;
top.value = y;
left.value = x;

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="250" @closed="emit('closed')">
<MkTooltip ref="tooltip" :showing="showing" :anchorElement="anchorElement" :maxWidth="250" @closed="emit('closed')">
<div :class="$style.root">
<div v-for="u in users" :key="u.id" :class="$style.user">
<MkAvatar :class="$style.avatar" :user="u"/>
@ -23,7 +23,7 @@ defineProps<{
showing: boolean;
users: Misskey.entities.UserLite[];
count: number;
targetElement: HTMLElement;
anchorElement: HTMLElement;
}>();
const emit = defineEmits<{

View File

@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
currentVisibility: typeof Misskey.noteVisibilities[number];
isSilenced: boolean;
localOnly: boolean;
anchorElement?: HTMLElement;
anchorElement?: HTMLElement | null;
isReplyVisibilitySpecified?: boolean;
}>(), {
});

View File

@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<div v-if="stats && instance.clientOptions.showActivityiesForVisitor !== false" :class="$style.stats">
<div v-if="stats && instance.clientOptions.showActivitiesForVisitor !== false" :class="$style.stats">
<div :class="[$style.statsItem, $style.panel]">
<div :class="$style.statsItemLabel">{{ i18n.ts.users }}</div>
<div :class="$style.statsItemCount"><MkNumber :value="stats.originalUsersCount"/></div>
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStreamingNotesTimeline src="local"/>
</div>
</div>
<div v-if="instance.clientOptions.showActivityiesForVisitor !== false" :class="$style.panel">
<div v-if="instance.clientOptions.showActivitiesForVisitor !== false" :class="$style.panel">
<XActiveUsersChart/>
</div>
</div>
@ -72,7 +72,7 @@ import { openInstanceMenu } from '@/ui/_common_/common.js';
const stats = ref<Misskey.entities.StatsResponse | null>(null);
if (instance.clientOptions.showActivityiesForVisitor !== false) {
if (instance.clientOptions.showActivitiesForVisitor !== false) {
misskeyApi('stats', {}).then((res) => {
stats.value = res;
});

View File

@ -62,7 +62,7 @@ if (props.showUrlPreview && isEnabledUrlPreview.value) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing,
url: props.url,
source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
anchorElement: el.value instanceof HTMLElement ? el.value : el.value?.$el,
}, {
closed: () => dispose(),
});

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

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="250" @closed="emit('closed')">
<MkTooltip ref="tooltip" :showing="showing" :anchorElement="anchorElement" :maxWidth="250" @closed="emit('closed')">
<div :class="$style.root">
{{ content }}
</div>
@ -18,7 +18,7 @@ import MkTooltip from '@/components/MkTooltip.vue';
defineProps<{
showing: boolean;
content: string;
targetElement: HTMLElement;
anchorElement: HTMLElement;
}>();
const emit = defineEmits<{

View File

@ -300,7 +300,7 @@ useTooltip(rootEl, (showing) => {
const result = os.popup(defineAsyncComponent(() => import('@/components/grid/MkCellTooltip.vue')), {
showing,
content,
targetElement: rootEl.value!,
anchorElement: rootEl.value!,
}, {
closed: () => {
result.dispose();

View File

@ -57,7 +57,7 @@ export default {
text: self.text,
asMfm: binding.modifiers.mfm,
direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top',
targetElement: el,
anchorElement: el,
}, {
closed: () => dispose(),
});

View File

@ -9,7 +9,7 @@ import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
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 { MenuItem } from '@/types/menu.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];
}
// 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 を許可するようにする
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>(
component: T,
props: ComponentProps<T>,

View File

@ -22,20 +22,46 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="blocked">{{ i18n.ts.blocked }}</option>
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
</MkSelect>
<MkSelect v-model="sort">
<MkSelect
v-model="sort" :items="[{
label: `${i18n.ts.pubSub} (${i18n.ts.descendingOrder})`,
value: '+pubSub',
}, {
label: `${i18n.ts.pubSub} (${i18n.ts.ascendingOrder})`,
value: '-pubSub',
}, {
label: `${i18n.ts.notes} (${i18n.ts.descendingOrder})`,
value: '+notes',
}, {
label: `${i18n.ts.notes} (${i18n.ts.ascendingOrder})`,
value: '-notes',
}, {
label: `${i18n.ts.users} (${i18n.ts.descendingOrder})`,
value: '+users',
}, {
label: `${i18n.ts.users} (${i18n.ts.ascendingOrder})`,
value: '-users',
}, {
label: `${i18n.ts.following} (${i18n.ts.descendingOrder})`,
value: '+following',
}, {
label: `${i18n.ts.following} (${i18n.ts.ascendingOrder})`,
value: '-following',
}, {
label: `${i18n.ts.followers} (${i18n.ts.descendingOrder})`,
value: '+followers',
}, {
label: `${i18n.ts.followers} (${i18n.ts.ascendingOrder})`,
value: '-followers',
}, {
label: `${i18n.ts.registeredAt} (${i18n.ts.descendingOrder})`,
value: '+firstRetrievedAt',
}, {
label: `${i18n.ts.registeredAt} (${i18n.ts.ascendingOrder})`,
value: '-firstRetrievedAt',
}] as const"
>
<template #label>{{ i18n.ts.sort }}</template>
<option value="+pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.descendingOrder }})</option>
<option value="-pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.ascendingOrder }})</option>
<option value="+notes">{{ i18n.ts.notes }} ({{ i18n.ts.descendingOrder }})</option>
<option value="-notes">{{ i18n.ts.notes }} ({{ i18n.ts.ascendingOrder }})</option>
<option value="+users">{{ i18n.ts.users }} ({{ i18n.ts.descendingOrder }})</option>
<option value="-users">{{ i18n.ts.users }} ({{ i18n.ts.ascendingOrder }})</option>
<option value="+following">{{ i18n.ts.following }} ({{ i18n.ts.descendingOrder }})</option>
<option value="-following">{{ i18n.ts.following }} ({{ i18n.ts.ascendingOrder }})</option>
<option value="+followers">{{ i18n.ts.followers }} ({{ i18n.ts.descendingOrder }})</option>
<option value="-followers">{{ i18n.ts.followers }} ({{ i18n.ts.ascendingOrder }})</option>
<option value="+firstRetrievedAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.descendingOrder }})</option>
<option value="-firstRetrievedAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.ascendingOrder }})</option>
</MkSelect>
</FormSplit>
</div>
@ -52,6 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue';
@ -62,7 +89,7 @@ import { Paginator } from '@/utility/paginator.js';
const host = ref('');
const state = ref('federating');
const sort = ref('+pubSub');
const sort = ref<NonNullable<Misskey.entities.FederationInstancesRequest['sort']>>('+pubSub');
const paginator = markRaw(new Paginator('federation/instances', {
limit: 10,
offsetMode: true,

View File

@ -48,34 +48,22 @@ watch(tab, () => {
const headerActions = computed(() => []);
const headerTabs = computed(() => {
const items = [];
items.push({
key: 'overview',
title: i18n.ts.overview,
}, {
key: 'emojis',
title: i18n.ts.customEmojis,
icon: 'ti ti-icons',
});
if (instance.federation !== 'none') {
items.push({
key: 'federation',
title: i18n.ts.federation,
icon: 'ti ti-whirl',
});
}
items.push({
key: 'charts',
title: i18n.ts.charts,
icon: 'ti ti-chart-line',
});
return items;
});
const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
}, {
key: 'emojis',
title: i18n.ts.customEmojis,
icon: 'ti ti-icons',
}, ...(instance.federation !== 'none' ? [{
key: 'federation',
title: i18n.ts.federation,
icon: 'ti ti-whirl',
}] : []), {
key: 'charts',
title: i18n.ts.charts,
icon: 'ti ti-chart-line',
}]);
definePage(() => ({
title: i18n.ts.instanceInfo,

View File

@ -111,13 +111,13 @@ const props = defineProps<{
fileId: string,
}>();
async function fetch() {
async function _fetch_() {
file.value = await misskeyApi('drive/files/show', { fileId: props.fileId });
info.value = await misskeyApi('admin/drive/show-file', { fileId: props.fileId });
isSensitive.value = file.value.isSensitive;
}
fetch();
_fetch_();
async function del() {
const { canceled } = await os.confirm({

View File

@ -6,58 +6,57 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<div v-if="tab === 'overview'" class="_gaps_m">
<div class="aeakzknw">
<MkAvatar class="avatar" :user="user" indicator link preview/>
<div class="body">
<span class="name"><MkUserName class="name" :user="user"/></span>
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
<span class="state">
<span v-if="suspended" class="suspended">Suspended</span>
<span v-if="silenced" class="silenced">Silenced</span>
<span v-if="moderator" class="moderator">Moderator</span>
</span>
</div>
<div v-if="tab === 'overview'" class="_gaps_m">
<div class="aeakzknw">
<MkAvatar class="avatar" :user="user" indicator link preview/>
<div class="body">
<span class="name"><MkUserName class="name" :user="user"/></span>
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
<span class="state">
<span v-if="suspended" class="suspended">Suspended</span>
<span v-if="silenced" class="silenced">Silenced</span>
<span v-if="moderator" class="moderator">Moderator</span>
</span>
</div>
</div>
<MkInfo v-if="isSystem">{{ i18n.ts.isSystemAccount }}</MkInfo>
<MkInfo v-if="isSystem">{{ i18n.ts.isSystemAccount }}</MkInfo>
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
<div style="display: flex; flex-direction: column; gap: 1em;">
<MkKeyValue :copy="user.id" oneline>
<template #key>ID</template>
<template #value><span class="_monospace">{{ user.id }}</span></template>
</MkKeyValue>
<!-- 要る
<div style="display: flex; flex-direction: column; gap: 1em;">
<MkKeyValue :copy="user.id" oneline>
<template #key>ID</template>
<template #value><span class="_monospace">{{ user.id }}</span></template>
</MkKeyValue>
<!-- 要る
<MkKeyValue v-if="ips.length > 0" :copy="user.id" oneline>
<template #key>IP (recent)</template>
<template #value><span class="_monospace">{{ ips[0].ip }}</span></template>
</MkKeyValue>
-->
<template v-if="!isSystem">
<MkKeyValue oneline>
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
</MkKeyValue>
<MkKeyValue v-if="info" oneline>
<template #key>{{ i18n.ts.lastActiveDate }}</template>
<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
</MkKeyValue>
<MkKeyValue v-if="info" oneline>
<template #key>{{ i18n.ts.email }}</template>
<template #value><span class="_monospace">{{ info.email }}</span></template>
</MkKeyValue>
</template>
</div>
<template v-if="!isSystem">
<MkKeyValue oneline>
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
</MkKeyValue>
<MkKeyValue v-if="info" oneline>
<template #key>{{ i18n.ts.lastActiveDate }}</template>
<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
</MkKeyValue>
<MkKeyValue v-if="info" oneline>
<template #key>{{ i18n.ts.email }}</template>
<template #value><span class="_monospace">{{ info.email }}</span></template>
</MkKeyValue>
</template>
</div>
<MkTextarea v-if="!isSystem" v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
<MkTextarea v-if="!isSystem" v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
<!--
<!--
<FormSection>
<template #label>ActivityPub</template>
@ -93,119 +92,118 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSection>
-->
<FormSection v-if="!isSystem">
<div class="_gaps">
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
<FormSection v-if="!isSystem">
<div class="_gaps">
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
<div>
<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
</div>
<MkFolder>
<template #icon><i class="ti ti-license"></i></template>
<template #label>{{ i18n.ts._role.policies }}</template>
<div class="_gaps">
<div v-for="policy in Object.keys(info.policies)" :key="policy">
{{ policy }} ... {{ info.policies[policy] }}
</div>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-password"></i></template>
<template #label>IP</template>
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
<template v-if="iAmAdmin && ips">
<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
<span class="date">{{ record.createdAt }}</span>
<span class="ip">{{ record.ip }}</span>
</div>
</template>
</MkFolder>
<div>
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
<MkButton v-if="iAmModerator" inline danger @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
</div>
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
<div>
<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
</div>
</FormSection>
</div>
<div v-else-if="tab === 'roles'" class="_gaps">
<MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
<div v-for="role in info.roles" :key="role.id">
<div :class="$style.roleItemMain">
<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/>
<button class="_button" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button>
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
</div>
<div v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub">
<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div>
</div>
</div>
<div v-else-if="tab === 'announcements'" class="_gaps">
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
<MkSelect v-model="announcementsStatus">
<template #label>{{ i18n.ts.filter }}</template>
<option value="active">{{ i18n.ts.active }}</option>
<option value="archived">{{ i18n.ts.archived }}</option>
</MkSelect>
<MkPagination :paginator="announcementsPaginator">
<template #default="{ items }">
<div class="_gaps_s">
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
<span style="margin-right: 0.5em;">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</span>
<span>{{ announcement.title }}</span>
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
<MkFolder>
<template #icon><i class="ti ti-license"></i></template>
<template #label>{{ i18n.ts._role.policies }}</template>
<div class="_gaps">
<div v-for="policy in Object.keys(info.policies)" :key="policy">
{{ policy }} ... {{ info.policies[policy] }}
</div>
</div>
</template>
</MkPagination>
</div>
</MkFolder>
<div v-else-if="tab === 'drive'" class="_gaps">
<MkFileListForAdmin :paginator="filesPaginator" viewMode="grid"/>
</div>
<MkFolder>
<template #icon><i class="ti ti-password"></i></template>
<template #label>IP</template>
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
<template v-if="iAmAdmin && ips">
<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
<span class="date">{{ record.createdAt }}</span>
<span class="ip">{{ record.ip }}</span>
</div>
</template>
</MkFolder>
<div v-else-if="tab === 'chart'" class="_gaps_m">
<div class="cmhjzshm">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
<option value="per-user-notes">{{ i18n.ts.notes }}</option>
</MkSelect>
</div>
<div class="charts">
<div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
<div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
<div>
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
<MkButton v-if="iAmModerator" inline danger @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
</div>
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
</div>
</FormSection>
</div>
<div v-else-if="tab === 'roles'" class="_gaps">
<MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
<div v-for="role in info.roles" :key="role.id">
<div :class="$style.roleItemMain">
<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/>
<button class="_button" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button>
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
</div>
<div v-if="expandedRoleIds.includes(role.id)" :class="$style.roleItemSub">
<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div>
</div>
</div>
<div v-else-if="tab === 'raw'" class="_gaps_m">
<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
</MkObjectView>
<div v-else-if="tab === 'announcements'" class="_gaps">
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
<MkObjectView tall :value="user">
</MkObjectView>
<MkSelect v-model="announcementsStatus">
<template #label>{{ i18n.ts.filter }}</template>
<option value="active">{{ i18n.ts.active }}</option>
<option value="archived">{{ i18n.ts.archived }}</option>
</MkSelect>
<MkPagination :paginator="announcementsPaginator">
<template #default="{ items }">
<div class="_gaps_s">
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
<span style="margin-right: 0.5em;">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</span>
<span>{{ announcement.title }}</span>
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
</div>
</div>
</template>
</MkPagination>
</div>
<div v-else-if="tab === 'drive'" class="_gaps">
<MkFileListForAdmin :paginator="filesPaginator" viewMode="grid"/>
</div>
<div v-else-if="tab === 'chart'" class="_gaps_m">
<div class="cmhjzshm">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
<option value="per-user-notes">{{ i18n.ts.notes }}</option>
</MkSelect>
</div>
<div class="charts">
<div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
<div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
</div>
</div>
</FormSuspense>
</div>
<div v-else-if="tab === 'raw'" class="_gaps_m">
<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
</MkObjectView>
<MkObjectView tall :value="user">
</MkObjectView>
</div>
</div>
</PageWithHeader>
</template>
@ -224,7 +222,6 @@ import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
@ -232,11 +229,13 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { acct } from '@/filters/user.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { iAmAdmin, $i, iAmModerator } from '@/i.js';
import { ensureSignin, iAmAdmin, iAmModerator } from '@/i.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
const $i = ensureSignin();
const props = withDefaults(defineProps<{
userId: string;
initialTab?: string;
@ -244,18 +243,19 @@ const props = withDefaults(defineProps<{
initialTab: 'overview',
});
const result = await _fetch_();
const tab = ref(props.initialTab);
const chartSrc = ref('per-user-notes');
const user = ref<null | Misskey.entities.UserDetailed>();
const init = ref<ReturnType<typeof createFetcher>>();
const info = ref<any>();
const ips = ref<Misskey.entities.AdminGetUserIpsResponse | null>(null);
const user = ref(result.user);
const info = ref(result.info);
const ips = ref(result.ips);
const ap = ref<any>(null);
const moderator = ref(false);
const silenced = ref(false);
const suspended = ref(false);
const isSystem = ref(false);
const moderationNote = ref('');
const moderator = ref(info.value.isModerator);
const silenced = ref(info.value.isSilenced);
const suspended = ref(info.value.isSuspended);
const isSystem = ref(user.value.host == null && user.value.username.includes('.'));
const moderationNote = ref(info.value.moderationNote);
const filesPaginator = markRaw(new Paginator('admin/drive/files', {
limit: 10,
computedParams: computed(() => ({
@ -272,34 +272,37 @@ const announcementsPaginator = markRaw(new Paginator('admin/announcements/list',
status: announcementsStatus.value,
})),
}));
const expandedRoles = ref([]);
const expandedRoleIds = ref<(typeof info.value.roles[number]['id'])[]>([]);
function createFetcher() {
return () => Promise.all([misskeyApi('users/show', {
function _fetch_() {
return Promise.all([misskeyApi('users/show', {
userId: props.userId,
}), misskeyApi('admin/show-user', {
userId: props.userId,
}), iAmAdmin ? misskeyApi('admin/get-user-ips', {
userId: props.userId,
}) : Promise.resolve(null)]).then(([_user, _info, _ips]) => {
user.value = _user;
info.value = _info;
ips.value = _ips;
moderator.value = info.value.isModerator;
silenced.value = info.value.isSilenced;
suspended.value = info.value.isSuspended;
moderationNote.value = info.value.moderationNote;
isSystem.value = user.value.host == null && user.value.username.includes('.');
watch(moderationNote, async () => {
await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
await refreshUser();
});
});
}) : Promise.resolve(null)]).then(([_user, _info, _ips]) => ({
user: _user,
info: _info,
ips: _ips,
}));
}
function refreshUser() {
init.value = createFetcher();
watch(moderationNote, async () => {
await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
await refreshUser();
});
async function refreshUser() {
const result = await _fetch_();
user.value = result.user;
info.value = result.info;
ips.value = result.ips;
moderator.value = info.value.isModerator;
silenced.value = info.value.isSilenced;
suspended.value = info.value.isSuspended;
isSystem.value = user.value.host == null && user.value.username.includes('.');
moderationNote.value = info.value.moderationNote;
}
async function updateRemoteUser() {
@ -456,7 +459,7 @@ async function assignRole() {
refreshUser();
}
async function unassignRole(role, ev) {
async function unassignRole(role: typeof info.value.roles[number], ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.unassign,
icon: 'ti ti-x',
@ -468,11 +471,11 @@ async function unassignRole(role, ev) {
}], ev.currentTarget ?? ev.target);
}
function toggleRoleItem(role) {
if (expandedRoles.value.includes(role.id)) {
expandedRoles.value = expandedRoles.value.filter(x => x !== role.id);
function toggleRoleItem(role: typeof info.value.roles[number]) {
if (expandedRoleIds.value.includes(role.id)) {
expandedRoleIds.value = expandedRoleIds.value.filter(x => x !== role.id);
} else {
expandedRoles.value.push(role.id);
expandedRoleIds.value.push(role.id);
}
}
@ -493,12 +496,6 @@ async function editAnnouncement(announcement) {
});
}
watch(() => props.userId, () => {
init.value = createFetcher();
}, {
immediate: true,
});
watch(user, () => {
misskeyApi('ap/get', {
uri: user.value.uri ?? `${url}/users/${user.value.id}`,

View File

@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['activity', 'activities']">
<MkSwitch v-model="showActivityiesForVisitor">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.showActivityiesForVisitor }}</SearchLabel></template>
<MkSwitch v-model="showActivitiesForVisitor">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.showActivitiesForVisitor }}</SearchLabel></template>
</MkSwitch>
</SearchMarker>
@ -168,7 +168,7 @@ const meta = await misskeyApi('admin/meta');
const entrancePageStyle = ref(meta.clientOptions.entrancePageStyle ?? 'classic');
const showTimelineForVisitor = ref(meta.clientOptions.showTimelineForVisitor ?? true);
const showActivityiesForVisitor = ref(meta.clientOptions.showActivityiesForVisitor ?? true);
const showActivitiesForVisitor = ref(meta.clientOptions.showActivitiesForVisitor ?? true);
const iconUrl = ref(meta.iconUrl);
const app192IconUrl = ref(meta.app192IconUrl);
const app512IconUrl = ref(meta.app512IconUrl);
@ -189,7 +189,7 @@ function save() {
clientOptions: {
entrancePageStyle: entrancePageStyle.value,
showTimelineForVisitor: showTimelineForVisitor.value,
showActivityiesForVisitor: showActivityiesForVisitor.value,
showActivitiesForVisitor: showActivitiesForVisitor.value,
},
iconUrl: iconUrl.value,
app192IconUrl: app192IconUrl.value,

View File

@ -41,7 +41,7 @@ async function addRelay() {
type: 'url',
placeholder: i18n.ts.inboxUrl,
});
if (canceled) return;
if (canceled || inbox == null) return;
misskeyApi('admin/relays/add', {
inbox,
}).then((relay: any) => {

View File

@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
</div>
</div>
<MkError v-else-if="error" @retry="fetch()"/>
<MkError v-else-if="error" @retry="_fetch_()"/>
<MkLoading v-else/>
</Transition>
</div>
@ -66,7 +66,7 @@ const announcement = ref<Misskey.entities.Announcement | null>(null);
const error = ref<any>(null);
const path = computed(() => props.announcementId);
function fetch() {
function _fetch_() {
announcement.value = null;
misskeyApi('announcements/show', {
announcementId: props.announcementId,
@ -96,7 +96,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
}
}
watch(() => path.value, fetch, { immediate: true });
watch(() => path.value, _fetch_, { immediate: true });
const headerActions = computed(() => []);

View File

@ -37,27 +37,14 @@ const props = defineProps<{
const antenna = ref<Misskey.entities.Antenna | null>(null);
const tlEl = useTemplateRef('tlEl');
async function timetravel() {
const { canceled, result: date } = await os.inputDate({
title: i18n.ts.date,
});
if (canceled) return;
tlEl.value.timetravel(date);
}
function settings() {
router.push('/my/antennas/:antennaId', {
params: {
antennaId: props.antennaId,
}
},
});
}
function focus() {
tlEl.value.focus();
}
watch(() => props.antennaId, async () => {
antenna.value = await misskeyApi('antennas/show', {
antennaId: props.antennaId,
@ -65,10 +52,6 @@ watch(() => props.antennaId, async () => {
}, { immediate: true });
const headerActions = computed(() => antenna.value ? [{
icon: 'ti ti-calendar-time',
text: i18n.ts.jumpToSpecifiedDate,
handler: timetravel,
}, {
icon: 'ti ti-settings',
text: i18n.ts.settings,
handler: settings,

View File

@ -44,11 +44,13 @@ const name = computed(() => {
});
function cancel() {
misskeyApi('auth/deny', {
token: props.session.token,
}).then(() => {
emit('denied');
});
//misskeyApi('auth/deny', {
// token: props.session.token,
//}).then(() => {
// emit('denied');
//});
emit('denied');
}
function accept() {

View File

@ -92,7 +92,7 @@ const props = defineProps<{
}>();
const channel = ref<Misskey.entities.Channel | null>(null);
const name = ref<string | null>(null);
const name = ref<string>('');
const description = ref<string | null>(null);
const bannerUrl = ref<string | null>(null);
const bannerId = ref<string | null>(null);
@ -114,20 +114,22 @@ watch(() => bannerId.value, async () => {
async function fetchChannel() {
if (props.channelId == null) return;
channel.value = await misskeyApi('channels/show', {
const result = await misskeyApi('channels/show', {
channelId: props.channelId,
});
name.value = channel.value.name;
description.value = channel.value.description;
bannerId.value = channel.value.bannerId;
bannerUrl.value = channel.value.bannerUrl;
isSensitive.value = channel.value.isSensitive;
pinnedNotes.value = channel.value.pinnedNoteIds.map(id => ({
name.value = result.name;
description.value = result.description;
bannerId.value = result.bannerId;
bannerUrl.value = result.bannerUrl;
isSensitive.value = result.isSensitive;
pinnedNotes.value = result.pinnedNoteIds.map(id => ({
id,
}));
color.value = channel.value.color;
allowRenoteToExternal.value = channel.value.allowRenoteToExternal;
color.value = result.color;
allowRenoteToExternal.value = result.allowRenoteToExternal;
channel.value = result;
}
fetchChannel();
@ -154,15 +156,17 @@ function save() {
name: name.value,
description: description.value,
bannerId: bannerId.value,
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
color: color.value,
isSensitive: isSensitive.value,
allowRenoteToExternal: allowRenoteToExternal.value,
};
} satisfies Misskey.entities.ChannelsCreateRequest;
if (props.channelId) {
params.channelId = props.channelId;
os.apiWithDialog('channels/update', params);
if (props.channelId != null) {
os.apiWithDialog('channels/update', {
...params,
channelId: props.channelId,
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
});
} else {
os.apiWithDialog('channels/create', params).then(created => {
router.push('/channels/:channelId', {
@ -175,12 +179,13 @@ function save() {
}
async function archive() {
if (props.channelId == null) return;
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value }),
text: i18n.ts.channelArchiveConfirmDescription,
});
if (canceled) return;
misskeyApi('channels/update', {

View File

@ -131,6 +131,8 @@ watch(() => props.channelId, async () => {
channel.value = await misskeyApi('channels/show', {
channelId: props.channelId,
});
if (channel.value == null) return; // TS
favorited.value = channel.value.isFavorited ?? false;
if (favorited.value || channel.value.isFollowing) {
tab.value = 'timeline';
@ -150,7 +152,7 @@ function edit() {
router.push('/channels/:channelId/edit', {
params: {
channelId: props.channelId,
}
},
});
}

View File

@ -105,7 +105,7 @@ const folderHierarchy = computed(() => {
});
const isImage = computed(() => file.value?.type.startsWith('image/'));
async function fetch() {
async function _fetch_() {
fetching.value = true;
file.value = await misskeyApi('drive/files/show', {
@ -134,7 +134,7 @@ function move() {
fileId: file.value.id,
folderId: folder[0] ? folder[0].id : null,
}).then(async () => {
await fetch();
await _fetch_();
});
});
}
@ -146,7 +146,7 @@ function toggleSensitive() {
fileId: file.value.id,
isSensitive: !file.value.isSensitive,
}).then(async () => {
await fetch();
await _fetch_();
}).catch(err => {
os.alert({
type: 'error',
@ -169,7 +169,7 @@ function rename() {
fileId: file.value.id,
name: name,
}).then(async () => {
await fetch();
await _fetch_();
});
});
}
@ -186,7 +186,7 @@ async function describe() {
fileId: file.value.id,
comment: caption.length === 0 ? null : caption,
}).then(async () => {
await fetch();
await _fetch_();
});
},
closed: () => dispose(),
@ -212,7 +212,7 @@ async function deleteFile() {
}
onMounted(async () => {
await fetch();
await _fetch_();
});
</script>

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkTextarea>
<div class="_gaps_s">
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : '' }">
<div class="name">{{ file.name }}</div>
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
</div>
@ -88,7 +88,7 @@ async function save() {
router.push('/gallery/:postId', {
params: {
postId: props.postId,
}
},
});
} else {
const created = await os.apiWithDialog('gallery/posts/create', {
@ -100,7 +100,7 @@ async function save() {
router.push('/gallery/:postId', {
params: {
postId: created.id,
}
},
});
}
}

View File

@ -80,7 +80,7 @@ function close_(): void {
}
}
async function fetch() {
async function _fetch_() {
if (!url.value || !hash.value) {
errorKV.value = {
title: i18n.ts._externalResourceInstaller._errors._invalidParams.title,
@ -229,7 +229,7 @@ async function install() {
const urlParams = new URLSearchParams(window.location.search);
url.value = urlParams.get('url');
hash.value = urlParams.get('hash');
fetch();
_fetch_();
definePage(() => ({
title: i18n.ts._externalResourceInstaller.title,

View File

@ -198,7 +198,7 @@ if (iAmModerator) {
});
}
async function fetch(): Promise<void> {
async function _fetch_(): Promise<void> {
if (iAmAdmin) {
meta.value = await misskeyApi('admin/meta');
}
@ -276,7 +276,7 @@ function refreshMetadata(): void {
});
}
fetch();
_fetch_();
const headerActions = computed(() => [{
text: `https://${props.host}`,

View File

@ -29,7 +29,7 @@ import MkButton from '@/components/MkButton.vue';
const state = ref<'fetching' | 'done'>('fetching');
function fetch() {
function _fetch_() {
const params = new URL(window.location.href).searchParams;
// acctdeprecated
@ -44,20 +44,18 @@ function fetch() {
if (uri.startsWith('https://')) {
promise = misskeyApi('ap/show', {
uri,
});
promise.then(res => {
}).then(res => {
if (res.type === 'User') {
mainRouter.replace('/@:acct/:page?', {
params: {
acct: res.host != null ? `${res.object.username}@${res.object.host}` : res.object.username,
}
},
});
} else if (res.type === 'Note') {
mainRouter.replace('/notes/:noteId/:initialTab?', {
params: {
noteId: res.object.id,
}
},
});
} else {
os.alert({
@ -70,12 +68,11 @@ function fetch() {
if (uri.startsWith('acct:')) {
uri = uri.slice(5);
}
promise = misskeyApi('users/show', Misskey.acct.parse(uri));
promise.then(user => {
promise = misskeyApi('users/show', Misskey.acct.parse(uri)).then(user => {
mainRouter.replace('/@:acct/:page?', {
params: {
acct: user.host != null ? `${user.username}@${user.host}` : user.username,
}
},
});
});
}
@ -96,7 +93,7 @@ function goToMisskey(): void {
window.location.href = '/';
}
fetch();
_fetch_();
const headerActions = computed(() => []);

View File

@ -30,11 +30,11 @@ import { antennasCache } from '@/cache.js';
const antennas = computed(() => antennasCache.value.value ?? []);
function fetch() {
function _fetch_() {
antennasCache.fetch();
}
fetch();
_fetch_();
const headerActions = computed(() => [{
asFullButton: true,
@ -42,7 +42,7 @@ const headerActions = computed(() => [{
text: i18n.ts.reload,
handler: () => {
antennasCache.delete();
fetch();
_fetch_();
},
}]);

View File

@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="items.length > 0" class="_gaps">
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div>
<MkAvatars :userIds="list.userIds" :limit="10"/>
<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds!.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div>
<MkAvatars :userIds="list.userIds!" :limit="10"/>
</MkA>
</div>
</div>
@ -40,20 +40,20 @@ const $i = ensureSignin();
const items = computed(() => userListsCache.value.value ?? []);
function fetch() {
function _fetch_() {
userListsCache.fetch();
}
fetch();
_fetch_();
async function create() {
const { canceled, result: name } = await os.inputText({
title: i18n.ts.enterListName,
});
if (canceled) return;
if (canceled || name == null) return;
await os.apiWithDialog('users/lists/create', { name: name });
userListsCache.delete();
fetch();
_fetch_();
}
const headerActions = computed(() => [{
@ -62,7 +62,7 @@ const headerActions = computed(() => [{
text: i18n.ts.reload,
handler: () => {
userListsCache.delete();
fetch();
_fetch_();
},
}]);
@ -74,7 +74,7 @@ definePage(() => ({
}));
onActivated(() => {
fetch();
_fetch_();
});
</script>

View File

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder defaultOpen>
<template #label>{{ i18n.ts.members }}</template>
<template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template>
<template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds!.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template>
<div class="_gaps_s">
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>

View File

@ -31,7 +31,7 @@ import { Paginator } from '@/utility/paginator.js';
const tab = ref('all');
const includeTypes = ref<string[] | null>(null);
const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value.includes(t)) : null);
const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value!.includes(t)) : null);
const mentionsPaginator = markRaw(new Paginator('notes/mentions', {
limit: 10,
@ -71,7 +71,7 @@ const headerActions = computed(() => [tab.value === 'all' ? {
text: i18n.ts.markAllAsRead,
icon: 'ti ti-check',
handler: () => {
os.apiWithDialog('notifications/mark-all-as-read');
os.apiWithDialog('notifications/mark-all-as-read', {});
},
} : undefined].filter(x => x !== undefined));

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
<div v-if="scopesWithDomain" class="_gaps_m">
<FormSection v-for="domain in scopesWithDomain" :key="domain.domain">
<FormSection v-for="domain in scopesWithDomain" :key="domain.domain ?? 'system'">
<template #label>{{ domain.domain ? domain.domain.toUpperCase() : i18n.ts.system }}</template>
<div class="_gaps_s">
<FormLink v-for="scope in domain.scopes" :to="`/registry/keys/${domain.domain ?? '@'}/${scope.join('/')}`" class="_monospace">{{ scope.length === 0 ? '(root)' : scope.join('/') }}</FormLink>

View File

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
<div v-text="i18n.ts._2fa.alreadyRegistered"/>
<template v-if="$i.securityKeysList.length > 0">
<template v-if="$i.securityKeysList!.length > 0">
<MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
<MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
</template>
@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-else>
<MkButton primary @click="addSecurityKey">{{ i18n.ts._2fa.registerSecurityKey }}</MkButton>
<MkFolder v-for="key in $i.securityKeysList" :key="key.id">
<MkFolder v-for="key in $i.securityKeysList!" :key="key.id">
<template #label>{{ key.name }}</template>
<template #suffix><I18n :src="i18n.ts.lastUsedAt"><template #t><MkTime :time="key.lastUsed"/></template></I18n></template>
<div class="_buttons">
@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['password', 'less', 'key', 'passkey', 'login', 'signin']">
<MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
<MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList!.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
<template #label><SearchLabel>{{ i18n.ts.passwordLessLogin }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.passwordLessLoginDescription }}</SearchText></template>
</MkSwitch>

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!--<MkButton @click="refreshAllAccounts"><i class="ti ti-refresh"></i></MkButton>-->
</div>
<MkUserCardMini v-for="x in accounts" :key="x[0] + x[1].id" :user="x[1]" :class="$style.user" @click.prevent="menu(x[0], x[1], $event)"/>
<MkUserCardMini v-for="x in accounts" :key="x[0] + x[1].id" :user="x[1]" :class="$style.user" @click.prevent="showMenu(x[0], x[1], $event)"/>
</div>
</SearchMarker>
</template>
@ -36,7 +36,7 @@ function refreshAllAccounts() {
// TODO
}
function menu(host: string, account: Misskey.entities.UserDetailed, ev: MouseEvent) {
function showMenu(host: string, account: Misskey.entities.UserDetailed, ev: MouseEvent) {
let menu: MenuItem[];
menu = [{

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #label>{{ token.name }}</template>
<template #caption>{{ token.description }}</template>
<template #suffix><MkTime :time="token.lastUsedAt"/></template>
<template v-if="token.lastUsedAt != null" #suffix><MkTime :time="token.lastUsedAt"/></template>
<template #footer>
<MkButton danger @click="revoke(token)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</template>
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #key>{{ i18n.ts.installedDate }}</template>
<template #value><MkTime :time="token.createdAt" :mode="'detail'"/></template>
</MkKeyValue>
<MkKeyValue oneline>
<MkKeyValue v-if="token.lastUsedAt != null" oneline>
<template #key>{{ i18n.ts.lastUsedDate }}</template>
<template #value><MkTime :time="token.lastUsedAt" :mode="'detail'"/></template>
</MkKeyValue>

View File

@ -17,13 +17,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.decorations">
<XDecoration
v-for="(avatarDecoration, i) in $i.avatarDecorations"
:decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)"
:decoration="avatarDecorations.find(d => d.id === avatarDecoration.id) ?? { id: '', url: '', name: '?', roleIdsThatCanBeUsedThisDecoration: [] }"
:angle="avatarDecoration.angle"
:flipH="avatarDecoration.flipH"
:offsetX="avatarDecoration.offsetX"
:offsetY="avatarDecoration.offsetY"
:active="true"
@click="openDecoration(avatarDecoration, i)"
@click="openAttachedDecoration(i)"
/>
</div>
@ -50,6 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, defineAsyncComponent, computed } from 'vue';
import * as Misskey from 'misskey-js';
import XDecoration from './avatar-decoration.decoration.vue';
import XDialog from './avatar-decoration.dialog.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -68,14 +69,24 @@ misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
loading.value = false;
});
async function openDecoration(avatarDecoration, index?: number) {
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration.dialog.vue').then(x => x.default), {
function openAttachedDecoration(index: number) {
openDecoration(avatarDecorations.value.find(d => d.id === $i.avatarDecorations[index].id) ?? { id: '', url: '', name: '?', roleIdsThatCanBeUsedThisDecoration: [] }, index);
}
async function openDecoration(avatarDecoration: {
id: string;
url: string;
name: string;
roleIdsThatCanBeUsedThisDecoration: string[];
}, index?: number) {
const { dispose } = os.popup(XDialog, {
decoration: avatarDecoration,
usingIndex: index,
usingIndex: index ?? null,
}, {
'attach': async (payload) => {
const decoration = {
id: avatarDecoration.id,
url: avatarDecoration.url,
angle: payload.angle,
flipH: payload.flipH,
offsetX: payload.offsetX,
@ -90,6 +101,7 @@ async function openDecoration(avatarDecoration, index?: number) {
'update': async (payload) => {
const decoration = {
id: avatarDecoration.id,
url: avatarDecoration.url,
angle: payload.angle,
flipH: payload.flipH,
offsetX: payload.offsetX,

View File

@ -43,7 +43,7 @@ async function edit() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkWatermarkEditorDialog.vue')), {
preset: deepClone(props.preset),
}, {
ok: (preset: WatermarkPreset) => {
ok: (preset) => {
emit('updatePreset', preset);
},
closed: () => dispose(),

View File

@ -74,7 +74,7 @@ import { instance } from '@/instance.js';
const $i = ensureSignin();
const emailAddress = ref($i.email);
const emailAddress = ref($i.email ?? '');
const onChangeReceiveAnnouncementEmail = (v) => {
misskeyApi('i/update', {

View File

@ -188,6 +188,8 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
}]);
onMounted(() => {
if (el.value == null) return; // TS
ro.observe(el.value);
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
@ -198,6 +200,8 @@ onMounted(() => {
});
onActivated(() => {
if (el.value == null) return; // TS
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
if (!narrow.value && currentPage.value?.route.name == null) {
@ -215,7 +219,7 @@ watch(router.currentRef, (to) => {
}
});
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
const emailNotConfigured = computed(() => $i && instance.enableEmail && ($i.email == null || !$i.emailVerified));
provideMetadataReceiver((metadataGetter) => {
const info = metadataGetter();

View File

@ -160,10 +160,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label><SearchLabel>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</SearchLabel></template>
<div class="_gaps_s">
<MkSelect :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
<option :value="null">{{ i18n.ts.none }}</option>
<option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
<option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
<MkSelect
:items="[{
value: null,
label: i18n.ts.none
}, {
value: 'relative',
label: i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod
}, {
value: 'absolute',
label: i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime
}] as const" :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null"
>
</MkSelect>
<MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore_selection">
@ -262,7 +270,7 @@ const makeNotesFollowersOnlyBefore_presets = [
const makeNotesFollowersOnlyBefore_isCustomMode = ref(
makeNotesFollowersOnlyBefore.value != null &&
makeNotesFollowersOnlyBefore.value < 0 &&
!makeNotesFollowersOnlyBefore_presets.some((preset) => preset.value === makeNotesFollowersOnlyBefore.value)
!makeNotesFollowersOnlyBefore_presets.some((preset) => preset.value === makeNotesFollowersOnlyBefore.value),
);
const makeNotesFollowersOnlyBefore_selection = computed({
@ -270,14 +278,14 @@ const makeNotesFollowersOnlyBefore_selection = computed({
set(value) {
makeNotesFollowersOnlyBefore_isCustomMode.value = value === 'custom';
if (value !== 'custom') makeNotesFollowersOnlyBefore.value = value;
}
},
});
const makeNotesFollowersOnlyBefore_customMonths = computed({
get: () => makeNotesFollowersOnlyBefore.value ? Math.abs(makeNotesFollowersOnlyBefore.value) / (30 * 24 * 60 * 60) : null,
set(value) {
if (value != null && value > 0) makeNotesFollowersOnlyBefore.value = -Math.abs(Math.floor(Number(value))) * 30 * 24 * 60 * 60;
}
},
});
const makeNotesHiddenBefore_type = computed(() => {
@ -303,7 +311,7 @@ const makeNotesHiddenBefore_presets = [
const makeNotesHiddenBefore_isCustomMode = ref(
makeNotesHiddenBefore.value != null &&
makeNotesHiddenBefore.value < 0 &&
!makeNotesHiddenBefore_presets.some((preset) => preset.value === makeNotesHiddenBefore.value)
!makeNotesHiddenBefore_presets.some((preset) => preset.value === makeNotesHiddenBefore.value),
);
const makeNotesHiddenBefore_selection = computed({
@ -311,14 +319,14 @@ const makeNotesHiddenBefore_selection = computed({
set(value) {
makeNotesHiddenBefore_isCustomMode.value = value === 'custom';
if (value !== 'custom') makeNotesHiddenBefore.value = value;
}
},
});
const makeNotesHiddenBefore_customMonths = computed({
get: () => makeNotesHiddenBefore.value ? Math.abs(makeNotesHiddenBefore.value) / (30 * 24 * 60 * 60) : null,
set(value) {
if (value != null && value > 0) makeNotesHiddenBefore.value = -Math.abs(Math.floor(Number(value))) * 30 * 24 * 60 * 60;
}
},
});
watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => {

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/profile" :label="i18n.ts.profile" :keywords="['profile']" icon="ti ti-user">
<div class="_gaps_m">
<div class="_panel">
<div :class="$style.banner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<div :class="$style.banner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : '' }">
<div :class="$style.bannerEdit">
<SearchMarker :keywords="['banner', 'change']">
<MkButton primary rounded @click="changeBanner"><SearchLabel>{{ i18n.ts._profile.changeBanner }}</SearchLabel></MkButton>

View File

@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="statusbar.props.shuffle">
<template #label>{{ i18n.ts.shuffle }}</template>
</MkSwitch>
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1">
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" :min="1">
<template #label>{{ i18n.ts.refreshInterval }}</template>
</MkInput>
<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</template>
<template v-else-if="statusbar.type === 'federation'">
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1">
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" :min="1">
<template #label>{{ i18n.ts.refreshInterval }}</template>
</MkInput>
<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
@ -104,7 +104,7 @@ const props = defineProps<{
userLists: Misskey.entities.UserList[] | null;
}>();
const statusbar = reactive(deepClone(prefer.s.statusbars.find(x => x.id === props._id)));
const statusbar = reactive(deepClone(prefer.s.statusbars.find(x => x.id === props._id))!);
watch(() => statusbar.type, () => {
if (statusbar.type === 'rss') {

View File

@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkCodeEditor>
<div class="_buttons">
<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" inline @click="() => previewTheme(installThemeCode!)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" primary inline @click="() => install(installThemeCode!)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
</div>
</div>
</template>
@ -39,7 +39,7 @@ async function install(code: string): Promise<void> {
});
installThemeCode.value = null;
router.push('/settings/theme');
} catch (err) {
} catch (err: any) {
switch (err.message.toLowerCase()) {
case 'this theme is already installed':
os.alert({

View File

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._theme.code }}</template>
<template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template>
</MkTextarea>
<MkButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" danger @click="uninstall()"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
<MkButton v-if="!builtinThemes.some(t => t.id == selectedTheme!.id)" danger @click="uninstall()"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
</template>
</div>
</template>

View File

@ -61,7 +61,7 @@ const event_reaction = ref(true);
const event_mention = ref(true);
async function create(): Promise<void> {
const events = [];
const events: string[] = [];
if (event_follow.value) events.push('follow');
if (event_followed.value) events.push('followed');
if (event_note.value) events.push('note');

View File

@ -52,7 +52,7 @@ async function post() {
const headerActions = computed(() => [{
icon: 'ti ti-dots',
label: i18n.ts.more,
text: i18n.ts.more,
handler: (ev: MouseEvent) => {
os.popupMenu([{
text: i18n.ts.embed,

View File

@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.backgroundColor }}</template>
<div class="cwepdizn-colors">
<div class="row">
<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
<button v-for="color in bgColors.filter(x => x.kind === 'light')" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
<div class="preview" :style="{ background: color.forPreview }"></div>
</button>
</div>
<div class="row">
<button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
<button v-for="color in bgColors.filter(x => x.kind === 'dark')" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
<div class="preview" :style="{ background: color.forPreview }"></div>
</button>
</div>
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.accentColor }}</template>
<div class="cwepdizn-colors">
<div class="row">
<button v-for="color in accentColors" :key="color" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)">
<button v-for="color in accentColors" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)">
<div class="preview" :style="{ background: color }"></div>
</button>
</div>
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.textColor }}</template>
<div class="cwepdizn-colors">
<div class="row">
<button v-for="color in fgColors" :key="color" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)">
<button v-for="color in fgColors" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)">
<div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
</button>
</div>
@ -75,17 +75,17 @@ SPDX-License-Identifier: AGPL-3.0-only
import { watch, ref, computed } from 'vue';
import { toUnicode } from 'punycode.js';
import tinycolor from 'tinycolor2';
import { genId } from '@/utility/id.js';
import JSON5 from 'json5';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import { host } from '@@/js/config.js';
import type { Theme } from '@/theme.js';
import { genId } from '@/utility/id.js';
import MkButton from '@/components/MkButton.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkFolder from '@/components/MkFolder.vue';
import { $i } from '@/i.js';
import { ensureSignin } from '@/i.js';
import { addTheme, applyTheme } from '@/theme.js';
import * as os from '@/os.js';
import { store } from '@/store.js';
@ -94,6 +94,8 @@ import { useLeaveGuard } from '@/composables/use-leave-guard.js';
import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js';
const $i = ensureSignin();
const bgColors = [
{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
{ color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
@ -123,12 +125,15 @@ const fgColors = [
{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
];
const theme = ref<Partial<Theme>>({
const theme = ref<Theme>({
id: genId(),
name: 'untitled',
author: `@${$i.username}@${toUnicode(host)}`,
base: 'light',
props: lightTheme.props,
});
const description = ref<string | null>(null);
const themeCode = ref<string | null>(null);
const themeCode = ref<string>('');
const changed = ref(false);
useLeaveGuard(changed);
@ -194,7 +199,6 @@ async function saveAs() {
theme.value.id = genId();
theme.value.name = name;
theme.value.author = `@${$i.username}@${toUnicode(host)}`;
if (description.value) theme.value.desc = description.value;
await addTheme(theme.value);
applyTheme(theme.value);

View File

@ -36,7 +36,7 @@ const props = defineProps<{
const chartEl = useTemplateRef('chartEl');
const legendEl = useTemplateRef('legendEl');
const now = new Date();
let chartInstance: Chart = null;
let chartInstance: Chart | null = null;
const chartLimit = 30;
const fetching = ref(true);

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<MkPagination v-slot="{items}" :paginator="type === 'following' ? followingPaginator : followersPaginator" withControl>
<div :class="$style.users">
<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" :user="user"/>
<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee! : x.follower!)" :key="user.id" :user="user"/>
</div>
</MkPagination>
</div>

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