Compare commits

..

48 Commits

Author SHA1 Message Date
github-actions[bot]
bcb891e4fd Bump version to 2025.3.2-alpha.11 2025-03-14 01:29:38 +00:00
syuilo
152660fcf2 enhance(frontend): re-organize settings page 2025-03-14 09:53:44 +09:00
syuilo
a1204f5e3e 🎨 2025-03-14 09:42:53 +09:00
かっこかり
7acd3d1a88 fix(frontend): fix test 2025-03-13 22:32:26 +09:00
syuilo
8c9ec5827f enhance(frontend): improve accounts management 2025-03-13 22:12:23 +09:00
syuilo
44073736de enhance(frontend): improve preferences 2025-03-13 19:44:23 +09:00
syuilo
0126dba475 enhance(frontend): re-organize settings page 2025-03-13 19:30:35 +09:00
github-actions[bot]
3280a3d661 Bump version to 2025.3.2-alpha.10 2025-03-13 09:07:37 +00:00
syuilo
bdf80c49d8 fix(frontend): better migration detection
Fix #15656
2025-03-13 18:05:44 +09:00
syuilo
59169a6450 🎨 2025-03-13 17:42:35 +09:00
syuilo
5d228fb0f3 enhance(frontend): re-organize settings page 2025-03-13 17:39:53 +09:00
syuilo
10b67e1b3a enhance(frontend): improve emoji picker settings 2025-03-13 16:56:47 +09:00
syuilo
3ced310f77 refactor(frontend): organize use functions 2025-03-13 14:05:04 +09:00
syuilo
010ec113c2 refactor(frontend): cond -> scope 2025-03-13 13:45:23 +09:00
syuilo
30005ba959 enhance(frontend): tweak search index 2025-03-13 09:26:04 +09:00
syuilo
6b69588c03 enhance(frontend): improve deck setting page 2025-03-13 09:24:15 +09:00
syuilo
8593aa1418 refactor 2025-03-13 09:10:09 +09:00
syuilo
9876ff9a7a Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-13 09:07:25 +09:00
syuilo
ce6eba77d9 🎨 2025-03-13 09:07:22 +09:00
syuilo
9b2af53025 enhance(frontend): improve pref manager 2025-03-13 09:02:38 +09:00
syuilo
7b6ff19ea3 Update CHANGELOG.md 2025-03-12 21:49:23 +09:00
github-actions[bot]
c9fa95429a Bump version to 2025.3.2-alpha.9 2025-03-12 12:45:35 +00:00
饺子w (Yumechi)
e5d117dc98 fix(backend): tighten an overly relaxed criteria and remove capability of matching multiple final URLs in URL authority checking (#15655)
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-12 12:39:24 +00:00
syuilo
4a73feb041 enhance(frontend): make deck profiles syncable 2025-03-12 21:12:08 +09:00
syuilo
a06b9eefaa enhance(frontend): suppress needless confirmation when turn on pref sync 2025-03-12 21:05:39 +09:00
syuilo
3129fcf164 fix(frontend): fix type errors 2025-03-12 20:17:54 +09:00
syuilo
35a4544477 add todo 2025-03-12 18:54:36 +09:00
zyoshoka
aa1cc2f817 fix(storybook): use type-only imports in generated stories (#15654) 2025-03-12 16:51:10 +09:00
github-actions[bot]
15685be4cc Bump version to 2025.3.2-alpha.8 2025-03-12 06:10:35 +00:00
syuilo
8508c4dadc refactor 2025-03-12 15:07:45 +09:00
かっこかり
e594fb0037 enhance(dev): frontendの検索インデックス作成を単独のコマンドで行えるように (#15653) 2025-03-12 14:37:57 +09:00
syuilo
a369721791 remove todo 2025-03-12 14:35:22 +09:00
syuilo
f8e244f48d enhance(frontend): アカウントオーバーライド設定とデバイス間同期の併用に対応 2025-03-12 14:34:10 +09:00
syuilo
8410611512 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-12 13:04:44 +09:00
syuilo
caab1ec7c3 🎨 2025-03-12 13:04:41 +09:00
github-actions[bot]
ffade9740e Bump version to 2025.3.2-alpha.7 2025-03-12 03:03:37 +00:00
syuilo
b03bcf26cd enhance(frontend): 設定値の同期を実装(実験的) 2025-03-12 11:39:05 +09:00
syuilo
ddbc83b2e4 chore(frontend): tweak settings page 2025-03-11 20:42:06 +09:00
syuilo
d185785f20 enhance(frontend): improve settings page 2025-03-11 14:52:04 +09:00
syuilo
02d7fbefc4 🎨 2025-03-11 12:08:15 +09:00
syuilo
f7ea92c68c chore: remove unused files 2025-03-11 12:02:41 +09:00
syuilo
e891d5c5d3 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-11 11:44:34 +09:00
syuilo
57a6b630b7 chore: add note 2025-03-11 11:44:25 +09:00
github-actions[bot]
eda768a08c Bump version to 2025.3.2-alpha.6 2025-03-11 02:43:27 +00:00
syuilo
1f345eb839 enhance(frontend): deckをpreferences管理に 2025-03-11 11:14:55 +09:00
syuilo
1f2801af02 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-10 21:42:30 +09:00
syuilo
a4ba096e2a chore(frontend): improve preference store stability 2025-03-10 21:42:17 +09:00
ろむねこ
6841cdfa76 enhance(frontend): CWの注釈テキストが入力されていない場合はPostボタンを非アクティブに (#15639)
* add condition to disable post button when CW text is empty

* standardize condition by using 1<= inserted of 0<

* unify CW text length condition to improve readability

* add missing CW state check

* fix state check, add empty/null check, improve max length validation

* simplify CW validation by removing minimum length check

* Update CHANGELOG

* remove CW text validation in post()

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-03-10 10:35:37 +00:00
224 changed files with 3992 additions and 3194 deletions

View File

@@ -6,13 +6,17 @@
### Client ### Client
- Feat: 設定の管理が強化されました - Feat: 設定の管理が強化されました
- 自動でバックアップされるように - 自動でバックアップされるように
- 任意の設定項目をデバイス間で同期できるように(実験的)
- Enhance: プラグインの管理が強化されました - Enhance: プラグインの管理が強化されました
- インストール/アンインストール/設定の変更時にリロード不要になりました
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
- Enhance: テーマ設定画面のデザインを改善 - Enhance: テーマ設定画面のデザインを改善
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正 - Fix: テーマ切り替え時に一部の色が変わらない問題を修正
### Server ### Server
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正 - Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
## 2025.3.1 ## 2025.3.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

128
locales/index.d.ts vendored
View File

@@ -5310,6 +5310,130 @@ export interface Locale extends ILocale {
* 復元 * 復元
*/ */
"restore": string; "restore": string;
/**
* デバイス間で同期
*/
"syncBetweenDevices": string;
/**
* サーバーに設定値が存在します
*/
"preferenceSyncConflictTitle": string;
/**
* 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?
*/
"preferenceSyncConflictText": string;
/**
* サーバーの設定値
*/
"preferenceSyncConflictChoiceServer": string;
/**
* デバイスの設定値
*/
"preferenceSyncConflictChoiceDevice": string;
/**
* 同期の有効化をキャンセル
*/
"preferenceSyncConflictChoiceCancel": string;
/**
* ペースト
*/
"paste": string;
/**
* 絵文字パレット
*/
"emojiPalette": string;
/**
* 投稿フォーム
*/
"postForm": string;
"_emojiPalette": {
/**
* パレット
*/
"palettes": string;
/**
* パレットのデバイス間同期を有効にする
*/
"enableSyncBetweenDevicesForPalettes": string;
/**
* メインで使用するパレット
*/
"paletteForMain": string;
/**
* リアクションで使用するパレット
*/
"paletteForReaction": string;
};
"_settings": {
/**
* ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。
*/
"driveBanner": string;
/**
* プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。
*/
"pluginBanner": string;
/**
* サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。
*/
"notificationsBanner": string;
/**
* API
*/
"api": string;
/**
* Webhook
*/
"webhook": string;
/**
* サービス連携
*/
"serviceConnection": string;
/**
* 外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。
*/
"serviceConnectionBanner": string;
/**
* アカウントのデータ
*/
"accountData": string;
/**
* アカウントデータのアーカイブをエクスポート/インポートして管理できます。
*/
"accountDataBanner": string;
/**
* 非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。
*/
"muteAndBlockBanner": string;
/**
* クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。
*/
"accessibilityBanner": string;
/**
* コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。
*/
"privacyBanner": string;
/**
* パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。
*/
"securityBanner": string;
/**
* 好みに応じた、クライアントの全体的な動作の設定が行えます。
*/
"preferencesBanner": string;
/**
* 好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。
*/
"appearanceBanner": string;
/**
* クライアントで再生するサウンドの設定が行えます。
*/
"soundsBanner": string;
/**
* タイムラインとノート
*/
"timelineAndNote": string;
};
"_preferencesProfile": { "_preferencesProfile": {
/** /**
* プロファイル名 * プロファイル名
@@ -9758,6 +9882,10 @@ export interface Locale extends ILocale {
* 幅を自動調整 * 幅を自動調整
*/ */
"flexible": string; "flexible": string;
/**
* プロファイル情報のデバイス間同期を有効にする
*/
"enableSyncBetweenDevicesForProfiles": string;
"_columns": { "_columns": {
/** /**
* メイン * メイン

View File

@@ -1323,6 +1323,40 @@ untitled: "無題"
noName: "名前はありません" noName: "名前はありません"
skip: "スキップ" skip: "スキップ"
restore: "復元" restore: "復元"
syncBetweenDevices: "デバイス間で同期"
preferenceSyncConflictTitle: "サーバーに設定値が存在します"
preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?"
preferenceSyncConflictChoiceServer: "サーバーの設定値"
preferenceSyncConflictChoiceDevice: "デバイスの設定値"
preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
paste: "ペースト"
emojiPalette: "絵文字パレット"
postForm: "投稿フォーム"
_emojiPalette:
palettes: "パレット"
enableSyncBetweenDevicesForPalettes: "パレットのデバイス間同期を有効にする"
paletteForMain: "メインで使用するパレット"
paletteForReaction: "リアクションで使用するパレット"
_settings:
driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。"
pluginBanner: "プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。"
notificationsBanner: "サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。"
api: "API"
webhook: "Webhook"
serviceConnection: "サービス連携"
serviceConnectionBanner: "外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。"
accountData: "アカウントのデータ"
accountDataBanner: "アカウントデータのアーカイブをエクスポート/インポートして管理できます。"
muteAndBlockBanner: "非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。"
accessibilityBanner: "クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。"
privacyBanner: "コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。"
securityBanner: "パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。"
preferencesBanner: "好みに応じた、クライアントの全体的な動作の設定が行えます。"
appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。"
soundsBanner: "クライアントで再生するサウンドの設定が行えます。"
timelineAndNote: "タイムラインとノート"
_preferencesProfile: _preferencesProfile:
profileName: "プロファイル名" profileName: "プロファイル名"
@@ -2579,6 +2613,7 @@ _deck:
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示" useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります" usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
flexible: "幅を自動調整" flexible: "幅を自動調整"
enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする"
_columns: _columns:
main: "メイン" main: "メイン"

View File

@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.3.2-alpha.5", "version": "2025.3.2-alpha.11",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -24,6 +24,7 @@
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
"build-storybook": "pnpm --filter frontend build-storybook", "build-storybook": "pnpm --filter frontend build-storybook",
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
"build-frontend-search-index": "pnpm --filter frontend build-search-index",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
"init": "pnpm migrate", "init": "pnpm migrate",

View File

@@ -16,7 +16,7 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from '@/core/activitypub/type.js'; import type { IObject } from '@/core/activitypub/type.js';
import type { Response } from 'node-fetch'; import type { Response } from 'node-fetch';
import type { URL } from 'node:url'; import type { URL } from 'node:url';
@@ -265,7 +265,7 @@ export class HttpRequestService {
const finalUrl = res.url; // redirects may have been involved const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject; const activity = await res.json() as IObject;
assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail); assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail);
return activity; return activity;
} }

View File

@@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; import { assertActivityMatchesUrl, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from './type.js'; import type { IObject } from './type.js';
type Request = { type Request = {
@@ -258,7 +258,7 @@ export class ApRequestService {
const finalUrl = res.url; // redirects may have been involved const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject; const activity = await res.json() as IObject;
assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail); assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail);
return activity; return activity;
} }

View File

@@ -75,7 +75,7 @@ function normalizeSynonymousSubdomain(url: URL | string): URL {
return new URL(urlParsed.toString().replace(host, normalizedHost)); return new URL(urlParsed.toString().replace(host, normalizedHost));
} }
export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IObject, candidateUrls: (string | URL)[], allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask { export function assertActivityMatchesUrl(requestUrl: string | URL, activity: IObject, finalUrl: string | URL, allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask {
// must have a unique identifier to verify authority // must have a unique identifier to verify authority
if (!activity.id) { if (!activity.id) {
throw new Error('bad Activity: missing id field'); throw new Error('bad Activity: missing id field');
@@ -95,26 +95,32 @@ export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IO
const requestUrlParsed = normalizeSynonymousSubdomain(requestUrl); const requestUrlParsed = normalizeSynonymousSubdomain(requestUrl);
const idParsed = normalizeSynonymousSubdomain(activity.id); const idParsed = normalizeSynonymousSubdomain(activity.id);
const candidateUrlsParsed = candidateUrls.map(it => normalizeSynonymousSubdomain(it)); const finalUrlParsed = normalizeSynonymousSubdomain(finalUrl);
// mastodon sends activities with hash in the URL
// currently it only happens with likes, deletes etc.
// but object ID never has hash
requestUrlParsed.hash = '';
finalUrlParsed.hash = '';
const requestUrlSecure = requestUrlParsed.protocol === 'https:'; const requestUrlSecure = requestUrlParsed.protocol === 'https:';
const finalUrlSecure = candidateUrlsParsed.every(it => it.protocol === 'https:'); const finalUrlSecure = finalUrlParsed.protocol === 'https:';
if (requestUrlSecure && !finalUrlSecure) { if (requestUrlSecure && !finalUrlSecure) {
throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`); throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`);
} }
// Compare final URL to the ID // Compare final URL to the ID
if (!candidateUrlsParsed.some(it => it.href === idParsed.href)) { if (finalUrlParsed.href !== idParsed.href) {
requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${candidateUrlsParsed.map(it => it.toString())})`); requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${finalUrlParsed.toString()})`);
// at lease host need to match exactly (ActivityPub requirement) // at lease host need to match exactly (ActivityPub requirement)
if (!candidateUrlsParsed.some(it => idParsed.host === it.host)) { if (idParsed.host !== finalUrlParsed.host) {
throw new Error(`bad Activity: id(${activity.id}) does not match response host(${candidateUrlsParsed.map(it => it.host)})`); throw new Error(`bad Activity: id(${activity.id}) does not match response host(${finalUrlParsed.host})`);
} }
} }
// Compare request URL to the ID // Compare request URL to the ID
if (!requestUrlParsed.href.includes(idParsed.href)) { if (requestUrlParsed.href !== idParsed.href) {
requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match request url(${requestUrlParsed.toString()})`); requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match request url(${requestUrlParsed.toString()})`);
// if cross-origin lookup is allowed, we can accept some variation between the original request URL to the final object ID (but not between the final URL and the object ID) // if cross-origin lookup is allowed, we can accept some variation between the original request URL to the final object ID (but not between the final URL and the object ID)

View File

@@ -8,7 +8,7 @@ import httpSignature from '@peertube/http-signature';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import { IObject } from '@/core/activitypub/type.js'; import { IObject } from '@/core/activitypub/type.js';
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
@@ -66,23 +66,26 @@ describe('ap-request', () => {
}); });
test('rejects non matching domain', () => { test('rejects non matching domain', () => {
assert.doesNotThrow(() => assertActivityMatchesUrls( assert.doesNotThrow(() => assertActivityMatchesUrl(
'https://alice.example.com/abc', 'https://alice.example.com/abc',
{ id: 'https://alice.example.com/abc' } as IObject, { id: 'https://alice.example.com/abc' } as IObject,
[
'https://alice.example.com/abc', 'https://alice.example.com/abc',
],
FetchAllowSoftFailMask.Strict, FetchAllowSoftFailMask.Strict,
), 'validation should pass base case'); ), 'validation should pass base case');
assert.throws(() => assertActivityMatchesUrls( assert.throws(() => assertActivityMatchesUrl(
'https://alice.example.com/abc', 'https://alice.example.com/abc',
{ id: 'https://bob.example.com/abc' } as IObject, { id: 'https://bob.example.com/abc' } as IObject,
[
'https://alice.example.com/abc', 'https://alice.example.com/abc',
],
FetchAllowSoftFailMask.Any, FetchAllowSoftFailMask.Any,
), 'validation should fail no matter what if the response URL is inconsistent with the object ID'); ), 'validation should fail no matter what if the response URL is inconsistent with the object ID');
assert.doesNotThrow(() => assertActivityMatchesUrl(
'https://alice.example.com/abc#test',
{ id: 'https://alice.example.com/abc' } as IObject,
'https://alice.example.com/abc',
FetchAllowSoftFailMask.Strict,
), 'validation should pass with hash in request URL');
// fix issues like threads // fix issues like threads
// https://github.com/misskey-dev/misskey/issues/15039 // https://github.com/misskey-dev/misskey/issues/15039
const withOrWithoutWWW = [ const withOrWithoutWWW = [
@@ -97,89 +100,71 @@ describe('ap-request', () => {
), ),
withOrWithoutWWW, withOrWithoutWWW,
).forEach(([[a, b], c]) => { ).forEach(([[a, b], c]) => {
assert.doesNotThrow(() => assertActivityMatchesUrls( assert.doesNotThrow(() => assertActivityMatchesUrl(
a, a,
{ id: b } as IObject, { id: b } as IObject,
[
c, c,
],
FetchAllowSoftFailMask.Strict, FetchAllowSoftFailMask.Strict,
), 'validation should pass with or without www. subdomain'); ), 'validation should pass with or without www. subdomain');
}); });
}); });
test('cross origin lookup', () => { test('cross origin lookup', () => {
assert.doesNotThrow(() => assertActivityMatchesUrls( assert.doesNotThrow(() => assertActivityMatchesUrl(
'https://alice.example.com/abc', 'https://alice.example.com/abc',
{ id: 'https://bob.example.com/abc' } as IObject, { id: 'https://bob.example.com/abc' } as IObject,
[
'https://bob.example.com/abc', 'https://bob.example.com/abc',
],
FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId,
), 'validation should pass if the response is otherwise consistent and cross-origin is allowed'); ), 'validation should pass if the response is otherwise consistent and cross-origin is allowed');
assert.throws(() => assertActivityMatchesUrls( assert.throws(() => assertActivityMatchesUrl(
'https://alice.example.com/abc', 'https://alice.example.com/abc',
{ id: 'https://bob.example.com/abc' } as IObject, { id: 'https://bob.example.com/abc' } as IObject,
[
'https://bob.example.com/abc', 'https://bob.example.com/abc',
],
FetchAllowSoftFailMask.Strict, FetchAllowSoftFailMask.Strict,
), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed'); ), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed');
}); });
test('rejects non-canonical ID', () => { test('rejects non-canonical ID', () => {
assert.throws(() => assertActivityMatchesUrls( assert.throws(() => assertActivityMatchesUrl(
'https://alice.example.com/@alice', 'https://alice.example.com/@alice',
{ id: 'https://alice.example.com/users/alice' } as IObject, { id: 'https://alice.example.com/users/alice' } as IObject,
[ 'https://alice.example.com/users/alice',
'https://alice.example.com/users/alice'
],
FetchAllowSoftFailMask.Strict, FetchAllowSoftFailMask.Strict,
), 'throws if the response ID did not exactly match the expected ID'); ), 'throws if the response ID did not exactly match the expected ID');
assert.doesNotThrow(() => assertActivityMatchesUrls( assert.doesNotThrow(() => assertActivityMatchesUrl(
'https://alice.example.com/@alice', 'https://alice.example.com/@alice',
{ id: 'https://alice.example.com/users/alice' } as IObject, { id: 'https://alice.example.com/users/alice' } as IObject,
[
'https://alice.example.com/users/alice', 'https://alice.example.com/users/alice',
],
FetchAllowSoftFailMask.NonCanonicalId, FetchAllowSoftFailMask.NonCanonicalId,
), 'does not throw if non-canonical ID is allowed'); ), 'does not throw if non-canonical ID is allowed');
}); });
test('origin relaxed alignment', () => { test('origin relaxed alignment', () => {
assert.doesNotThrow(() => assertActivityMatchesUrls( assert.doesNotThrow(() => assertActivityMatchesUrl(
'https://alice.example.com/abc', 'https://alice.example.com/abc',
{ id: 'https://ap.alice.example.com/abc' } as IObject, { id: 'https://ap.alice.example.com/abc' } as IObject,
[
'https://ap.alice.example.com/abc', 'https://ap.alice.example.com/abc',
],
FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId, FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
), 'validation should pass if response is a subdomain of the expected origin'); ), 'validation should pass if response is a subdomain of the expected origin');
assert.throws(() => assertActivityMatchesUrls( assert.throws(() => assertActivityMatchesUrl(
'https://alice.multi-tenant.example.com/abc', 'https://alice.multi-tenant.example.com/abc',
{ id: 'https://alice.multi-tenant.example.com/abc' } as IObject, { id: 'https://alice.multi-tenant.example.com/abc' } as IObject,
[
'https://bob.multi-tenant.example.com/abc', 'https://bob.multi-tenant.example.com/abc',
],
FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId, FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
), 'validation should fail if response is a disjoint domain of the expected origin'); ), 'validation should fail if response is a disjoint domain of the expected origin');
assert.throws(() => assertActivityMatchesUrls( assert.throws(() => assertActivityMatchesUrl(
'https://alice.example.com/abc', 'https://alice.example.com/abc',
{ id: 'https://ap.alice.example.com/abc' } as IObject, { id: 'https://ap.alice.example.com/abc' } as IObject,
[
'https://ap.alice.example.com/abc', 'https://ap.alice.example.com/abc',
],
FetchAllowSoftFailMask.Strict, FetchAllowSoftFailMask.Strict,
), 'throws if relaxed origin is forbidden'); ), 'throws if relaxed origin is forbidden');
}); });
test('resist HTTP downgrade', () => { test('resist HTTP downgrade', () => {
assert.throws(() => assertActivityMatchesUrls( assert.throws(() => assertActivityMatchesUrl(
'https://alice.example.com/abc', 'https://alice.example.com/abc',
{ id: 'https://alice.example.com/abc' } as IObject, { id: 'https://alice.example.com/abc' } as IObject,
[
'http://alice.example.com/abc', 'http://alice.example.com/abc',
],
FetchAllowSoftFailMask.Strict, FetchAllowSoftFailMask.Strict,
), 'throws if HTTP downgrade is detected'); ), 'throws if HTTP downgrade is detected');
}); });

View File

@@ -17,8 +17,52 @@ interface SatisfiesExpression extends estree.BaseExpression {
reference: estree.Identifier; reference: estree.Identifier;
} }
interface ImportDeclaration extends estree.ImportDeclaration {
kind?: 'type';
}
const generator = { const generator = {
...GENERATOR, ...GENERATOR,
ImportDeclaration(node: ImportDeclaration, state: State) {
state.write('import ');
if (node.kind === 'type') state.write('type ');
const { specifiers } = node;
if (specifiers.length > 0) {
let i = 0;
for (; i < specifiers.length; i++) {
if (i > 0) {
state.write(', ');
}
const specifier = specifiers[i]!;
if (specifier.type === 'ImportDefaultSpecifier') {
state.write(specifier.local.name, specifier);
} else if (specifier.type === 'ImportNamespaceSpecifier') {
state.write(`* as ${specifier.local.name}`, specifier);
} else {
break;
}
}
if (i < specifiers.length) {
state.write('{');
for (; i < specifiers.length; i++) {
const specifier = specifiers[i]! as estree.ImportSpecifier;
const { name } = specifier.imported as estree.Identifier;
state.write(name, specifier);
if (name !== specifier.local.name) {
state.write(` as ${specifier.local.name}`);
}
if (i < specifiers.length - 1) {
state.write(', ');
}
}
state.write('}');
}
state.write(' from ');
}
this.Literal(node.source, state);
state.write(';');
},
SatisfiesExpression(node: SatisfiesExpression, state: State) { SatisfiesExpression(node: SatisfiesExpression, state: State) {
switch (node.expression.type) { switch (node.expression.type) {
case 'ArrowFunctionExpression': { case 'ArrowFunctionExpression': {
@@ -155,7 +199,8 @@ function toStories(component: string): Promise<string> {
/> as estree.ImportSpecifier, /> as estree.ImportSpecifier,
]), ]),
]} ]}
/> as estree.ImportDeclaration, kind={'type'}
/> as ImportDeclaration,
...(hasMsw ...(hasMsw
? [ ? [
<import-declaration <import-declaration
@@ -165,7 +210,7 @@ function toStories(component: string): Promise<string> {
local={<identifier name='msw' /> as estree.Identifier} local={<identifier name='msw' /> as estree.Identifier}
/> as estree.ImportNamespaceSpecifier, /> as estree.ImportNamespaceSpecifier,
]} ]}
/> as estree.ImportDeclaration, /> as ImportDeclaration,
] ]
: []), : []),
...(hasImplStories ...(hasImplStories
@@ -176,7 +221,7 @@ function toStories(component: string): Promise<string> {
specifiers={[ specifiers={[
<import-default-specifier local={identifier} /> as estree.ImportDefaultSpecifier, <import-default-specifier local={identifier} /> as estree.ImportDefaultSpecifier,
]} ]}
/> as estree.ImportDeclaration, /> as ImportDeclaration,
]), ]),
...(hasMetaStories ...(hasMetaStories
? [ ? [
@@ -187,7 +232,7 @@ function toStories(component: string): Promise<string> {
local={<identifier name='storiesMeta' /> as estree.Identifier} local={<identifier name='storiesMeta' /> as estree.Identifier}
/> as estree.ImportNamespaceSpecifier, /> as estree.ImportNamespaceSpecifier,
]} ]}
/> as estree.ImportDeclaration, /> as ImportDeclaration,
] ]
: []), : []),
<variable-declaration <variable-declaration

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1428,6 +1428,23 @@ async function processVueFile(
}; };
} }
export async function generateSearchIndex(options: Options, transformedCodeCache: Record<string, string> = {}) {
const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => {
const matchedFiles = glob.sync(filePathPattern);
return [...acc, ...matchedFiles];
}, []);
for (const filePath of filePaths) {
const id = path.resolve(filePath); // 絶対パスに変換
const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む
const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す
transformedCodeCache = newCache; // キャッシュを更新
}
await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行
return transformedCodeCache; // キャッシュを返す
}
// Rollup プラグインとして export // Rollup プラグインとして export
export default function pluginCreateSearchIndex(options: Options): Plugin { export default function pluginCreateSearchIndex(options: Options): Plugin {
@@ -1445,19 +1462,7 @@ export default function pluginCreateSearchIndex(options: Options): Plugin {
return; return;
} }
const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => { transformedCodeCache = await generateSearchIndex(options, transformedCodeCache);
const matchedFiles = glob.sync(filePathPattern);
return [...acc, ...matchedFiles];
}, []);
for (const filePath of filePaths) {
const id = path.resolve(filePath); // 絶対パスに変換
const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む
const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す
transformedCodeCache = newCache; // キャッシュを更新
}
await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行
}, },
async transform(code, id) { async transform(code, id) {

View File

@@ -5,6 +5,7 @@
"scripts": { "scripts": {
"watch": "vite", "watch": "vite",
"build": "vite build", "build": "vite build",
"build-search-index": "vite-node --config \"./vite-node.config.ts\" \"./scripts/generate-search-index.ts\"",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static", "build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
@@ -133,6 +134,7 @@
"start-server-and-test": "2.0.10", "start-server-and-test": "2.0.10",
"storybook": "8.6.4", "storybook": "8.6.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-node": "3.0.8",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "3.0.8", "vitest": "3.0.8",
"vitest-fetch-mock": "0.4.5", "vitest-fetch-mock": "0.4.5",

View File

@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { searchIndexes } from '../vite.config.js';
import { generateSearchIndex } from '../lib/vite-plugin-create-search-index.js';
async function main() {
for (const searchIndex of searchIndexes) {
await generateSearchIndex(searchIndex);
}
}
main();

View File

@@ -3,116 +3,55 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { defineAsyncComponent, reactive, ref } from 'vue'; import { defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { apiUrl } from '@@/js/config.js'; import { apiUrl, host } from '@@/js/config.js';
import type { MenuItem, MenuButton } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import { defaultMemoryStorage } from '@/memory-storage';
import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js'; import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { del, get, set } from '@/utility/idb-proxy.js';
import { waiting, popup, popupMenu, success, alert } from '@/os.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { unisonReload, reloadChannel } from '@/utility/unison-reload.js'; import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
import { prefer } from '@/preferences.js';
import { store } from '@/store.js';
import { $i } from '@/i.js';
import { signout } from '@/signout.js';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
type Account = Misskey.entities.MeDetailed & { token: string }; type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
const accountData = miLocalStorage.getItem('account'); export async function getAccounts(): Promise<{
host: string;
// TODO: 外部からはreadonlyに user: Misskey.entities.User;
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; token: string | null;
}[]> {
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true); const tokens = store.s.accountTokens;
export const iAmAdmin = $i != null && $i.isAdmin; const accounts = prefer.s.accounts;
return accounts.map(([host, user]) => ({
export function signinRequired() { host,
if ($i == null) throw new Error('signin required'); user,
return $i; token: tokens[host + '/' + user.id] ?? null,
}));
} }
export let notesCount = $i == null ? 0 : $i.notesCount; async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) {
export function incNotesCount() { if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) {
notesCount++; store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
} prefer.commit('accounts', [...prefer.s.accounts, [host, user]]);
export async function signout() {
if (!$i) return;
defaultMemoryStorage.clear();
waiting();
document.cookie.split(';').forEach((cookie) => {
const cookieName = cookie.split('=')[0].trim();
if (cookieName === 'token') {
document.cookie = `${cookieName}=; max-age=0; path=/`;
}
});
miLocalStorage.removeItem('account');
await removeAccount($i.id);
const accounts = await getAccounts();
//#region Remove service worker registration
try {
if (navigator.serviceWorker.controller) {
const registration = await navigator.serviceWorker.ready;
const push = await registration.pushManager.getSubscription();
if (push) {
await window.fetch(`${apiUrl}/sw/unregister`, {
method: 'POST',
body: JSON.stringify({
i: $i.token,
endpoint: push.endpoint,
}),
headers: {
'Content-Type': 'application/json',
},
});
} }
} }
if (accounts.length === 0) { export async function removeAccount(host: string, id: AccountWithToken['id']) {
await navigator.serviceWorker.getRegistrations() const tokens = JSON.parse(JSON.stringify(store.s.accountTokens));
.then(registrations => { delete tokens[host + '/' + id];
return Promise.all(registrations.map(registration => registration.unregister())); store.set('accountTokens', tokens);
}); prefer.commit('accounts', prefer.s.accounts.filter(x => x[0] !== host || x[1].id !== id));
}
} catch (err) {}
//#endregion
if (accounts.length > 0) login(accounts[0].token);
else unisonReload('/');
} }
export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> { const isAccountDeleted = Symbol('isAccountDeleted');
return (await get('accounts')) || [];
}
export async function addAccount(id: Account['id'], token: Account['token']) {
const accounts = await getAccounts();
if (!accounts.some(x => x.id === id)) {
await set('accounts', accounts.concat([{ id, token }]));
}
}
export async function removeAccount(idOrToken: Account['id']) {
const accounts = await getAccounts();
const i = accounts.findIndex(x => x.id === idOrToken || x.token === idOrToken);
if (i !== -1) accounts.splice(i, 1);
if (accounts.length > 0) {
await set('accounts', accounts);
} else {
await del('accounts');
}
}
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> {
document.cookie = 'token=; path=/; max-age=0';
document.cookie = `token=${token}; path=/queue; max-age=86400; SameSite=Strict; Secure`; // bull dashboardの認証とかで使う
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Misskey.entities.MeDetailed> {
return new Promise((done, fail) => { return new Promise((done, fail) => {
window.fetch(`${apiUrl}/i`, { window.fetch(`${apiUrl}/i`, {
method: 'POST', method: 'POST',
@@ -123,7 +62,7 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}) })
.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => { .then(res => new Promise<Misskey.entities.MeDetailed | { error: Record<string, any> }>((done2, fail2) => {
if (res.status >= 500 && res.status < 600) { if (res.status >= 500 && res.status < 600) {
// サーバーエラー(5xx)の場合をrejectとする // サーバーエラー(5xx)の場合をrejectとする
// 認証エラーなど4xxはresolve // 認証エラーなど4xxはresolve
@@ -166,46 +105,70 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
}); });
} }
// rejectかつ理由がtrueの場合、削除対象であることを示す fail(isAccountDeleted);
fail(true);
} else { } else {
(res as Account).token = token; done(res);
done(res as Account);
} }
}) })
.catch(fail); .catch(fail);
}); });
} }
export function updateAccount(accountData: Account) { export function updateCurrentAccount(accountData: Misskey.entities.MeDetailed) {
if (!$i) return; if (!$i) return;
const token = $i.token;
for (const key of Object.keys($i)) { for (const key of Object.keys($i)) {
delete $i[key]; delete $i[key];
} }
for (const [key, value] of Object.entries(accountData)) { for (const [key, value] of Object.entries(accountData)) {
$i[key] = value; $i[key] = value;
} }
prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
// TODO: $iのホストも比較したいけど通常null
if (user.id === $i.id) {
return [host, $i];
} else {
return [host, user];
}
}));
$i.token = token;
miLocalStorage.setItem('account', JSON.stringify($i)); miLocalStorage.setItem('account', JSON.stringify($i));
} }
export function updateAccountPartial(accountData: Partial<Account>) { export function updateCurrentAccountPartial(accountData: Partial<Misskey.entities.MeDetailed>) {
if (!$i) return; if (!$i) return;
for (const [key, value] of Object.entries(accountData)) { for (const [key, value] of Object.entries(accountData)) {
$i[key] = value; $i[key] = value;
} }
prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
// TODO: $iのホストも比較したいけど通常null
if (user.id === $i.id) {
const newUser = JSON.parse(JSON.stringify($i));
for (const [key, value] of Object.entries(accountData)) {
newUser[key] = value;
}
return [host, newUser];
}
return [host, user];
}));
miLocalStorage.setItem('account', JSON.stringify($i)); miLocalStorage.setItem('account', JSON.stringify($i));
} }
export async function refreshAccount() { export async function refreshCurrentAccount() {
if (!$i) return; if (!$i) return;
return fetchAccount($i.token, $i.id) return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => {
.then(updateAccount, reason => { if (reason === isAccountDeleted) {
if (reason === true) return signout(); removeAccount(host, $i.id);
return; if (Object.keys(store.s.accountTokens).length > 0) {
login(Object.values(store.s.accountTokens)[0]);
} else {
signout();
}
}
}); });
} }
export async function login(token: Account['token'], redirect?: string) { export async function login(token: AccountWithToken['token'], redirect?: string) {
const showing = ref(true); const showing = ref(true);
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
success: false, success: false,
@@ -213,19 +176,18 @@ export async function login(token: Account['token'], redirect?: string) {
}, { }, {
closed: () => dispose(), closed: () => dispose(),
}); });
if (_DEV_) console.log('logging as token ', token);
const me = await fetchAccount(token, undefined, true)
.catch(reason => {
if (reason === true) {
// 削除対象の場合
removeAccount(token);
}
const me = await fetchAccount(token, undefined, true).catch(reason => {
showing.value = false; showing.value = false;
throw reason; throw reason;
}); });
miLocalStorage.setItem('account', JSON.stringify(me));
await addAccount(me.id, token); miLocalStorage.setItem('account', JSON.stringify({
...me,
token,
}));
await addAccount(host, me, token);
if (redirect) { if (redirect) {
// 他のタブは再読み込みするだけ // 他のタブは再読み込みするだけ
@@ -238,60 +200,51 @@ export async function login(token: Account['token'], redirect?: string) {
unisonReload(); unisonReload();
} }
export async function switchAccount(host: string, id: string) {
const token = store.s.accountTokens[host + '/' + id];
if (token) {
login(token);
} else {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i });
login(res.i);
},
closed: () => {
dispose();
},
});
}
}
export async function openAccountMenu(opts: { export async function openAccountMenu(opts: {
includeCurrentAccount?: boolean; includeCurrentAccount?: boolean;
withExtraOperation: boolean; withExtraOperation: boolean;
active?: Misskey.entities.UserDetailed['id']; active?: Misskey.entities.User['id'];
onChoose?: (account: Misskey.entities.UserDetailed) => void; onChoose?: (account: Misskey.entities.User) => void;
}, ev: MouseEvent) { }, ev: MouseEvent) {
if (!$i) return; if (!$i) return;
async function switchAccount(account: Misskey.entities.UserDetailed) { function createItem(host: string, account: Misskey.entities.User): MenuItem {
const storedAccounts = await getAccounts();
const found = storedAccounts.find(x => x.id === account.id);
if (found == null) return;
switchAccountWithToken(found.token);
}
function switchAccountWithToken(token: string) {
login(token);
}
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) });
function createItem(account: Misskey.entities.UserDetailed) {
return { return {
type: 'user' as const, type: 'user' as const,
user: account, user: account,
active: opts.active != null ? opts.active === account.id : false, active: opts.active != null ? opts.active === account.id : false,
action: () => { action: async () => {
if (opts.onChoose) { if (opts.onChoose) {
opts.onChoose(account); opts.onChoose(account);
} else { } else {
switchAccount(account); switchAccount(host, account.id);
} }
}, },
}; };
} }
const accountItemPromises = storedAccounts.map(a => new Promise<ReturnType<typeof createItem> | MenuButton>(res => {
accountsPromise.then(accounts => {
const account = accounts.find(x => x.id === a.id);
if (account == null) return res({
type: 'button' as const,
text: a.id,
action: () => {
switchAccountWithToken(a.token);
},
});
res(createItem(account));
});
}));
const menuItems: MenuItem[] = []; const menuItems: MenuItem[] = [];
// TODO: $iのホストも比較したいけど通常null
const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.user.id !== $i.id))).map(a => createItem(a.host, a.user));
if (opts.withExtraOperation) { if (opts.withExtraOperation) {
menuItems.push({ menuItems.push({
type: 'link', type: 'link',
@@ -303,10 +256,10 @@ export async function openAccountMenu(opts: {
}); });
if (opts.includeCurrentAccount) { if (opts.includeCurrentAccount) {
menuItems.push(createItem($i)); menuItems.push(createItem(host, $i));
} }
menuItems.push(...accountItemPromises); menuItems.push(...accountItems);
menuItems.push({ menuItems.push({
type: 'parent', type: 'parent',
@@ -326,7 +279,7 @@ export async function openAccountMenu(opts: {
action: () => { action: () => {
getAccountWithSignupDialog().then(res => { getAccountWithSignupDialog().then(res => {
if (res != null) { if (res != null) {
switchAccountWithToken(res.token); switchAccount(host, res.id);
} }
}); });
}, },
@@ -339,10 +292,10 @@ export async function openAccountMenu(opts: {
}); });
} else { } else {
if (opts.includeCurrentAccount) { if (opts.includeCurrentAccount) {
menuItems.push(createItem($i)); menuItems.push(createItem(host, $i));
} }
menuItems.push(...accountItemPromises); menuItems.push(...accountItems);
} }
popupMenu(menuItems, ev.currentTarget ?? ev.target, { popupMenu(menuItems, ev.currentTarget ?? ev.target, {
@@ -354,7 +307,8 @@ export function getAccountWithSigninDialog(): Promise<{ id: string, token: strin
return new Promise((resolve) => { return new Promise((resolve) => {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => { done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
await addAccount(res.id, res.i); const user = await fetchAccount(res.i, res.id, true);
await addAccount(host, user, res.i);
resolve({ id: res.id, token: res.i }); resolve({ id: res.id, token: res.i });
}, },
cancelled: () => { cancelled: () => {
@@ -371,7 +325,9 @@ export function getAccountWithSignupDialog(): Promise<{ id: string, token: strin
return new Promise((resolve) => { return new Promise((resolve) => {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
done: async (res: Misskey.entities.SignupResponse) => { done: async (res: Misskey.entities.SignupResponse) => {
await addAccount(res.id, res.token); const user = JSON.parse(JSON.stringify(res));
delete user.token;
await addAccount(host, user, res.token);
resolve({ id: res.id, token: res.token }); resolve({ id: res.id, token: res.token });
}, },
cancelled: () => { cancelled: () => {
@@ -383,7 +339,3 @@ export function getAccountWithSignupDialog(): Promise<{ id: string, token: strin
}); });
}); });
} }
if (_DEV_) {
(window as any).$i = $i;
}

View File

@@ -9,7 +9,7 @@ import { url, lang } from '@@/js/config.js';
import { assertStringAndIsIn } from './common.js'; import { assertStringAndIsIn } from './common.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js'; import { customEmojis } from '@/custom-emojis.js';

View File

@@ -15,7 +15,7 @@ import components from '@/components/index.js';
import { applyTheme } from '@/theme.js'; import { applyTheme } from '@/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js'; import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { updateI18n, i18n } from '@/i18n.js'; import { updateI18n, i18n } from '@/i18n.js';
import { $i, refreshAccount, login } from '@/account.js'; import { refreshCurrentAccount, login } from '@/accounts.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js'; import { fetchInstance, instance } from '@/instance.js';
import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js'; import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js';
@@ -29,6 +29,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js';
import { setupRouter } from '@/router/main.js'; import { setupRouter } from '@/router/main.js';
import { createMainRouter } from '@/router/definition.js'; import { createMainRouter } from '@/router/definition.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { $i } from '@/i.js';
export async function common(createVue: () => App<Element>) { export async function common(createVue: () => App<Element>) {
console.info(`Misskey v${version}`); console.info(`Misskey v${version}`);
@@ -38,11 +39,6 @@ export async function common(createVue: () => App<Element>) {
console.info(`vue ${vueVersion}`); console.info(`vue ${vueVersion}`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).$i = $i;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).$store = store;
window.addEventListener('error', event => { window.addEventListener('error', event => {
console.error(event); console.error(event);
/* /*
@@ -244,7 +240,7 @@ export async function common(createVue: () => App<Element>) {
console.log('account cache found. refreshing...'); console.log('account cache found. refreshing...');
} }
refreshAccount(); refreshCurrentAccount();
} }
//#endregion //#endregion
@@ -326,6 +322,7 @@ export async function common(createVue: () => App<Element>) {
return { return {
isClientUpdated, isClientUpdated,
lastVersion,
app, app,
}; };
} }

View File

@@ -6,14 +6,17 @@
import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { createApp, defineAsyncComponent, markRaw } from 'vue';
import { ui } from '@@/js/config.js'; import { ui } from '@@/js/config.js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid';
import { compareVersions } from 'compare-versions';
import { common } from './common.js'; import { common } from './common.js';
import type { Component } from 'vue'; import type { Component } from 'vue';
import type { Keymap } from '@/utility/hotkey.js'; import type { Keymap } from '@/utility/hotkey.js';
import type { DeckProfile } from '@/deck.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { alert, confirm, popup, post, toast } from '@/os.js'; import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
import { $i, signout, updateAccountPartial } from '@/account.js'; import { $i } from '@/i.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { ColdDeviceStorage, store } from '@/store.js'; import { ColdDeviceStorage, store } from '@/store.js';
import { reactionPicker } from '@/utility/reaction-picker.js'; import { reactionPicker } from '@/utility/reaction-picker.js';
@@ -28,9 +31,12 @@ import { prefer } from '@/preferences.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { deckStore } from '@/ui/deck/deck-store.js'; import { deckStore } from '@/ui/deck/deck-store.js';
import { launchPlugins } from '@/plugin.js'; import { launchPlugins } from '@/plugin.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { updateCurrentAccountPartial } from '@/accounts.js';
import { signout } from '@/signout.js';
export async function mainBoot() { export async function mainBoot() {
const { isClientUpdated } = await common(() => { const { isClientUpdated, lastVersion } = await common(() => {
let uiStyle = ui; let uiStyle = ui;
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
@@ -70,6 +76,137 @@ export async function mainBoot() {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {
closed: () => dispose(), closed: () => dispose(),
}); });
// prefereces migration
// TODO: そのうち消す
if (lastVersion && (compareVersions('2025.3.2-alpha.0', lastVersion) === 1)) {
console.log('Preferences migration');
store.loaded.then(async () => {
const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []);
if (themes.length > 0) {
prefer.commit('themes', themes);
}
const plugins = ColdDeviceStorage.get('plugins');
prefer.commit('plugins', plugins.map(p => ({
...p,
installId: (p as any).id,
id: undefined,
})));
prefer.commit('deck.profile', deckStore.s.profile);
misskeyApi('i/registry/keys', {
scope: ['client', 'deck', 'profiles'],
}).then(async keys => {
const profiles: DeckProfile[] = [];
for (const key of keys) {
const deck = await misskeyApi('i/registry/get', {
scope: ['client', 'deck', 'profiles'],
key: key,
});
profiles.push({
id: uuid(),
name: key,
columns: deck.columns,
layout: deck.layout,
});
}
prefer.commit('deck.profiles', profiles);
});
prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme'));
prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme'));
prefer.commit('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
prefer.commit('emojiPalettes', [{
id: 'reactions',
name: '',
emojis: store.s.reactions,
}, {
id: 'pinnedEmojis',
name: '',
emojis: store.s.pinnedEmojis,
}]);
prefer.commit('emojiPaletteForMain', 'pinnedEmojis');
prefer.commit('emojiPaletteForReaction', 'reactions');
prefer.commit('overridedDeviceKind', store.s.overridedDeviceKind);
prefer.commit('widgets', store.s.widgets);
prefer.commit('keepCw', store.s.keepCw);
prefer.commit('collapseRenotes', store.s.collapseRenotes);
prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility);
prefer.commit('uploadFolder', store.s.uploadFolder);
prefer.commit('keepOriginalUploading', store.s.keepOriginalUploading);
prefer.commit('menu', store.s.menu);
prefer.commit('statusbars', store.s.statusbars);
prefer.commit('pinnedUserLists', store.s.pinnedUserLists);
prefer.commit('serverDisconnectedBehavior', store.s.serverDisconnectedBehavior);
prefer.commit('nsfw', store.s.nsfw);
prefer.commit('highlightSensitiveMedia', store.s.highlightSensitiveMedia);
prefer.commit('animation', store.s.animation);
prefer.commit('animatedMfm', store.s.animatedMfm);
prefer.commit('advancedMfm', store.s.advancedMfm);
prefer.commit('showReactionsCount', store.s.showReactionsCount);
prefer.commit('enableQuickAddMfmFunction', store.s.enableQuickAddMfmFunction);
prefer.commit('loadRawImages', store.s.loadRawImages);
prefer.commit('imageNewTab', store.s.imageNewTab);
prefer.commit('disableShowingAnimatedImages', store.s.disableShowingAnimatedImages);
prefer.commit('emojiStyle', store.s.emojiStyle);
prefer.commit('menuStyle', store.s.menuStyle);
prefer.commit('useBlurEffectForModal', store.s.useBlurEffectForModal);
prefer.commit('useBlurEffect', store.s.useBlurEffect);
prefer.commit('showFixedPostForm', store.s.showFixedPostForm);
prefer.commit('showFixedPostFormInChannel', store.s.showFixedPostFormInChannel);
prefer.commit('enableInfiniteScroll', store.s.enableInfiniteScroll);
prefer.commit('useReactionPickerForContextMenu', store.s.useReactionPickerForContextMenu);
prefer.commit('showGapBetweenNotesInTimeline', store.s.showGapBetweenNotesInTimeline);
prefer.commit('instanceTicker', store.s.instanceTicker);
prefer.commit('emojiPickerScale', store.s.emojiPickerScale);
prefer.commit('emojiPickerWidth', store.s.emojiPickerWidth);
prefer.commit('emojiPickerHeight', store.s.emojiPickerHeight);
prefer.commit('emojiPickerStyle', store.s.emojiPickerStyle);
prefer.commit('reportError', store.s.reportError);
prefer.commit('squareAvatars', store.s.squareAvatars);
prefer.commit('showAvatarDecorations', store.s.showAvatarDecorations);
prefer.commit('numberOfPageCache', store.s.numberOfPageCache);
prefer.commit('showNoteActionsOnlyHover', store.s.showNoteActionsOnlyHover);
prefer.commit('showClipButtonInNoteFooter', store.s.showClipButtonInNoteFooter);
prefer.commit('reactionsDisplaySize', store.s.reactionsDisplaySize);
prefer.commit('limitWidthOfReaction', store.s.limitWidthOfReaction);
prefer.commit('forceShowAds', store.s.forceShowAds);
prefer.commit('aiChanMode', store.s.aiChanMode);
prefer.commit('devMode', store.s.devMode);
prefer.commit('mediaListWithOneImageAppearance', store.s.mediaListWithOneImageAppearance);
prefer.commit('notificationPosition', store.s.notificationPosition);
prefer.commit('notificationStackAxis', store.s.notificationStackAxis);
prefer.commit('enableCondensedLine', store.s.enableCondensedLine);
prefer.commit('keepScreenOn', store.s.keepScreenOn);
prefer.commit('disableStreamingTimeline', store.s.disableStreamingTimeline);
prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications);
prefer.commit('dataSaver', store.s.dataSaver);
prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect);
prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe);
prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer);
prefer.commit('keepOriginalFilename', store.s.keepOriginalFilename);
prefer.commit('alwaysConfirmFollow', store.s.alwaysConfirmFollow);
prefer.commit('confirmWhenRevealingSensitiveMedia', store.s.confirmWhenRevealingSensitiveMedia);
prefer.commit('contextMenu', store.s.contextMenu);
prefer.commit('skipNoteRender', store.s.skipNoteRender);
prefer.commit('showSoftWordMutedWord', store.s.showSoftWordMutedWord);
prefer.commit('confirmOnReact', store.s.confirmOnReact);
prefer.commit('defaultFollowWithReplies', store.s.defaultWithReplies);
prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
prefer.commit('sound.on.note', store.s.sound_note as any);
prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any);
prefer.commit('sound.on.notification', store.s.sound_notification as any);
prefer.commit('sound.on.reaction', store.s.sound_reaction as any);
window.setTimeout(() => {
unisonReload();
}, 5000);
});
}
} }
const stream = useStream(); const stream = useStream();
@@ -136,99 +273,6 @@ export async function mainBoot() {
if ($i) { if ($i) {
store.loaded.then(async () => { store.loaded.then(async () => {
// prefereces migration
// TODO: そのうち消す
if (store.s.menu.length > 0) {
const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []);
if (themes.length > 0) {
prefer.commit('themes', themes);
}
const plugins = ColdDeviceStorage.get('plugins');
prefer.commit('plugins', plugins.map(p => ({
...p,
installId: (p as any).id,
id: undefined,
})));
prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme'));
prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme'));
prefer.commit('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
prefer.commit('overridedDeviceKind', store.s.overridedDeviceKind);
prefer.commit('widgets', store.s.widgets);
prefer.commit('keepCw', store.s.keepCw);
prefer.commit('collapseRenotes', store.s.collapseRenotes);
prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility);
prefer.commit('uploadFolder', store.s.uploadFolder);
prefer.commit('keepOriginalUploading', store.s.keepOriginalUploading);
prefer.commit('menu', store.s.menu);
prefer.commit('statusbars', store.s.statusbars);
prefer.commit('pinnedUserLists', store.s.pinnedUserLists);
prefer.commit('serverDisconnectedBehavior', store.s.serverDisconnectedBehavior);
prefer.commit('nsfw', store.s.nsfw);
prefer.commit('highlightSensitiveMedia', store.s.highlightSensitiveMedia);
prefer.commit('animation', store.s.animation);
prefer.commit('animatedMfm', store.s.animatedMfm);
prefer.commit('advancedMfm', store.s.advancedMfm);
prefer.commit('showReactionsCount', store.s.showReactionsCount);
prefer.commit('enableQuickAddMfmFunction', store.s.enableQuickAddMfmFunction);
prefer.commit('loadRawImages', store.s.loadRawImages);
prefer.commit('imageNewTab', store.s.imageNewTab);
prefer.commit('disableShowingAnimatedImages', store.s.disableShowingAnimatedImages);
prefer.commit('emojiStyle', store.s.emojiStyle);
prefer.commit('menuStyle', store.s.menuStyle);
prefer.commit('useBlurEffectForModal', store.s.useBlurEffectForModal);
prefer.commit('useBlurEffect', store.s.useBlurEffect);
prefer.commit('showFixedPostForm', store.s.showFixedPostForm);
prefer.commit('showFixedPostFormInChannel', store.s.showFixedPostFormInChannel);
prefer.commit('enableInfiniteScroll', store.s.enableInfiniteScroll);
prefer.commit('useReactionPickerForContextMenu', store.s.useReactionPickerForContextMenu);
prefer.commit('showGapBetweenNotesInTimeline', store.s.showGapBetweenNotesInTimeline);
prefer.commit('instanceTicker', store.s.instanceTicker);
prefer.commit('emojiPickerScale', store.s.emojiPickerScale);
prefer.commit('emojiPickerWidth', store.s.emojiPickerWidth);
prefer.commit('emojiPickerHeight', store.s.emojiPickerHeight);
prefer.commit('emojiPickerStyle', store.s.emojiPickerStyle);
prefer.commit('reportError', store.s.reportError);
prefer.commit('squareAvatars', store.s.squareAvatars);
prefer.commit('showAvatarDecorations', store.s.showAvatarDecorations);
prefer.commit('numberOfPageCache', store.s.numberOfPageCache);
prefer.commit('showNoteActionsOnlyHover', store.s.showNoteActionsOnlyHover);
prefer.commit('showClipButtonInNoteFooter', store.s.showClipButtonInNoteFooter);
prefer.commit('reactionsDisplaySize', store.s.reactionsDisplaySize);
prefer.commit('limitWidthOfReaction', store.s.limitWidthOfReaction);
prefer.commit('forceShowAds', store.s.forceShowAds);
prefer.commit('aiChanMode', store.s.aiChanMode);
prefer.commit('devMode', store.s.devMode);
prefer.commit('mediaListWithOneImageAppearance', store.s.mediaListWithOneImageAppearance);
prefer.commit('notificationPosition', store.s.notificationPosition);
prefer.commit('notificationStackAxis', store.s.notificationStackAxis);
prefer.commit('enableCondensedLine', store.s.enableCondensedLine);
prefer.commit('keepScreenOn', store.s.keepScreenOn);
prefer.commit('disableStreamingTimeline', store.s.disableStreamingTimeline);
prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications);
prefer.commit('dataSaver', store.s.dataSaver);
prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect);
prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe);
prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer);
prefer.commit('keepOriginalFilename', store.s.keepOriginalFilename);
prefer.commit('alwaysConfirmFollow', store.s.alwaysConfirmFollow);
prefer.commit('confirmWhenRevealingSensitiveMedia', store.s.confirmWhenRevealingSensitiveMedia);
prefer.commit('contextMenu', store.s.contextMenu);
prefer.commit('skipNoteRender', store.s.skipNoteRender);
prefer.commit('showSoftWordMutedWord', store.s.showSoftWordMutedWord);
prefer.commit('confirmOnReact', store.s.confirmOnReact);
prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
prefer.commit('sound.on.note', store.s.sound_note as any);
prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any);
prefer.commit('sound.on.notification', store.s.sound_notification as any);
prefer.commit('sound.on.reaction', store.s.sound_reaction as any);
store.set('deck.profile', deckStore.s.profile);
store.set('deck.columns', deckStore.s.columns);
store.set('deck.layout', deckStore.s.layout);
store.set('menu', []);
}
if (store.s.accountSetupWizard !== -1) { if (store.s.accountSetupWizard !== -1) {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
closed: () => dispose(), closed: () => dispose(),
@@ -438,11 +482,11 @@ export async function mainBoot() {
// 自分の情報が更新されたとき // 自分の情報が更新されたとき
main.on('meUpdated', i => { main.on('meUpdated', i => {
updateAccountPartial(i); updateCurrentAccountPartial(i);
}); });
main.on('readAllNotifications', () => { main.on('readAllNotifications', () => {
updateAccountPartial({ updateCurrentAccountPartial({
hasUnreadNotification: false, hasUnreadNotification: false,
unreadNotificationsCount: 0, unreadNotificationsCount: 0,
}); });
@@ -450,39 +494,39 @@ export async function mainBoot() {
main.on('unreadNotification', () => { main.on('unreadNotification', () => {
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1; const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
updateAccountPartial({ updateCurrentAccountPartial({
hasUnreadNotification: true, hasUnreadNotification: true,
unreadNotificationsCount, unreadNotificationsCount,
}); });
}); });
main.on('unreadMention', () => { main.on('unreadMention', () => {
updateAccountPartial({ hasUnreadMentions: true }); updateCurrentAccountPartial({ hasUnreadMentions: true });
}); });
main.on('readAllUnreadMentions', () => { main.on('readAllUnreadMentions', () => {
updateAccountPartial({ hasUnreadMentions: false }); updateCurrentAccountPartial({ hasUnreadMentions: false });
}); });
main.on('unreadSpecifiedNote', () => { main.on('unreadSpecifiedNote', () => {
updateAccountPartial({ hasUnreadSpecifiedNotes: true }); updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: true });
}); });
main.on('readAllUnreadSpecifiedNotes', () => { main.on('readAllUnreadSpecifiedNotes', () => {
updateAccountPartial({ hasUnreadSpecifiedNotes: false }); updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: false });
}); });
main.on('readAllAntennas', () => { main.on('readAllAntennas', () => {
updateAccountPartial({ hasUnreadAntenna: false }); updateCurrentAccountPartial({ hasUnreadAntenna: false });
}); });
main.on('unreadAntenna', () => { main.on('unreadAntenna', () => {
updateAccountPartial({ hasUnreadAntenna: true }); updateCurrentAccountPartial({ hasUnreadAntenna: true });
sound.playMisskeySfx('antenna'); sound.playMisskeySfx('antenna');
}); });
main.on('readAllAnnouncements', () => { main.on('readAllAnnouncements', () => {
updateAccountPartial({ hasUnreadAnnouncement: false }); updateCurrentAccountPartial({ hasUnreadAnnouncement: false });
}); });
// 個人宛てお知らせが発行されたとき // 個人宛てお知らせが発行されたとき

View File

@@ -29,7 +29,8 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i, updateAccountPartial } from '@/account.js'; import { $i } from '@/i.js';
import { updateCurrentAccountPartial } from '@/accounts.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
announcement: Misskey.entities.Announcement; announcement: Misskey.entities.Announcement;
@@ -51,7 +52,7 @@ async function ok() {
modal.value?.close(); modal.value?.close();
misskeyApi('i/read-announcement', { announcementId: props.announcement.id }); misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
updateAccountPartial({ updateCurrentAccountPartial({
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id), unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
}); });
} }

View File

@@ -117,10 +117,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { $i } from '@/i.js';
import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js'; import { getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { getProxiedImageUrl } from '@/utility/media-proxy.js'; import { getProxiedImageUrl } from '@/utility/media-proxy.js';
@@ -158,7 +157,7 @@ async function init() {
const accounts = await getAccounts(); const accounts = await getAccounts();
const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id)); const accountIdsToFetch = accounts.map(a => a.user.id).filter(id => !users.value.has(id));
if (accountIdsToFetch.length > 0) { if (accountIdsToFetch.length > 0) {
const usersRes = await misskeyApi('users/show', { const usersRes = await misskeyApi('users/show', {
@@ -170,7 +169,7 @@ async function init() {
users.value.set(user.id, { users.value.set(user.id, {
...user, ...user,
token: accounts.find(a => a.id === user.id)!.token, token: accounts.find(a => a.user.id === user.id)!.token,
}); });
} }
} }

View File

@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button <button
v-if="!link" v-if="!link"
ref="el" class="_button" ref="el" class="_button"
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly }]"
:type="type" :type="type"
:name="name" :name="name"
:value="value" :value="value"
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</button> </button>
<MkA <MkA
v-else class="_button" v-else class="_button"
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly }]"
:to="to ?? '#'" :to="to ?? '#'"
:behavior="linkBehavior" :behavior="linkBehavior"
@mousedown="onMousedown" @mousedown="onMousedown"
@@ -57,6 +57,7 @@ const props = defineProps<{
name?: string; name?: string;
value?: string; value?: string;
disabled?: boolean; disabled?: boolean;
iconOnly?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -147,6 +148,11 @@ function onMousedown(evt: MouseEvent): void {
background: var(--MI_THEME-buttonHoverBg); background: var(--MI_THEME-buttonHoverBg);
} }
&.iconOnly {
padding: 7px;
min-width: auto;
}
&.small { &.small {
font-size: 90%; font-size: 90%;
padding: 6px 12px; padding: 6px 12px;
@@ -220,28 +226,28 @@ function onMousedown(evt: MouseEvent): void {
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
&:not(:disabled):hover { &:not(:disabled):hover {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
} }
&:not(:disabled):active { &:not(:disabled):active {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
} }
} }
&.danger { &.danger {
font-weight: bold; font-weight: bold;
color: #ff2a2a; color: var(--MI_THEME-error);
&.primary { &.primary {
color: #fff; color: #fff;
background: #ff2a2a; background: var(--MI_THEME-error);
&:not(:disabled):hover { &:not(:disabled):hover {
background: #ff4242; background: hsl(from var(--MI_THEME-error) h s calc(l + 10));
} }
&:not(:disabled):active { &:not(:disabled):active {
background: #d42e2e; background: hsl(from var(--MI_THEME-error) h s calc(l - 10));
} }
} }
} }

View File

@@ -55,7 +55,7 @@ import { Chart } from 'chart.js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { misskeyApiGet } from '@/utility/misskey-api.js'; import { misskeyApiGet } from '@/utility/misskey-api.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js'; import { chartVLine } from '@/utility/chart-vline.js';
import { alpha } from '@/utility/color.js'; import { alpha } from '@/utility/color.js';
import date from '@/filters/date.js'; import date from '@/filters/date.js';

View File

@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { computed } from 'vue'; import { computed } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{

View File

@@ -38,7 +38,7 @@ import tinycolor from 'tinycolor2';
import { apiUrl } from '@@/js/config.js'; import { apiUrl } from '@@/js/config.js';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getProxiedImageUrl } from '@/utility/media-proxy.js'; import { getProxiedImageUrl } from '@/utility/media-proxy.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';

View File

@@ -44,7 +44,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import bytes from '@/filters/bytes.js'; import bytes from '@/filters/bytes.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js'; import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { deviceKind } from '@/utility/device-kind.js'; import { deviceKind } from '@/utility/device-kind.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';

View File

@@ -136,7 +136,7 @@ import { deviceKind } from '@/utility/device-kind.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js'; import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';

View File

@@ -0,0 +1,43 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-panel :class="$style.root">
<img :class="$style.img" :src="icon"/>
<div :class="$style.text">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
icon: string;
color: string;
}>(), {
});
</script>
<style module lang="scss">
.root {
padding: 20px 24px;
text-align: center;
border-radius: var(--MI-radius);
background: linear-gradient(180deg, color(from v-bind(color) srgb r g b / 0.1), color(from v-bind(color) srgb r g b / 0));
}
.img {
display: block;
margin: 0 auto;
width: 40px;
aspect-ratio: 1;
}
.text {
margin-top: 12px;
font-size: 85%;
mix-blend-mode: luminosity;
}
</style>

View File

@@ -44,8 +44,7 @@ import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { claimAchievement } from '@/utility/achievements.js'; import { claimAchievement } from '@/utility/achievements.js';
import { pleaseLogin } from '@/utility/please-login.js'; import { pleaseLogin } from '@/utility/please-login.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { store } from '@/store.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@@ -121,11 +120,11 @@ async function onClick() {
} else { } else {
await misskeyApi('following/create', { await misskeyApi('following/create', {
userId: props.user.id, userId: props.user.id,
withReplies: store.s.defaultWithReplies, withReplies: prefer.s.defaultFollowWithReplies,
}); });
emit('update:user', { emit('update:user', {
...props.user, ...props.user,
withReplies: store.s.defaultWithReplies, withReplies: prefer.s.defaultFollowWithReplies,
}); });
hasPendingFollowRequestFromYou.value = true; hasPendingFollowRequestFromYou.value = true;

View File

@@ -18,7 +18,7 @@ import { Chart } from 'chart.js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { alpha } from '@/utility/color.js'; import { alpha } from '@/utility/color.js';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';

View File

@@ -88,8 +88,8 @@ import { onMounted, ref, computed, shallowRef } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue'; import MkChart from '@/components/MkChart.vue';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApiGet } from '@/utility/misskey-api.js'; import { misskeyApiGet } from '@/utility/misskey-api.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';

View File

@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'; import { defineAsyncComponent, ref } from 'vue';
import { url as local } from '@@/js/config.js'; import { url as local } from '@@/js/config.js';
import { useTooltip } from '@/utility/use-tooltip.js'; import { useTooltip } from '@/use/use-tooltip.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { isEnabledUrlPreview } from '@/instance.js'; import { isEnabledUrlPreview } from '@/instance.js';
import type { MkABehavior } from '@/components/global/MkA.vue'; import type { MkABehavior } from '@/components/global/MkA.vue';

View File

@@ -98,7 +98,7 @@ import * as os from '@/os.js';
import bytes from '@/filters/bytes.js'; import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js'; import { hms } from '@/filters/hms.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
import { $i, iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/i.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
const props = defineProps<{ const props = defineProps<{

View File

@@ -60,7 +60,7 @@ import bytes from '@/filters/bytes.js';
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i, iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/i.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{

View File

@@ -121,7 +121,7 @@ import * as os from '@/os.js';
import { exitFullscreen, requestFullscreen } from '@/utility/fullscreen.js'; import { exitFullscreen, requestFullscreen } from '@/utility/fullscreen.js';
import hasAudio from '@/utility/media-has-audio.js'; import hasAudio from '@/utility/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
import { $i, iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/i.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
const props = defineProps<{ const props = defineProps<{

View File

@@ -18,7 +18,7 @@ import { toUnicode } from 'punycode.js';
import { computed } from 'vue'; import { computed } from 'vue';
import { host as localHost } from '@@/js/config.js'; import { host as localHost } from '@@/js/config.js';
import type { MkABehavior } from '@/components/global/MkA.vue'; import type { MkABehavior } from '@/components/global/MkA.vue';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { getStaticImageUrl } from '@/utility/media-proxy.js'; import { getStaticImageUrl } from '@/utility/media-proxy.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';

View File

@@ -177,12 +177,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts"> <script lang="ts">
import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue'; import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
import MkSwitchButton from '@/components/MkSwitch.button.vue';
import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js'; import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
import type { Keymap } from '@/utility/hotkey.js';
import MkSwitchButton from '@/components/MkSwitch.button.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { isTouchUsing } from '@/utility/touch.js'; import { isTouchUsing } from '@/utility/touch.js';
import type { Keymap } from '@/utility/hotkey.js';
import { isFocusable } from '@/utility/focus.js'; import { isFocusable } from '@/utility/focus.js';
import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js'; import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js';
@@ -558,11 +558,11 @@ onBeforeUnmount(() => {
} }
&.danger { &.danger {
--menuFg: #ff2a2a; --menuFg: var(--MI_THEME-error);
--menuHoverFg: #fff; --menuHoverFg: #fff;
--menuHoverBg: #ff4242; --menuHoverBg: var(--MI_THEME-error);
--menuActiveFg: #fff; --menuActiveFg: #fff;
--menuActiveBg: #d42e2e; --menuActiveBg: hsl(from var(--MI_THEME-error) h s calc(l - 10));
} }
&.radio { &.radio {

View File

@@ -208,12 +208,12 @@ import * as sound from '@/utility/sound.js';
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import { reactionPicker } from '@/utility/reaction-picker.js'; import { reactionPicker } from '@/utility/reaction-picker.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js'; import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
import { useNoteCapture } from '@/utility/use-note-capture.js'; import { useNoteCapture } from '@/use/use-note-capture.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import { useTooltip } from '@/utility/use-tooltip.js'; import { useTooltip } from '@/use/use-tooltip.js';
import { claimAchievement } from '@/utility/achievements.js'; import { claimAchievement } from '@/utility/achievements.js';
import { getNoteSummary } from '@/utility/get-note-summary.js'; import { getNoteSummary } from '@/utility/get-note-summary.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';

View File

@@ -238,12 +238,12 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
import { reactionPicker } from '@/utility/reaction-picker.js'; import { reactionPicker } from '@/utility/reaction-picker.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js'; import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
import { useNoteCapture } from '@/utility/use-note-capture.js'; import { useNoteCapture } from '@/use/use-note-capture.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import { useTooltip } from '@/utility/use-tooltip.js'; import { useTooltip } from '@/use/use-tooltip.js';
import { claimAchievement } from '@/utility/achievements.js'; import { claimAchievement } from '@/utility/achievements.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/utility/show-moved-dialog.js'; import { showMovedDialog } from '@/utility/show-moved-dialog.js';

View File

@@ -48,7 +48,7 @@ import MkCwButton from '@/components/MkCwButton.vue';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { checkWordMute } from '@/utility/check-word-mute.js'; import { checkWordMute } from '@/utility/check-word-mute.js';

View File

@@ -169,7 +169,7 @@ import { notePage } from '@/filters/note.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { signinRequired } from '@/account.js'; import { signinRequired } from '@/i.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
const $i = signinRequired(); const $i = signinRequired();

View File

@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :marginMin="20" :marginMax="28"> <MkSpacer :marginMin="20" :marginMax="28">
<div style="padding: 0 0 16px 0; text-align: center;"> <div style="padding: 0 0 16px 0; text-align: center;">
<img src="/fluent-emoji/1f510.png" alt="🔐" style="display: block; margin: 0 auto; width: 48px;"> <img src="/client-assets/locked_with_key_3d.png" alt="🔐" style="display: block; margin: 0 auto; width: 48px;">
<div style="margin-top: 16px;">{{ i18n.ts.authenticationRequiredToContinue }}</div> <div style="margin-top: 16px;">{{ i18n.ts.authenticationRequiredToContinue }}</div>
</div> </div>
@@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { signinRequired } from '@/account.js'; import { signinRequired } from '@/i.js';
const $i = signinRequired(); const $i = signinRequired();

View File

@@ -127,7 +127,8 @@ import { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { signinRequired, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js'; import { signinRequired, notesCount, incNotesCount } from '@/i.js';
import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { uploadFile } from '@/utility/upload.js'; import { uploadFile } from '@/utility/upload.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
@@ -265,7 +266,13 @@ const canPost = computed((): boolean => {
quoteId.value != null quoteId.value != null
) && ) &&
(textLength.value <= maxTextLength.value) && (textLength.value <= maxTextLength.value) &&
(cwTextLength.value <= maxCwTextLength) && (
useCw.value ?
(
cw.value != null && cw.value.trim() !== '' &&
cwTextLength.value <= maxCwTextLength
) : true
) &&
(files.value.length <= 16) && (files.value.length <= 16) &&
(!poll.value || poll.value.choices.length >= 2); (!poll.value || poll.value.choices.length >= 2);
}); });
@@ -744,14 +751,6 @@ function isAnnoying(text: string): boolean {
} }
async function post(ev?: MouseEvent) { async function post(ev?: MouseEvent) {
if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
os.alert({
type: 'error',
text: i18n.ts.cwNotationRequired,
});
return;
}
if (ev) { if (ev) {
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null; const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
@@ -839,7 +838,7 @@ async function post(ev?: MouseEvent) {
if (postAccount.value) { if (postAccount.value) {
const storedAccounts = await getAccounts(); const storedAccounts = await getAccounts();
token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token; token = storedAccounts.find(x => x.user.id === postAccount.value?.id)?.token;
} }
posting.value = true; posting.value = true;

View File

@@ -4,14 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="$style.root"> <div :class="$style.root" @contextmenu.prevent.stop="showMenu($event, true)">
<div :class="$style.body"> <div :class="$style.body">
<slot></slot> <slot></slot>
</div> </div>
<div :class="$style.menu"> <div :class="$style.menu">
<i v-if="isSyncEnabled" class="ti ti-cloud-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i>
<i v-if="isAccountOverrided" class="ti ti-user-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i> <i v-if="isAccountOverrided" class="ti ti-user-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i>
<div :class="$style.buttons"> <div :class="$style.buttons">
<button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu"><i class="ti ti-dots"></i></button> <button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu($event)"><i class="ti ti-dots"></i></button>
</div> </div>
</div> </div>
</div> </div>
@@ -21,25 +22,33 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue'; import { ref } from 'vue';
import type { PREF_DEF } from '@/preferences/def.js'; import type { PREF_DEF } from '@/preferences/def.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { profileManager } from '@/preferences.js'; import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
k: keyof typeof PREF_DEF; k: keyof typeof PREF_DEF;
}>(), { }>(), {
}); });
const isAccountOverrided = ref(profileManager.isAccountOverrided(props.k)); const isAccountOverrided = ref(prefer.isAccountOverrided(props.k));
const isSyncEnabled = ref(prefer.isSyncEnabled(props.k));
function showMenu(ev: MouseEvent) { function showMenu(ev: MouseEvent, contextmenu?: boolean) {
const i = window.setInterval(() => { const i = window.setInterval(() => {
isAccountOverrided.value = profileManager.isAccountOverrided(props.k); isAccountOverrided.value = prefer.isAccountOverrided(props.k);
isSyncEnabled.value = prefer.isSyncEnabled(props.k);
}, 100); }, 100);
os.popupMenu(profileManager.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, { if (contextmenu) {
os.contextMenu(prefer.getPerPrefMenu(props.k), ev).then(() => {
window.clearInterval(i);
});
} else {
os.popupMenu(prefer.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
onClosing: () => { onClosing: () => {
window.clearInterval(i); window.clearInterval(i);
}, },
}); });
} }
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>
@@ -48,7 +57,7 @@ function showMenu(ev: MouseEvent) {
display: flex; display: flex;
&:hover { &:hover {
&::after { &::before {
content: ''; content: '';
position: absolute; position: absolute;
top: -8px; top: -8px;

View File

@@ -43,7 +43,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkRadio from '@/components/MkRadio.vue'; import MkRadio from '@/components/MkRadio.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import * as config from '@@/js/config.js'; import * as config from '@@/js/config.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
const text = ref(''); const text = ref('');
const flag = ref(true); const flag = ref(true);

View File

@@ -42,12 +42,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { $i, getAccounts } from '@/account.js'; import { $i } from '@/i.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { apiWithDialog, promiseDialog } from '@/os.js'; import { apiWithDialog, promiseDialog } from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getAccounts } from '@/accounts.js';
defineProps<{ defineProps<{
primary?: boolean; primary?: boolean;

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, shallowRef } from 'vue'; import { defineAsyncComponent, shallowRef } from 'vue';
import { useTooltip } from '@/utility/use-tooltip.js'; import { useTooltip } from '@/use/use-tooltip.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
const props = defineProps<{ const props = defineProps<{

View File

@@ -26,8 +26,8 @@ import XDetails from '@/components/MkReactionsViewer.details.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import { useTooltip } from '@/utility/use-tooltip.js'; import { useTooltip } from '@/use/use-tooltip.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import MkReactionEffect from '@/components/MkReactionEffect.vue'; import MkReactionEffect from '@/components/MkReactionEffect.vue';
import { claimAchievement } from '@/utility/achievements.js'; import { claimAchievement } from '@/utility/achievements.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View File

@@ -17,7 +17,7 @@ import { onMounted, nextTick, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { alpha } from '@/utility/color.js'; import { alpha } from '@/utility/color.js';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';

View File

@@ -12,7 +12,7 @@ import { onMounted, shallowRef } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js'; import { chartVLine } from '@/utility/chart-vline.js';
import { alpha } from '@/utility/color.js'; import { alpha } from '@/utility/color.js';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';

View File

@@ -67,20 +67,19 @@ SPDX-License-Identifier: AGPL-3.0-only
import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue'; import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import type { PwResponse } from '@/components/MkSignin.password.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js'; import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import XInput from '@/components/MkSignin.input.vue'; import XInput from '@/components/MkSignin.input.vue';
import XPassword from '@/components/MkSignin.password.vue'; import XPassword from '@/components/MkSignin.password.vue';
import type { PwResponse } from '@/components/MkSignin.password.vue';
import XTotp from '@/components/MkSignin.totp.vue'; import XTotp from '@/components/MkSignin.totp.vue';
import XPasskey from '@/components/MkSignin.passkey.vue'; import XPasskey from '@/components/MkSignin.passkey.vue';
import { login } from '@/accounts.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void; (ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;

View File

@@ -85,13 +85,13 @@ import * as Misskey from 'misskey-js';
import * as config from '@@/js/config.js'; import * as config from '@@/js/config.js';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue'; import MkInput from './MkInput.vue';
import MkCaptcha from '@/components/MkCaptcha.vue';
import type { Captcha } from '@/components/MkCaptcha.vue'; import type { Captcha } from '@/components/MkCaptcha.vue';
import MkCaptcha from '@/components/MkCaptcha.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { login } from '@/account.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { login } from '@/accounts.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
autoSet?: boolean; autoSet?: boolean;

View File

@@ -25,7 +25,7 @@ import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';

View File

@@ -55,7 +55,7 @@ import MkButton from './MkButton.vue';
import MkInfo from './MkInfo.vue'; import MkInfo from './MkInfo.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { iAmAdmin } from '@/account.js'; import { iAmAdmin } from '@/i.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
title?: string | null; title?: string | null;

View File

@@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js';
import { ref, reactive } from 'vue'; import { ref, reactive } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { globalEvents } from '@/events.js'; import { globalEvents } from '@/events.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import MkNote from '@/components/MkNote.vue'; import MkNote from '@/components/MkNote.vue';
const props = defineProps<{ const props = defineProps<{

View File

@@ -31,7 +31,7 @@ import MkPostForm from '@/components/MkPostForm.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkNote from '@/components/MkNote.vue'; import MkNote from '@/components/MkNote.vue';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'succeeded'): void; (ev: 'succeeded'): void;

View File

@@ -39,7 +39,7 @@ import MkFollowButton from '@/components/MkFollowButton.vue';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
import { getStaticImageUrl } from '@/utility/media-proxy.js'; import { getStaticImageUrl } from '@/utility/media-proxy.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';

View File

@@ -65,7 +65,7 @@ import { getUserMenu } from '@/utility/get-user-menu.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
import { getStaticImageUrl } from '@/utility/media-proxy.js'; import { getStaticImageUrl } from '@/utility/media-proxy.js';

View File

@@ -70,7 +70,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -39,7 +39,7 @@ import FormSlot from '@/components/form/slot.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { chooseFileFromPc } from '@/utility/select-file.js'; import { chooseFileFromPc } from '@/utility/select-file.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { signinRequired } from '@/account.js'; import { signinRequired } from '@/i.js';
const $i = signinRequired(); const $i = signinRequired();

View File

@@ -19,7 +19,7 @@ import gradient from 'chartjs-plugin-gradient';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js'; import { chartVLine } from '@/utility/chart-vline.js';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';

View File

@@ -47,7 +47,7 @@ import { instance } from '@/instance.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { store } from '@/store.js'; import { store } from '@/store.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
type Ad = (typeof instance)['ads'][number]; type Ad = (typeof instance)['ads'][number];

View File

@@ -35,7 +35,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
const props = defineProps<{ const props = defineProps<{

View File

@@ -50,7 +50,8 @@ import type { PageHeaderItem } from '@/types/page-header.js';
import type { PageMetadata } from '@/page.js'; import type { PageMetadata } from '@/page.js';
import { globalEvents } from '@/events.js'; import { globalEvents } from '@/events.js';
import { injectReactiveMetadata } from '@/page.js'; import { injectReactiveMetadata } from '@/page.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { $i } from '@/i.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
overridePageMetadata?: PageMetadata; overridePageMetadata?: PageMetadata;

View File

@@ -29,7 +29,7 @@ import { defineAsyncComponent, ref } from 'vue';
import { toUnicode as decodePunycode } from 'punycode.js'; import { toUnicode as decodePunycode } from 'punycode.js';
import { url as local } from '@@/js/config.js'; import { url as local } from '@@/js/config.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { useTooltip } from '@/utility/use-tooltip.js'; import { useTooltip } from '@/use/use-tooltip.js';
import { isEnabledUrlPreview } from '@/instance.js'; import { isEnabledUrlPreview } from '@/instance.js';
import type { MkABehavior } from '@/components/global/MkA.vue'; import type { MkABehavior } from '@/components/global/MkA.vue';

View File

@@ -90,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue'; import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue';
import { GridEventEmitter } from '@/components/grid/grid.js'; import { GridEventEmitter } from '@/components/grid/grid.js';
import { useTooltip } from '@/utility/use-tooltip.js'; import { useTooltip } from '@/use/use-tooltip.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js'; import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
import type { Size } from '@/components/grid/grid.js'; import type { Size } from '@/components/grid/grid.js';

View File

@@ -3,13 +3,23 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { throttle } from 'throttle-debounce';
import { notificationTypes } from 'misskey-js'; import { notificationTypes } from 'misskey-js';
import { ref } from 'vue';
import { v4 as uuid } from 'uuid';
import { i18n } from './i18n.js';
import type { BasicTimelineType } from '@/timelines.js'; import type { BasicTimelineType } from '@/timelines.js';
import type { SoundStore } from '@/preferences/def.js'; import type { SoundStore } from '@/preferences/def.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import type { MenuItem } from '@/types/menu.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import { store } from '@/store.js'; import { prefer } from '@/preferences.js';
import * as os from '@/os.js';
export type DeckProfile = {
name: string;
id: string;
columns: Column[];
layout: Column['id'][][];
};
type ColumnWidget = { type ColumnWidget = {
name: string; name: string;
@@ -53,127 +63,132 @@ export type Column = {
soundSetting?: SoundStore; soundSetting?: SoundStore;
}; };
export const loadDeck = async () => { const _currentProfile = prefer.s['deck.profiles'].find(p => p.name === prefer.s['deck.profile']);
let deck; const __currentProfile = _currentProfile ? deepClone(_currentProfile) : null;
export const columns = ref(__currentProfile ? __currentProfile.columns : []);
export const layout = ref(__currentProfile ? __currentProfile.layout : []);
try { if (prefer.s['deck.profile'] == null) {
deck = await misskeyApi('i/registry/get', { addProfile('Main');
scope: ['client', 'deck', 'profiles'],
key: store.s['deck.profile'],
});
} catch (err) {
if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
// 後方互換性のため
if (store.s['deck.profile'] === 'default') {
saveDeck();
return;
} }
store.set('deck.columns', []); export function forceSaveCurrentDeckProfile() {
store.set('deck.layout', []); const currentProfile = prefer.s['deck.profiles'].find(p => p.name === prefer.s['deck.profile']);
return; if (currentProfile == null) return;
}
throw err; const newProfile = deepClone(currentProfile);
newProfile.columns = columns.value;
newProfile.layout = layout.value;
const newProfiles = prefer.s['deck.profiles'].filter(p => p.name !== prefer.s['deck.profile']);
newProfiles.push(newProfile);
prefer.commit('deck.profiles', newProfiles);
} }
store.set('deck.columns', deck.columns); export const saveCurrentDeckProfile = () => {
store.set('deck.layout', deck.layout); forceSaveCurrentDeckProfile();
}; };
export async function forceSaveDeck() { function switchProfile(profile: DeckProfile) {
await misskeyApi('i/registry/set', { prefer.commit('deck.profile', profile.name);
scope: ['client', 'deck', 'profiles'], const currentProfile = deepClone(profile);
key: store.s['deck.profile'], columns.value = currentProfile.columns;
value: { layout.value = currentProfile.layout;
columns: store.r['deck.columns'].value, forceSaveCurrentDeckProfile();
layout: store.r['deck.layout'].value,
},
});
} }
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する function addProfile(name: string) {
export const saveDeck = throttle(1000, () => { if (name.trim() === '') return;
forceSaveDeck(); if (prefer.s['deck.profiles'].find(p => p.name === name)) return;
});
export async function getProfiles(): Promise<string[]> { const newProfile: DeckProfile = {
return await misskeyApi('i/registry/keys', { id: uuid(),
scope: ['client', 'deck', 'profiles'], name,
}); columns: [],
layout: [],
};
prefer.commit('deck.profiles', [...prefer.s['deck.profiles'], newProfile]);
switchProfile(newProfile);
} }
export async function deleteProfile(key: string): Promise<void> { function createFirstProfile() {
return await misskeyApi('i/registry/remove', { addProfile('Main');
scope: ['client', 'deck', 'profiles'], }
key: key,
}); export function deleteProfile(name: string): void {
const newProfiles = prefer.s['deck.profiles'].filter(p => p.name !== name);
prefer.commit('deck.profiles', newProfiles);
if (prefer.s['deck.profiles'].length === 0) {
createFirstProfile();
} else {
switchProfile(prefer.s['deck.profiles'][0]);
}
} }
export function addColumn(column: Column) { export function addColumn(column: Column) {
if (column.name === undefined) column.name = null; if (column.name === undefined) column.name = null;
store.push('deck.columns', column); columns.value.push(column);
store.push('deck.layout', [column.id]); layout.value.push([column.id]);
saveDeck(); saveCurrentDeckProfile();
} }
export function removeColumn(id: Column['id']) { export function removeColumn(id: Column['id']) {
store.set('deck.columns', store.s['deck.columns'].filter(c => c.id !== id)); columns.value = columns.value.filter(c => c.id !== id);
store.set('deck.layout', store.s['deck.layout'] layout.value = layout.value.map(ids => ids.filter(_id => _id !== id)).filter(ids => ids.length > 0);
.map(ids => ids.filter(_id => _id !== id)) saveCurrentDeckProfile();
.filter(ids => ids.length > 0));
saveDeck();
} }
export function swapColumn(a: Column['id'], b: Column['id']) { export function swapColumn(a: Column['id'], b: Column['id']) {
const aX = store.s['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1); const aX = layout.value.findIndex(ids => ids.indexOf(a) !== -1);
const aY = store.s['deck.layout'][aX].findIndex(id => id === a); const aY = layout.value[aX].findIndex(id => id === a);
const bX = store.s['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1); const bX = layout.value.findIndex(ids => ids.indexOf(b) !== -1);
const bY = store.s['deck.layout'][bX].findIndex(id => id === b); const bY = layout.value[bX].findIndex(id => id === b);
const layout = deepClone(store.s['deck.layout']); const newLayout = deepClone(layout.value);
layout[aX][aY] = b; newLayout[aX][aY] = b;
layout[bX][bY] = a; newLayout[bX][bY] = a;
store.set('deck.layout', layout); layout.value = newLayout;
saveDeck(); saveCurrentDeckProfile();
} }
export function swapLeftColumn(id: Column['id']) { export function swapLeftColumn(id: Column['id']) {
const layout = deepClone(store.s['deck.layout']); const newLayout = deepClone(layout.value);
store.s['deck.layout'].some((ids, i) => { layout.value.some((ids, i) => {
if (ids.includes(id)) { if (ids.includes(id)) {
const left = store.s['deck.layout'][i - 1]; const left = layout.value[i - 1];
if (left) { if (left) {
layout[i - 1] = store.s['deck.layout'][i]; newLayout[i - 1] = layout.value[i];
layout[i] = left; newLayout[i] = left;
store.set('deck.layout', layout); layout.value = newLayout;
} }
return true; return true;
} }
return false; return false;
}); });
saveDeck(); saveCurrentDeckProfile();
} }
export function swapRightColumn(id: Column['id']) { export function swapRightColumn(id: Column['id']) {
const layout = deepClone(store.s['deck.layout']); const newLayout = deepClone(layout.value);
store.s['deck.layout'].some((ids, i) => { layout.value.some((ids, i) => {
if (ids.includes(id)) { if (ids.includes(id)) {
const right = store.s['deck.layout'][i + 1]; const right = layout.value[i + 1];
if (right) { if (right) {
layout[i + 1] = store.s['deck.layout'][i]; newLayout[i + 1] = layout.value[i];
layout[i] = right; newLayout[i] = right;
store.set('deck.layout', layout); layout.value = newLayout;
} }
return true; return true;
} }
return false; return false;
}); });
saveDeck(); saveCurrentDeckProfile();
} }
export function swapUpColumn(id: Column['id']) { export function swapUpColumn(id: Column['id']) {
const layout = deepClone(store.s['deck.layout']); const newLayout = deepClone(layout.value);
const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id)); const idsIndex = layout.value.findIndex(ids => ids.includes(id));
const ids = deepClone(store.s['deck.layout'][idsIndex]); const ids = deepClone(layout.value[idsIndex]);
ids.some((x, i) => { ids.some((x, i) => {
if (x === id) { if (x === id) {
const up = ids[i - 1]; const up = ids[i - 1];
@@ -181,20 +196,20 @@ export function swapUpColumn(id: Column['id']) {
ids[i - 1] = id; ids[i - 1] = id;
ids[i] = up; ids[i] = up;
layout[idsIndex] = ids; newLayout[idsIndex] = ids;
store.set('deck.layout', layout); layout.value = newLayout;
} }
return true; return true;
} }
return false; return false;
}); });
saveDeck(); saveCurrentDeckProfile();
} }
export function swapDownColumn(id: Column['id']) { export function swapDownColumn(id: Column['id']) {
const layout = deepClone(store.s['deck.layout']); const newLayout = deepClone(layout.value);
const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id)); const idsIndex = layout.value.findIndex(ids => ids.includes(id));
const ids = deepClone(store.s['deck.layout'][idsIndex]); const ids = deepClone(layout.value[idsIndex]);
ids.some((x, i) => { ids.some((x, i) => {
if (x === id) { if (x === id) {
const down = ids[i + 1]; const down = ids[i + 1];
@@ -202,105 +217,137 @@ export function swapDownColumn(id: Column['id']) {
ids[i + 1] = id; ids[i + 1] = id;
ids[i] = down; ids[i] = down;
layout[idsIndex] = ids; newLayout[idsIndex] = ids;
store.set('deck.layout', layout); layout.value = newLayout;
} }
return true; return true;
} }
return false; return false;
}); });
saveDeck(); saveCurrentDeckProfile();
} }
export function stackLeftColumn(id: Column['id']) { export function stackLeftColumn(id: Column['id']) {
let layout = deepClone(store.s['deck.layout']); let newLayout = deepClone(layout.value);
const i = store.s['deck.layout'].findIndex(ids => ids.includes(id)); const i = layout.value.findIndex(ids => ids.includes(id));
layout = layout.map(ids => ids.filter(_id => _id !== id)); newLayout = newLayout.map(ids => ids.filter(_id => _id !== id));
layout[i - 1].push(id); newLayout[i - 1].push(id);
layout = layout.filter(ids => ids.length > 0); newLayout = newLayout.filter(ids => ids.length > 0);
store.set('deck.layout', layout); layout.value = newLayout;
saveDeck(); saveCurrentDeckProfile();
} }
export function popRightColumn(id: Column['id']) { export function popRightColumn(id: Column['id']) {
let layout = deepClone(store.s['deck.layout']); let newLayout = deepClone(layout.value);
const i = store.s['deck.layout'].findIndex(ids => ids.includes(id)); const i = layout.value.findIndex(ids => ids.includes(id));
const affected = layout[i]; const affected = newLayout[i];
layout = layout.map(ids => ids.filter(_id => _id !== id)); newLayout = newLayout.map(ids => ids.filter(_id => _id !== id));
layout.splice(i + 1, 0, [id]); newLayout.splice(i + 1, 0, [id]);
layout = layout.filter(ids => ids.length > 0); newLayout = newLayout.filter(ids => ids.length > 0);
store.set('deck.layout', layout); layout.value = newLayout;
const columns = deepClone(store.s['deck.columns']); const newColumns = deepClone(columns.value);
for (const column of columns) { for (const column of newColumns) {
if (affected.includes(column.id)) { if (affected.includes(column.id)) {
column.active = true; column.active = true;
} }
} }
store.set('deck.columns', columns); columns.value = newColumns;
saveDeck(); saveCurrentDeckProfile();
} }
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) { export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = deepClone(store.s['deck.columns']); const newColumns = deepClone(columns.value);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); const columnIndex = columns.value.findIndex(c => c.id === id);
const column = deepClone(store.s['deck.columns'][columnIndex]); const column = deepClone(columns.value[columnIndex]);
if (column == null) return; if (column == null) return;
if (column.widgets == null) column.widgets = []; if (column.widgets == null) column.widgets = [];
column.widgets.unshift(widget); column.widgets.unshift(widget);
columns[columnIndex] = column; newColumns[columnIndex] = column;
store.set('deck.columns', columns); columns.value = newColumns;
saveDeck(); saveCurrentDeckProfile();
} }
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = deepClone(store.s['deck.columns']); const newColumns = deepClone(columns.value);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); const columnIndex = columns.value.findIndex(c => c.id === id);
const column = deepClone(store.s['deck.columns'][columnIndex]); const column = deepClone(columns.value[columnIndex]);
if (column == null) return; if (column == null) return;
if (column.widgets == null) column.widgets = []; if (column.widgets == null) column.widgets = [];
column.widgets = column.widgets.filter(w => w.id !== widget.id); column.widgets = column.widgets.filter(w => w.id !== widget.id);
columns[columnIndex] = column; newColumns[columnIndex] = column;
store.set('deck.columns', columns); columns.value = newColumns;
saveDeck(); saveCurrentDeckProfile();
} }
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
const columns = deepClone(store.s['deck.columns']); const newColumns = deepClone(columns.value);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); const columnIndex = columns.value.findIndex(c => c.id === id);
const column = deepClone(store.s['deck.columns'][columnIndex]); const column = deepClone(columns.value[columnIndex]);
if (column == null) return; if (column == null) return;
column.widgets = widgets; column.widgets = widgets;
columns[columnIndex] = column; newColumns[columnIndex] = column;
store.set('deck.columns', columns); columns.value = newColumns;
saveDeck(); saveCurrentDeckProfile();
} }
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) { export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
const columns = deepClone(store.s['deck.columns']); const newColumns = deepClone(columns.value);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); const columnIndex = columns.value.findIndex(c => c.id === id);
const column = deepClone(store.s['deck.columns'][columnIndex]); const column = deepClone(columns.value[columnIndex]);
if (column == null) return; if (column == null) return;
if (column.widgets == null) column.widgets = []; if (column.widgets == null) column.widgets = [];
column.widgets = column.widgets.map(w => w.id === widgetId ? { column.widgets = column.widgets.map(w => w.id === widgetId ? {
...w, ...w,
data: widgetData, data: widgetData,
} : w); } : w);
columns[columnIndex] = column; newColumns[columnIndex] = column;
store.set('deck.columns', columns); columns.value = newColumns;
saveDeck(); saveCurrentDeckProfile();
} }
export function updateColumn(id: Column['id'], column: Partial<Column>) { export function updateColumn(id: Column['id'], column: Partial<Column>) {
const columns = deepClone(store.s['deck.columns']); const newColumns = deepClone(columns.value);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); const columnIndex = columns.value.findIndex(c => c.id === id);
const currentColumn = deepClone(store.s['deck.columns'][columnIndex]); const currentColumn = deepClone(columns.value[columnIndex]);
if (currentColumn == null) return; if (currentColumn == null) return;
for (const [k, v] of Object.entries(column)) { for (const [k, v] of Object.entries(column)) {
currentColumn[k] = v; currentColumn[k] = v;
} }
columns[columnIndex] = currentColumn; newColumns[columnIndex] = currentColumn;
store.set('deck.columns', columns); columns.value = newColumns;
saveDeck(); saveCurrentDeckProfile();
}
export function switchProfileMenu(ev: MouseEvent) {
const items: MenuItem[] = prefer.s['deck.profile'] ? [{
text: prefer.s['deck.profile'],
active: true,
action: () => {},
}] : [];
const profiles = prefer.s['deck.profiles'];
items.push(...(profiles.filter(p => p.name !== prefer.s['deck.profile']).map(p => ({
text: p.name,
action: () => {
switchProfile(p);
},
}))), { type: 'divider' as const }, {
text: i18n.ts._deck.newProfile,
icon: 'ti ti-plus',
action: async () => {
const { canceled, result: name } = await os.inputText({
title: i18n.ts._deck.profile,
minLength: 1,
});
if (canceled || name == null || name.trim() === '') return;
addProfile(name);
},
});
os.popupMenu(items, ev.currentTarget ?? ev.target);
} }

View File

@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { reactive } from 'vue';
import * as Misskey from 'misskey-js';
import { miLocalStorage } from '@/local-storage.js';
// TODO: 他のタブと永続化されたstateを同期
type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
const accountData = miLocalStorage.getItem('account');
// TODO: 外部からはreadonlyに
export const $i = accountData ? reactive(JSON.parse(accountData) as AccountWithToken) : null;
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
export const iAmAdmin = $i != null && $i.isAdmin;
export function signinRequired() {
if ($i == null) throw new Error('signin required');
return $i;
}
export let notesCount = $i == null ? 0 : $i.notesCount;
export function incNotesCount() {
notesCount++;
}
if (_DEV_) {
(window as any).$i = $i;
}

View File

@@ -9,7 +9,6 @@ export type Keys = (
'instance' | 'instance' |
'instanceCachedAt' | 'instanceCachedAt' |
'account' | 'account' |
'accounts' |
'latestDonationInfoShownAt' | 'latestDonationInfoShownAt' |
'neverShowDonationInfo' | 'neverShowDonationInfo' |
'neverShowLocalOnlyInfo' | 'neverShowLocalOnlyInfo' |

View File

@@ -5,7 +5,7 @@
import { computed, reactive } from 'vue'; import { computed, reactive } from 'vue';
import { clearCache } from './utility/clear-cache.js'; import { clearCache } from './utility/clear-cache.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js'; import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
import { lookup } from '@/utility/lookup.js'; import { lookup } from '@/utility/lookup.js';

View File

@@ -143,11 +143,11 @@ import MkInfo from '@/components/MkInfo.vue';
import { physics } from '@/utility/physics.js'; import { physics } from '@/utility/physics.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { store } from '@/store.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { claimAchievement, claimedAchievements } from '@/utility/achievements.js'; import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { prefer } from '@/preferences.js';
const patronsWithIcon = [{ const patronsWithIcon = [{
name: 'カイヤン', name: 'カイヤン',
@@ -406,7 +406,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
const containerEl = shallowRef<HTMLElement>(); const containerEl = shallowRef<HTMLElement>();
function iconLoaded() { function iconLoaded() {
const emojis = store.s.reactions; const emojis = prefer.s.emojiPalettes[0].emojis;
const containerWidth = containerEl.value.offsetWidth; const containerWidth = containerEl.value.offsetWidth;
for (let i = 0; i < 32; i++) { for (let i = 0; i < 32; i++) {
easterEggEmojis.value.push({ easterEggEmojis.value.push({

View File

@@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js'; import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
const customEmojiTags = getCustomEmojiTags(); const customEmojiTags = getCustomEmojiTags();
const q = ref(''); const q = ref('');

View File

@@ -17,7 +17,7 @@ import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
import MkAchievements from '@/components/MkAchievements.vue'; import MkAchievements from '@/components/MkAchievements.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { $i } from '@/account.js'; import { $i } from '@/i.js';
import { claimAchievement } from '@/utility/achievements.js'; import { claimAchievement } from '@/utility/achievements.js';
let timer: number | null; let timer: number | null;

View File

@@ -86,7 +86,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { iAmAdmin, iAmModerator } from '@/account.js'; import { iAmAdmin, iAmModerator } from '@/i.js';
const tab = ref('overview'); const tab = ref('overview');
const file = ref<Misskey.entities.DriveFile | null>(null); const file = ref<Misskey.entities.DriveFile | null>(null);

View File

@@ -233,7 +233,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { acct } from '@/filters/user.js'; import { acct } from '@/filters/user.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { iAmAdmin, $i, iAmModerator } from '@/account.js'; import { iAmAdmin, $i, iAmModerator } from '@/i.js';
import MkRolePreview from '@/components/MkRolePreview.vue'; import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';

View File

@@ -163,7 +163,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js'; import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useForm } from '@/utility/use-form.js'; import { useForm } from '@/use/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue'; import MkFormFooter from '@/components/MkFormFooter.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';

View File

@@ -18,7 +18,7 @@ import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient'; import gradient from 'chartjs-plugin-gradient';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js'; import { chartVLine } from '@/utility/chart-vline.js';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';

View File

@@ -25,7 +25,7 @@ import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient'; import gradient from 'chartjs-plugin-gradient';
import isChromatic from 'chromatic'; import isChromatic from 'chromatic';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js'; import { chartVLine } from '@/utility/chart-vline.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { alpha } from '@/utility/color.js'; import { alpha } from '@/utility/color.js';

View File

@@ -54,7 +54,7 @@ import { misskeyApiGet } from '@/utility/misskey-api.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
import MkNumberDiff from '@/components/MkNumberDiff.vue'; import MkNumberDiff from '@/components/MkNumberDiff.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
const topSubInstancesForPie = ref<InstanceForPie[] | null>(null); const topSubInstancesForPie = ref<InstanceForPie[] | null>(null);
const topPubInstancesForPie = ref<InstanceForPie[] | null>(null); const topPubInstancesForPie = ref<InstanceForPie[] | null>(null);

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, shallowRef } from 'vue'; import { onMounted, shallowRef } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';
export type InstanceForPie = { export type InstanceForPie = {

View File

@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, shallowRef } from 'vue'; import { onMounted, shallowRef } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js'; import { chartVLine } from '@/utility/chart-vline.js';
import { alpha } from '@/utility/color.js'; import { alpha } from '@/utility/color.js';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';

View File

@@ -119,7 +119,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkLink from '@/components/MkLink.vue'; import MkLink from '@/components/MkLink.vue';
import { useForm } from '@/utility/use-form.js'; import { useForm } from '@/use/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue'; import MkFormFooter from '@/components/MkFormFooter.vue';
const meta = await misskeyApi('admin/meta'); const meta = await misskeyApi('admin/meta');

View File

@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, shallowRef } from 'vue'; import { onMounted, shallowRef } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; import { useChartTooltip } from '@/use/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js'; import { chartVLine } from '@/utility/chart-vline.js';
import { alpha } from '@/utility/color.js'; import { alpha } from '@/utility/color.js';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';

View File

@@ -135,7 +135,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js'; import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { useForm } from '@/utility/use-form.js'; import { useForm } from '@/use/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue'; import MkFormFooter from '@/components/MkFormFooter.vue';
const meta = await misskeyApi('admin/meta'); const meta = await misskeyApi('admin/meta');

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