Compare commits
65 Commits
2025.3.2-a
...
2025.3.2-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcb891e4fd | ||
|
|
152660fcf2 | ||
|
|
a1204f5e3e | ||
|
|
7acd3d1a88 | ||
|
|
8c9ec5827f | ||
|
|
44073736de | ||
|
|
0126dba475 | ||
|
|
3280a3d661 | ||
|
|
bdf80c49d8 | ||
|
|
59169a6450 | ||
|
|
5d228fb0f3 | ||
|
|
10b67e1b3a | ||
|
|
3ced310f77 | ||
|
|
010ec113c2 | ||
|
|
30005ba959 | ||
|
|
6b69588c03 | ||
|
|
8593aa1418 | ||
|
|
9876ff9a7a | ||
|
|
ce6eba77d9 | ||
|
|
9b2af53025 | ||
|
|
7b6ff19ea3 | ||
|
|
c9fa95429a | ||
|
|
e5d117dc98 | ||
|
|
4a73feb041 | ||
|
|
a06b9eefaa | ||
|
|
3129fcf164 | ||
|
|
35a4544477 | ||
|
|
aa1cc2f817 | ||
|
|
15685be4cc | ||
|
|
8508c4dadc | ||
|
|
e594fb0037 | ||
|
|
a369721791 | ||
|
|
f8e244f48d | ||
|
|
8410611512 | ||
|
|
caab1ec7c3 | ||
|
|
ffade9740e | ||
|
|
b03bcf26cd | ||
|
|
ddbc83b2e4 | ||
|
|
d185785f20 | ||
|
|
02d7fbefc4 | ||
|
|
f7ea92c68c | ||
|
|
e891d5c5d3 | ||
|
|
57a6b630b7 | ||
|
|
eda768a08c | ||
|
|
1f345eb839 | ||
|
|
1f2801af02 | ||
|
|
a4ba096e2a | ||
|
|
6841cdfa76 | ||
|
|
794f360bc2 | ||
|
|
f797765b1d | ||
|
|
9dce512fbb | ||
|
|
9e91f85370 | ||
|
|
9998cb84e8 | ||
|
|
5ed1101bbd | ||
|
|
6c9153300d | ||
|
|
7957ee5191 | ||
|
|
b200743845 | ||
|
|
08f7e7d9b3 | ||
|
|
16ad6b3f6c | ||
|
|
4df9083bf0 | ||
|
|
6419af2179 | ||
|
|
d9858b03c9 | ||
|
|
88efc0a3be | ||
|
|
ac21fa7194 | ||
|
|
c76afce9a7 |
@@ -6,10 +6,17 @@
|
||||
### Client
|
||||
- Feat: 設定の管理が強化されました
|
||||
- 自動でバックアップされるように
|
||||
- 任意の設定項目をデバイス間で同期できるように(実験的)
|
||||
- Enhance: プラグインの管理が強化されました
|
||||
- インストール/アンインストール/設定の変更時にリロード不要になりました
|
||||
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
|
||||
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
|
||||
- Enhance: テーマ設定画面のデザインを改善
|
||||
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
|
||||
|
||||
### Server
|
||||
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
|
||||
|
||||
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
|
||||
|
||||
## 2025.3.1
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 148 KiB |
136
locales/index.d.ts
vendored
@@ -5310,6 +5310,130 @@ export interface Locale extends ILocale {
|
||||
* 復元
|
||||
*/
|
||||
"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": {
|
||||
/**
|
||||
* プロファイル名
|
||||
@@ -5395,6 +5519,10 @@ export interface Locale extends ILocale {
|
||||
* リモートサーバーに連合されたノートには効果が及ばない場合があります。
|
||||
*/
|
||||
"mayNotEffectForFederatedNotes": string;
|
||||
/**
|
||||
* これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。
|
||||
*/
|
||||
"mayNotEffectSomeSituations": string;
|
||||
/**
|
||||
* 指定した時間を経過しているノート
|
||||
*/
|
||||
@@ -7742,6 +7870,10 @@ export interface Locale extends ILocale {
|
||||
* 標準のテーマ
|
||||
*/
|
||||
"builtinThemes": string;
|
||||
/**
|
||||
* サーバーのテーマ
|
||||
*/
|
||||
"instanceTheme": string;
|
||||
/**
|
||||
* そのテーマは既にインストールされています
|
||||
*/
|
||||
@@ -9750,6 +9882,10 @@ export interface Locale extends ILocale {
|
||||
* 幅を自動調整
|
||||
*/
|
||||
"flexible": string;
|
||||
/**
|
||||
* プロファイル情報のデバイス間同期を有効にする
|
||||
*/
|
||||
"enableSyncBetweenDevicesForProfiles": string;
|
||||
"_columns": {
|
||||
/**
|
||||
* メイン
|
||||
|
||||
@@ -1323,6 +1323,40 @@ untitled: "無題"
|
||||
noName: "名前はありません"
|
||||
skip: "スキップ"
|
||||
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:
|
||||
profileName: "プロファイル名"
|
||||
@@ -1349,6 +1383,7 @@ _accountSettings:
|
||||
makeNotesHiddenBefore: "過去のノートを非公開化する"
|
||||
makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。"
|
||||
mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。"
|
||||
mayNotEffectSomeSituations: "これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。"
|
||||
notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート"
|
||||
notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート"
|
||||
|
||||
@@ -2030,6 +2065,7 @@ _theme:
|
||||
installed: "{name}をインストールしました"
|
||||
installedThemes: "インストールされたテーマ"
|
||||
builtinThemes: "標準のテーマ"
|
||||
instanceTheme: "サーバーのテーマ"
|
||||
alreadyInstalled: "そのテーマは既にインストールされています"
|
||||
invalid: "テーマの形式が間違っています"
|
||||
make: "テーマを作る"
|
||||
@@ -2577,6 +2613,7 @@ _deck:
|
||||
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
|
||||
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
|
||||
flexible: "幅を自動調整"
|
||||
enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする"
|
||||
|
||||
_columns:
|
||||
main: "メイン"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.3.2-alpha.2",
|
||||
"version": "2025.3.2-alpha.11",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,6 +24,7 @@
|
||||
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
||||
"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-frontend-search-index": "pnpm --filter frontend build-search-index",
|
||||
"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",
|
||||
"init": "pnpm migrate",
|
||||
@@ -65,12 +66,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "2.1.0",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/node": "22.13.10",
|
||||
"@typescript-eslint/eslint-plugin": "8.26.0",
|
||||
"@typescript-eslint/parser": "8.26.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "14.1.0",
|
||||
"eslint": "9.21.0",
|
||||
"eslint": "9.22.0",
|
||||
"globals": "16.0.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.6.1",
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { Config } from '@/config.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { bindThis } from '@/decorators.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 { Response } from 'node-fetch';
|
||||
import type { URL } from 'node:url';
|
||||
@@ -265,7 +265,7 @@ export class HttpRequestService {
|
||||
const finalUrl = res.url; // redirects may have been involved
|
||||
const activity = await res.json() as IObject;
|
||||
|
||||
assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail);
|
||||
assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail);
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type Logger from '@/logger.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';
|
||||
|
||||
type Request = {
|
||||
@@ -258,7 +258,7 @@ export class ApRequestService {
|
||||
const finalUrl = res.url; // redirects may have been involved
|
||||
const activity = await res.json() as IObject;
|
||||
|
||||
assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail);
|
||||
assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail);
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ function normalizeSynonymousSubdomain(url: URL | string): URL {
|
||||
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
|
||||
if (!activity.id) {
|
||||
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 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 finalUrlSecure = candidateUrlsParsed.every(it => it.protocol === 'https:');
|
||||
const finalUrlSecure = finalUrlParsed.protocol === 'https:';
|
||||
if (requestUrlSecure && !finalUrlSecure) {
|
||||
throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`);
|
||||
}
|
||||
|
||||
// Compare final URL to the ID
|
||||
if (!candidateUrlsParsed.some(it => it.href === idParsed.href)) {
|
||||
requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${candidateUrlsParsed.map(it => it.toString())})`);
|
||||
if (finalUrlParsed.href !== idParsed.href) {
|
||||
requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${finalUrlParsed.toString()})`);
|
||||
|
||||
// at lease host need to match exactly (ActivityPub requirement)
|
||||
if (!candidateUrlsParsed.some(it => idParsed.host === it.host)) {
|
||||
throw new Error(`bad Activity: id(${activity.id}) does not match response host(${candidateUrlsParsed.map(it => it.host)})`);
|
||||
if (idParsed.host !== finalUrlParsed.host) {
|
||||
throw new Error(`bad Activity: id(${activity.id}) does not match response host(${finalUrlParsed.host})`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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()})`);
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -8,7 +8,7 @@ import httpSignature from '@peertube/http-signature';
|
||||
|
||||
import { genRsaKeyPair } from '@/misc/gen-key-pair.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';
|
||||
|
||||
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
|
||||
@@ -66,23 +66,26 @@ describe('ap-request', () => {
|
||||
});
|
||||
|
||||
test('rejects non matching domain', () => {
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrls(
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrl(
|
||||
'https://alice.example.com/abc',
|
||||
{ id: 'https://alice.example.com/abc' } as IObject,
|
||||
[
|
||||
'https://alice.example.com/abc',
|
||||
],
|
||||
'https://alice.example.com/abc',
|
||||
FetchAllowSoftFailMask.Strict,
|
||||
), 'validation should pass base case');
|
||||
assert.throws(() => assertActivityMatchesUrls(
|
||||
assert.throws(() => assertActivityMatchesUrl(
|
||||
'https://alice.example.com/abc',
|
||||
{ id: 'https://bob.example.com/abc' } as IObject,
|
||||
[
|
||||
'https://alice.example.com/abc',
|
||||
],
|
||||
'https://alice.example.com/abc',
|
||||
FetchAllowSoftFailMask.Any,
|
||||
), '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
|
||||
// https://github.com/misskey-dev/misskey/issues/15039
|
||||
const withOrWithoutWWW = [
|
||||
@@ -97,89 +100,71 @@ describe('ap-request', () => {
|
||||
),
|
||||
withOrWithoutWWW,
|
||||
).forEach(([[a, b], c]) => {
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrls(
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrl(
|
||||
a,
|
||||
{ id: b } as IObject,
|
||||
[
|
||||
c,
|
||||
],
|
||||
c,
|
||||
FetchAllowSoftFailMask.Strict,
|
||||
), 'validation should pass with or without www. subdomain');
|
||||
});
|
||||
});
|
||||
|
||||
test('cross origin lookup', () => {
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrls(
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrl(
|
||||
'https://alice.example.com/abc',
|
||||
{ id: 'https://bob.example.com/abc' } as IObject,
|
||||
[
|
||||
'https://bob.example.com/abc',
|
||||
],
|
||||
'https://bob.example.com/abc',
|
||||
FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId,
|
||||
), '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',
|
||||
{ id: 'https://bob.example.com/abc' } as IObject,
|
||||
[
|
||||
'https://bob.example.com/abc',
|
||||
],
|
||||
'https://bob.example.com/abc',
|
||||
FetchAllowSoftFailMask.Strict,
|
||||
), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed');
|
||||
});
|
||||
|
||||
test('rejects non-canonical ID', () => {
|
||||
assert.throws(() => assertActivityMatchesUrls(
|
||||
assert.throws(() => assertActivityMatchesUrl(
|
||||
'https://alice.example.com/@alice',
|
||||
{ id: 'https://alice.example.com/users/alice' } as IObject,
|
||||
[
|
||||
'https://alice.example.com/users/alice'
|
||||
],
|
||||
'https://alice.example.com/users/alice',
|
||||
FetchAllowSoftFailMask.Strict,
|
||||
), 'throws if the response ID did not exactly match the expected ID');
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrls(
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrl(
|
||||
'https://alice.example.com/@alice',
|
||||
{ id: 'https://alice.example.com/users/alice' } as IObject,
|
||||
[
|
||||
'https://alice.example.com/users/alice',
|
||||
],
|
||||
'https://alice.example.com/users/alice',
|
||||
FetchAllowSoftFailMask.NonCanonicalId,
|
||||
), 'does not throw if non-canonical ID is allowed');
|
||||
});
|
||||
|
||||
test('origin relaxed alignment', () => {
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrls(
|
||||
assert.doesNotThrow(() => assertActivityMatchesUrl(
|
||||
'https://alice.example.com/abc',
|
||||
{ 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,
|
||||
), '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',
|
||||
{ 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,
|
||||
), 'validation should fail if response is a disjoint domain of the expected origin');
|
||||
assert.throws(() => assertActivityMatchesUrls(
|
||||
assert.throws(() => assertActivityMatchesUrl(
|
||||
'https://alice.example.com/abc',
|
||||
{ id: 'https://ap.alice.example.com/abc' } as IObject,
|
||||
[
|
||||
'https://ap.alice.example.com/abc',
|
||||
],
|
||||
'https://ap.alice.example.com/abc',
|
||||
FetchAllowSoftFailMask.Strict,
|
||||
), 'throws if relaxed origin is forbidden');
|
||||
});
|
||||
|
||||
test('resist HTTP downgrade', () => {
|
||||
assert.throws(() => assertActivityMatchesUrls(
|
||||
assert.throws(() => assertActivityMatchesUrl(
|
||||
'https://alice.example.com/abc',
|
||||
{ id: 'https://alice.example.com/abc' } as IObject,
|
||||
[
|
||||
'http://alice.example.com/abc',
|
||||
],
|
||||
'http://alice.example.com/abc',
|
||||
FetchAllowSoftFailMask.Strict,
|
||||
), 'throws if HTTP downgrade is detected');
|
||||
});
|
||||
|
||||
@@ -17,8 +17,52 @@ interface SatisfiesExpression extends estree.BaseExpression {
|
||||
reference: estree.Identifier;
|
||||
}
|
||||
|
||||
interface ImportDeclaration extends estree.ImportDeclaration {
|
||||
kind?: 'type';
|
||||
}
|
||||
|
||||
const 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) {
|
||||
switch (node.expression.type) {
|
||||
case 'ArrowFunctionExpression': {
|
||||
@@ -62,7 +106,7 @@ type ToKebab<T extends readonly string[]> = T extends readonly [
|
||||
: T extends readonly [
|
||||
infer XH extends string,
|
||||
...infer XR extends readonly string[]
|
||||
]
|
||||
]
|
||||
? `${XH}${XR extends readonly string[] ? `-${ToKebab<XR>}` : ''}`
|
||||
: '';
|
||||
|
||||
@@ -132,7 +176,7 @@ function toStories(component: string): Promise<string> {
|
||||
kind={'init' as const}
|
||||
shorthand
|
||||
/> as estree.Property,
|
||||
]
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/> as estree.ObjectExpression;
|
||||
@@ -155,7 +199,8 @@ function toStories(component: string): Promise<string> {
|
||||
/> as estree.ImportSpecifier,
|
||||
]),
|
||||
]}
|
||||
/> as estree.ImportDeclaration,
|
||||
kind={'type'}
|
||||
/> as ImportDeclaration,
|
||||
...(hasMsw
|
||||
? [
|
||||
<import-declaration
|
||||
@@ -165,8 +210,8 @@ function toStories(component: string): Promise<string> {
|
||||
local={<identifier name='msw' /> as estree.Identifier}
|
||||
/> as estree.ImportNamespaceSpecifier,
|
||||
]}
|
||||
/> as estree.ImportDeclaration,
|
||||
]
|
||||
/> as ImportDeclaration,
|
||||
]
|
||||
: []),
|
||||
...(hasImplStories
|
||||
? []
|
||||
@@ -176,8 +221,8 @@ function toStories(component: string): Promise<string> {
|
||||
specifiers={[
|
||||
<import-default-specifier local={identifier} /> as estree.ImportDefaultSpecifier,
|
||||
]}
|
||||
/> as estree.ImportDeclaration,
|
||||
]),
|
||||
/> as ImportDeclaration,
|
||||
]),
|
||||
...(hasMetaStories
|
||||
? [
|
||||
<import-declaration
|
||||
@@ -187,7 +232,7 @@ function toStories(component: string): Promise<string> {
|
||||
local={<identifier name='storiesMeta' /> as estree.Identifier}
|
||||
/> as estree.ImportNamespaceSpecifier,
|
||||
]}
|
||||
/> as estree.ImportDeclaration,
|
||||
/> as ImportDeclaration,
|
||||
]
|
||||
: []),
|
||||
<variable-declaration
|
||||
|
||||
BIN
packages/frontend/assets/bell_3d.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
packages/frontend/assets/cloud_3d.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
packages/frontend/assets/desktop_computer_3d.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
packages/frontend/assets/electric_plug_3d.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
packages/frontend/assets/gear_3d.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
packages/frontend/assets/link_3d.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
packages/frontend/assets/locked_with_key_3d.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
packages/frontend/assets/mens_room_3d.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/frontend/assets/musical_note_3d.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/frontend/assets/package_3d.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
packages/frontend/assets/prohibited_3d.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
packages/frontend/assets/speaker_high_volume_3d.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
packages/frontend/assets/unlocked_3d.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
@@ -74,7 +74,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
||||
let fetching = ref(true);
|
||||
let images = ref([]);
|
||||
function thumbnail(image) {
|
||||
return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
|
||||
return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
|
||||
}
|
||||
onMounted(() => {
|
||||
const image = [
|
||||
@@ -190,7 +190,7 @@ const index_photos = defineComponent({
|
||||
let fetching = ref(true);
|
||||
let images = ref([]);
|
||||
function thumbnail(image) {
|
||||
return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
|
||||
return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
|
||||
}
|
||||
onMounted(() => {
|
||||
const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
|
||||
|
||||
@@ -1213,22 +1213,37 @@ async function processVueFile(
|
||||
transformedCodeCache: Record<string, string>
|
||||
}> {
|
||||
const normalizedId = id.replace(/\\/g, '/'); // ファイルパスを正規化
|
||||
// すでにキャッシュに存在する場合は、そのまま返す
|
||||
if (transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) {
|
||||
|
||||
// 開発モード時はコード内容に変更があれば常に再処理する
|
||||
// コード内容が同じ場合のみキャッシュを使用
|
||||
const isDevMode = process.env.NODE_ENV === 'development';
|
||||
|
||||
const s = new MagicString(code); // magic-string のインスタンスを作成
|
||||
|
||||
if (!isDevMode && transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) {
|
||||
logger.info(`Using cached version for ${id}`);
|
||||
return {
|
||||
code: transformedCodeCache[normalizedId],
|
||||
map: null,
|
||||
map: s.generateMap({ source: id, includeContent: true }),
|
||||
transformedCodeCache
|
||||
};
|
||||
}
|
||||
|
||||
// すでに処理済みのファイルでコードに変更がない場合はキャッシュを返す
|
||||
if (transformedCodeCache[normalizedId] === code) {
|
||||
logger.info(`Code unchanged for ${id}, using cached version`);
|
||||
return {
|
||||
code: transformedCodeCache[normalizedId],
|
||||
map: s.generateMap({ source: id, includeContent: true }),
|
||||
transformedCodeCache
|
||||
};
|
||||
}
|
||||
|
||||
const s = new MagicString(code); // magic-string のインスタンスを作成
|
||||
const parsed = vueSfcParse(code, { filename: id });
|
||||
if (!parsed.descriptor.template) {
|
||||
return {
|
||||
code,
|
||||
map: null,
|
||||
map: s.generateMap({ source: id, includeContent: true }),
|
||||
transformedCodeCache
|
||||
};
|
||||
}
|
||||
@@ -1413,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
|
||||
export default function pluginCreateSearchIndex(options: Options): Plugin {
|
||||
@@ -1430,19 +1462,7 @@ export default function pluginCreateSearchIndex(options: Options): Plugin {
|
||||
return;
|
||||
}
|
||||
|
||||
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 を実行
|
||||
transformedCodeCache = await generateSearchIndex(options, transformedCodeCache);
|
||||
},
|
||||
|
||||
async transform(code, id) {
|
||||
@@ -1466,16 +1486,21 @@ export default function pluginCreateSearchIndex(options: Options): Plugin {
|
||||
if (isMatch) break; // いずれかのパターンでマッチしたら、outer loop も抜ける
|
||||
}
|
||||
|
||||
|
||||
if (!isMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ファイルの内容が変更された場合は再処理を行う
|
||||
const normalizedId = id.replace(/\\/g, '/');
|
||||
const hasContentChanged = !transformedCodeCache[normalizedId] || transformedCodeCache[normalizedId] !== code;
|
||||
|
||||
const transformed = await processVueFile(code, id, options, transformedCodeCache);
|
||||
transformedCodeCache = transformed.transformedCodeCache; // キャッシュを更新
|
||||
if (isDevServer) {
|
||||
await analyzeVueProps({ ...options, transformedCodeCache }); // analyzeVueProps を呼び出す
|
||||
|
||||
if (isDevServer && hasContentChanged) {
|
||||
await analyzeVueProps({ ...options, transformedCodeCache }); // ファイルが変更されたときのみ分析を実行
|
||||
}
|
||||
|
||||
return transformed;
|
||||
},
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"scripts": {
|
||||
"watch": "vite",
|
||||
"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\"",
|
||||
"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",
|
||||
@@ -133,6 +134,7 @@
|
||||
"start-server-and-test": "2.0.10",
|
||||
"storybook": "8.6.4",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-node": "3.0.8",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "3.0.8",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
|
||||
15
packages/frontend/scripts/generate-search-index.ts
Normal 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();
|
||||
@@ -3,116 +3,55 @@
|
||||
* 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 { apiUrl } from '@@/js/config.js';
|
||||
import type { MenuItem, MenuButton } from '@/types/menu.js';
|
||||
import { defaultMemoryStorage } from '@/memory-storage';
|
||||
import { apiUrl, host } from '@@/js/config.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
|
||||
import { i18n } from '@/i18n.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 { misskeyApi } from '@/utility/misskey-api.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を同期
|
||||
|
||||
type Account = Misskey.entities.MeDetailed & { token: string };
|
||||
type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
|
||||
|
||||
const accountData = miLocalStorage.getItem('account');
|
||||
|
||||
// TODO: 外部からはreadonlyに
|
||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : 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 async function getAccounts(): Promise<{
|
||||
host: string;
|
||||
user: Misskey.entities.User;
|
||||
token: string | null;
|
||||
}[]> {
|
||||
const tokens = store.s.accountTokens;
|
||||
const accounts = prefer.s.accounts;
|
||||
return accounts.map(([host, user]) => ({
|
||||
host,
|
||||
user,
|
||||
token: tokens[host + '/' + user.id] ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
export let notesCount = $i == null ? 0 : $i.notesCount;
|
||||
export function incNotesCount() {
|
||||
notesCount++;
|
||||
}
|
||||
|
||||
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) {
|
||||
await navigator.serviceWorker.getRegistrations()
|
||||
.then(registrations => {
|
||||
return Promise.all(registrations.map(registration => registration.unregister()));
|
||||
});
|
||||
}
|
||||
} catch (err) {}
|
||||
//#endregion
|
||||
|
||||
if (accounts.length > 0) login(accounts[0].token);
|
||||
else unisonReload('/');
|
||||
}
|
||||
|
||||
export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> {
|
||||
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 }]));
|
||||
async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) {
|
||||
if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) {
|
||||
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
|
||||
prefer.commit('accounts', [...prefer.s.accounts, [host, user]]);
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
export async function removeAccount(host: string, id: AccountWithToken['id']) {
|
||||
const tokens = JSON.parse(JSON.stringify(store.s.accountTokens));
|
||||
delete tokens[host + '/' + id];
|
||||
store.set('accountTokens', tokens);
|
||||
prefer.commit('accounts', prefer.s.accounts.filter(x => x[0] !== host || x[1].id !== id));
|
||||
}
|
||||
|
||||
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の認証とかで使う
|
||||
const isAccountDeleted = Symbol('isAccountDeleted');
|
||||
|
||||
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Misskey.entities.MeDetailed> {
|
||||
return new Promise((done, fail) => {
|
||||
window.fetch(`${apiUrl}/i`, {
|
||||
method: 'POST',
|
||||
@@ -123,7 +62,7 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
|
||||
'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) {
|
||||
// サーバーエラー(5xx)の場合をrejectとする
|
||||
// (認証エラーなど4xxはresolve)
|
||||
@@ -166,46 +105,70 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
|
||||
});
|
||||
}
|
||||
|
||||
// rejectかつ理由がtrueの場合、削除対象であることを示す
|
||||
fail(true);
|
||||
fail(isAccountDeleted);
|
||||
} else {
|
||||
(res as Account).token = token;
|
||||
done(res as Account);
|
||||
done(res);
|
||||
}
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateAccount(accountData: Account) {
|
||||
export function updateCurrentAccount(accountData: Misskey.entities.MeDetailed) {
|
||||
if (!$i) return;
|
||||
const token = $i.token;
|
||||
for (const key of Object.keys($i)) {
|
||||
delete $i[key];
|
||||
}
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$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));
|
||||
}
|
||||
|
||||
export function updateAccountPartial(accountData: Partial<Account>) {
|
||||
export function updateCurrentAccountPartial(accountData: Partial<Misskey.entities.MeDetailed>) {
|
||||
if (!$i) return;
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$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));
|
||||
}
|
||||
|
||||
export async function refreshAccount() {
|
||||
export async function refreshCurrentAccount() {
|
||||
if (!$i) return;
|
||||
return fetchAccount($i.token, $i.id)
|
||||
.then(updateAccount, reason => {
|
||||
if (reason === true) return signout();
|
||||
return;
|
||||
});
|
||||
return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => {
|
||||
if (reason === isAccountDeleted) {
|
||||
removeAccount(host, $i.id);
|
||||
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 { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
|
||||
success: false,
|
||||
@@ -213,19 +176,18 @@ export async function login(token: Account['token'], redirect?: string) {
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
if (_DEV_) console.log('logging as token ', token);
|
||||
const me = await fetchAccount(token, undefined, true)
|
||||
.catch(reason => {
|
||||
if (reason === true) {
|
||||
// 削除対象の場合
|
||||
removeAccount(token);
|
||||
}
|
||||
|
||||
showing.value = false;
|
||||
throw reason;
|
||||
});
|
||||
miLocalStorage.setItem('account', JSON.stringify(me));
|
||||
await addAccount(me.id, token);
|
||||
const me = await fetchAccount(token, undefined, true).catch(reason => {
|
||||
showing.value = false;
|
||||
throw reason;
|
||||
});
|
||||
|
||||
miLocalStorage.setItem('account', JSON.stringify({
|
||||
...me,
|
||||
token,
|
||||
}));
|
||||
|
||||
await addAccount(host, me, token);
|
||||
|
||||
if (redirect) {
|
||||
// 他のタブは再読み込みするだけ
|
||||
@@ -238,60 +200,51 @@ export async function login(token: Account['token'], redirect?: string) {
|
||||
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: {
|
||||
includeCurrentAccount?: boolean;
|
||||
withExtraOperation: boolean;
|
||||
active?: Misskey.entities.UserDetailed['id'];
|
||||
onChoose?: (account: Misskey.entities.UserDetailed) => void;
|
||||
active?: Misskey.entities.User['id'];
|
||||
onChoose?: (account: Misskey.entities.User) => void;
|
||||
}, ev: MouseEvent) {
|
||||
if (!$i) return;
|
||||
|
||||
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
||||
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) {
|
||||
function createItem(host: string, account: Misskey.entities.User): MenuItem {
|
||||
return {
|
||||
type: 'user' as const,
|
||||
user: account,
|
||||
active: opts.active != null ? opts.active === account.id : false,
|
||||
action: () => {
|
||||
action: async () => {
|
||||
if (opts.onChoose) {
|
||||
opts.onChoose(account);
|
||||
} 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[] = [];
|
||||
|
||||
// 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) {
|
||||
menuItems.push({
|
||||
type: 'link',
|
||||
@@ -303,10 +256,10 @@ export async function openAccountMenu(opts: {
|
||||
});
|
||||
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem($i));
|
||||
menuItems.push(createItem(host, $i));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItemPromises);
|
||||
menuItems.push(...accountItems);
|
||||
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
@@ -326,7 +279,7 @@ export async function openAccountMenu(opts: {
|
||||
action: () => {
|
||||
getAccountWithSignupDialog().then(res => {
|
||||
if (res != null) {
|
||||
switchAccountWithToken(res.token);
|
||||
switchAccount(host, res.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -339,10 +292,10 @@ export async function openAccountMenu(opts: {
|
||||
});
|
||||
} else {
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem($i));
|
||||
menuItems.push(createItem(host, $i));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItemPromises);
|
||||
menuItems.push(...accountItems);
|
||||
}
|
||||
|
||||
popupMenu(menuItems, ev.currentTarget ?? ev.target, {
|
||||
@@ -354,7 +307,8 @@ export function getAccountWithSigninDialog(): Promise<{ id: string, token: strin
|
||||
return new Promise((resolve) => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
||||
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 });
|
||||
},
|
||||
cancelled: () => {
|
||||
@@ -371,7 +325,9 @@ export function getAccountWithSignupDialog(): Promise<{ id: string, token: strin
|
||||
return new Promise((resolve) => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
|
||||
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 });
|
||||
},
|
||||
cancelled: () => {
|
||||
@@ -383,7 +339,3 @@ export function getAccountWithSignupDialog(): Promise<{ id: string, token: strin
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (_DEV_) {
|
||||
(window as any).$i = $i;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { url, lang } from '@@/js/config.js';
|
||||
import { assertStringAndIsIn } from './common.js';
|
||||
import * as os from '@/os.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 { customEmojis } from '@/custom-emojis.js';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import components from '@/components/index.js';
|
||||
import { applyTheme } from '@/theme.js';
|
||||
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.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 { fetchInstance, instance } from '@/instance.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 { createMainRouter } from '@/router/definition.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
export async function common(createVue: () => App<Element>) {
|
||||
console.info(`Misskey v${version}`);
|
||||
@@ -38,11 +39,6 @@ export async function common(createVue: () => App<Element>) {
|
||||
|
||||
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 => {
|
||||
console.error(event);
|
||||
/*
|
||||
@@ -154,26 +150,26 @@ export async function common(createVue: () => App<Element>) {
|
||||
//#endregion
|
||||
|
||||
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
|
||||
watch(store.reactiveState.darkMode, (darkMode) => {
|
||||
watch(store.r.darkMode, (darkMode) => {
|
||||
applyTheme(darkMode
|
||||
? (prefer.s.darkTheme ?? defaultDarkTheme)
|
||||
: (prefer.s.lightTheme ?? defaultLightTheme),
|
||||
);
|
||||
}, { immediate: miLocalStorage.getItem('theme') == null });
|
||||
|
||||
document.documentElement.dataset.colorScheme = store.state.darkMode ? 'dark' : 'light';
|
||||
document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
|
||||
|
||||
const darkTheme = prefer.model('darkTheme');
|
||||
const lightTheme = prefer.model('lightTheme');
|
||||
|
||||
watch(darkTheme, (theme) => {
|
||||
if (store.state.darkMode) {
|
||||
if (store.s.darkMode) {
|
||||
applyTheme(theme ?? defaultDarkTheme);
|
||||
}
|
||||
});
|
||||
|
||||
watch(lightTheme, (theme) => {
|
||||
if (!store.state.darkMode) {
|
||||
if (!store.s.darkMode) {
|
||||
applyTheme(theme ?? defaultLightTheme);
|
||||
}
|
||||
});
|
||||
@@ -190,16 +186,16 @@ export async function common(createVue: () => App<Element>) {
|
||||
});
|
||||
//#endregion
|
||||
|
||||
if (prefer.s.darkTheme && store.state.darkMode) {
|
||||
if (prefer.s.darkTheme && store.s.darkMode) {
|
||||
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
|
||||
} else if (prefer.s.lightTheme && !store.state.darkMode) {
|
||||
} else if (prefer.s.lightTheme && !store.s.darkMode) {
|
||||
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
|
||||
}
|
||||
|
||||
fetchInstanceMetaPromise.then(() => {
|
||||
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
|
||||
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.set('lightTheme', JSON.parse(instance.defaultLightTheme));
|
||||
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
|
||||
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
|
||||
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
|
||||
});
|
||||
|
||||
watch(prefer.r.overridedDeviceKind, (kind) => {
|
||||
@@ -244,7 +240,7 @@ export async function common(createVue: () => App<Element>) {
|
||||
console.log('account cache found. refreshing...');
|
||||
}
|
||||
|
||||
refreshAccount();
|
||||
refreshCurrentAccount();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
@@ -326,6 +322,7 @@ export async function common(createVue: () => App<Element>) {
|
||||
|
||||
return {
|
||||
isClientUpdated,
|
||||
lastVersion,
|
||||
app,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,14 +6,17 @@
|
||||
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
||||
import { ui } from '@@/js/config.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { common } from './common.js';
|
||||
import type { Component } from 'vue';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
import type { DeckProfile } from '@/deck.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { alert, confirm, popup, post, toast } from '@/os.js';
|
||||
import { useStream } from '@/stream.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 { ColdDeviceStorage, store } from '@/store.js';
|
||||
import { reactionPicker } from '@/utility/reaction-picker.js';
|
||||
@@ -27,10 +30,13 @@ import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { launchPlugin } 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() {
|
||||
const { isClientUpdated } = await common(() => {
|
||||
const { isClientUpdated, lastVersion } = await common(() => {
|
||||
let uiStyle = ui;
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -70,6 +76,137 @@ export async function mainBoot() {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {
|
||||
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();
|
||||
@@ -105,9 +242,7 @@ export async function mainBoot() {
|
||||
removeCustomEmojis(emojiData.emojis);
|
||||
});
|
||||
|
||||
for (const plugin of prefer.s.plugins.filter(p => p.active)) {
|
||||
launchPlugin(plugin);
|
||||
}
|
||||
launchPlugins();
|
||||
|
||||
try {
|
||||
if (prefer.s.enableSeasonalScreenEffect) {
|
||||
@@ -138,100 +273,7 @@ export async function mainBoot() {
|
||||
|
||||
if ($i) {
|
||||
store.loaded.then(async () => {
|
||||
// prefereces migration
|
||||
// TODO: そのうち消す
|
||||
if (store.state.menu.length > 0) {
|
||||
const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []);
|
||||
if (themes.length > 0) {
|
||||
prefer.set('themes', themes);
|
||||
}
|
||||
const plugins = ColdDeviceStorage.get('plugins');
|
||||
prefer.set('plugins', plugins.map(p => ({
|
||||
...p,
|
||||
installId: (p as any).id,
|
||||
id: undefined,
|
||||
})));
|
||||
prefer.set('lightTheme', ColdDeviceStorage.get('lightTheme'));
|
||||
prefer.set('darkTheme', ColdDeviceStorage.get('darkTheme'));
|
||||
prefer.set('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
|
||||
prefer.set('overridedDeviceKind', store.state.overridedDeviceKind);
|
||||
prefer.set('widgets', store.state.widgets);
|
||||
prefer.set('keepCw', store.state.keepCw);
|
||||
prefer.set('collapseRenotes', store.state.collapseRenotes);
|
||||
prefer.set('rememberNoteVisibility', store.state.rememberNoteVisibility);
|
||||
prefer.set('uploadFolder', store.state.uploadFolder);
|
||||
prefer.set('keepOriginalUploading', store.state.keepOriginalUploading);
|
||||
prefer.set('menu', store.state.menu);
|
||||
prefer.set('statusbars', store.state.statusbars);
|
||||
prefer.set('pinnedUserLists', store.state.pinnedUserLists);
|
||||
prefer.set('serverDisconnectedBehavior', store.state.serverDisconnectedBehavior);
|
||||
prefer.set('nsfw', store.state.nsfw);
|
||||
prefer.set('highlightSensitiveMedia', store.state.highlightSensitiveMedia);
|
||||
prefer.set('animation', store.state.animation);
|
||||
prefer.set('animatedMfm', store.state.animatedMfm);
|
||||
prefer.set('advancedMfm', store.state.advancedMfm);
|
||||
prefer.set('showReactionsCount', store.state.showReactionsCount);
|
||||
prefer.set('enableQuickAddMfmFunction', store.state.enableQuickAddMfmFunction);
|
||||
prefer.set('loadRawImages', store.state.loadRawImages);
|
||||
prefer.set('imageNewTab', store.state.imageNewTab);
|
||||
prefer.set('disableShowingAnimatedImages', store.state.disableShowingAnimatedImages);
|
||||
prefer.set('emojiStyle', store.state.emojiStyle);
|
||||
prefer.set('menuStyle', store.state.menuStyle);
|
||||
prefer.set('useBlurEffectForModal', store.state.useBlurEffectForModal);
|
||||
prefer.set('useBlurEffect', store.state.useBlurEffect);
|
||||
prefer.set('showFixedPostForm', store.state.showFixedPostForm);
|
||||
prefer.set('showFixedPostFormInChannel', store.state.showFixedPostFormInChannel);
|
||||
prefer.set('enableInfiniteScroll', store.state.enableInfiniteScroll);
|
||||
prefer.set('useReactionPickerForContextMenu', store.state.useReactionPickerForContextMenu);
|
||||
prefer.set('showGapBetweenNotesInTimeline', store.state.showGapBetweenNotesInTimeline);
|
||||
prefer.set('instanceTicker', store.state.instanceTicker);
|
||||
prefer.set('emojiPickerScale', store.state.emojiPickerScale);
|
||||
prefer.set('emojiPickerWidth', store.state.emojiPickerWidth);
|
||||
prefer.set('emojiPickerHeight', store.state.emojiPickerHeight);
|
||||
prefer.set('emojiPickerStyle', store.state.emojiPickerStyle);
|
||||
prefer.set('reportError', store.state.reportError);
|
||||
prefer.set('squareAvatars', store.state.squareAvatars);
|
||||
prefer.set('showAvatarDecorations', store.state.showAvatarDecorations);
|
||||
prefer.set('numberOfPageCache', store.state.numberOfPageCache);
|
||||
prefer.set('showNoteActionsOnlyHover', store.state.showNoteActionsOnlyHover);
|
||||
prefer.set('showClipButtonInNoteFooter', store.state.showClipButtonInNoteFooter);
|
||||
prefer.set('reactionsDisplaySize', store.state.reactionsDisplaySize);
|
||||
prefer.set('limitWidthOfReaction', store.state.limitWidthOfReaction);
|
||||
prefer.set('forceShowAds', store.state.forceShowAds);
|
||||
prefer.set('aiChanMode', store.state.aiChanMode);
|
||||
prefer.set('devMode', store.state.devMode);
|
||||
prefer.set('mediaListWithOneImageAppearance', store.state.mediaListWithOneImageAppearance);
|
||||
prefer.set('notificationPosition', store.state.notificationPosition);
|
||||
prefer.set('notificationStackAxis', store.state.notificationStackAxis);
|
||||
prefer.set('enableCondensedLine', store.state.enableCondensedLine);
|
||||
prefer.set('keepScreenOn', store.state.keepScreenOn);
|
||||
prefer.set('disableStreamingTimeline', store.state.disableStreamingTimeline);
|
||||
prefer.set('useGroupedNotifications', store.state.useGroupedNotifications);
|
||||
prefer.set('dataSaver', store.state.dataSaver);
|
||||
prefer.set('enableSeasonalScreenEffect', store.state.enableSeasonalScreenEffect);
|
||||
prefer.set('enableHorizontalSwipe', store.state.enableHorizontalSwipe);
|
||||
prefer.set('useNativeUiForVideoAudioPlayer', store.state.useNativeUIForVideoAudioPlayer);
|
||||
prefer.set('keepOriginalFilename', store.state.keepOriginalFilename);
|
||||
prefer.set('alwaysConfirmFollow', store.state.alwaysConfirmFollow);
|
||||
prefer.set('confirmWhenRevealingSensitiveMedia', store.state.confirmWhenRevealingSensitiveMedia);
|
||||
prefer.set('contextMenu', store.state.contextMenu);
|
||||
prefer.set('skipNoteRender', store.state.skipNoteRender);
|
||||
prefer.set('showSoftWordMutedWord', store.state.showSoftWordMutedWord);
|
||||
prefer.set('confirmOnReact', store.state.confirmOnReact);
|
||||
prefer.set('sound.masterVolume', store.state.sound_masterVolume);
|
||||
prefer.set('sound.notUseSound', store.state.sound_notUseSound);
|
||||
prefer.set('sound.useSoundOnlyWhenActive', store.state.sound_useSoundOnlyWhenActive);
|
||||
prefer.set('sound.on.note', store.state.sound_note as any);
|
||||
prefer.set('sound.on.noteMy', store.state.sound_noteMy as any);
|
||||
prefer.set('sound.on.notification', store.state.sound_notification as any);
|
||||
prefer.set('sound.on.reaction', store.state.sound_reaction as any);
|
||||
store.set('deck.profile', deckStore.state.profile);
|
||||
store.set('deck.columns', deckStore.state.columns);
|
||||
store.set('deck.layout', deckStore.state.layout);
|
||||
store.set('menu', []);
|
||||
}
|
||||
|
||||
if (store.state.accountSetupWizard !== -1) {
|
||||
if (store.s.accountSetupWizard !== -1) {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
@@ -440,11 +482,11 @@ export async function mainBoot() {
|
||||
|
||||
// 自分の情報が更新されたとき
|
||||
main.on('meUpdated', i => {
|
||||
updateAccountPartial(i);
|
||||
updateCurrentAccountPartial(i);
|
||||
});
|
||||
|
||||
main.on('readAllNotifications', () => {
|
||||
updateAccountPartial({
|
||||
updateCurrentAccountPartial({
|
||||
hasUnreadNotification: false,
|
||||
unreadNotificationsCount: 0,
|
||||
});
|
||||
@@ -452,39 +494,39 @@ export async function mainBoot() {
|
||||
|
||||
main.on('unreadNotification', () => {
|
||||
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
|
||||
updateAccountPartial({
|
||||
updateCurrentAccountPartial({
|
||||
hasUnreadNotification: true,
|
||||
unreadNotificationsCount,
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadMention', () => {
|
||||
updateAccountPartial({ hasUnreadMentions: true });
|
||||
updateCurrentAccountPartial({ hasUnreadMentions: true });
|
||||
});
|
||||
|
||||
main.on('readAllUnreadMentions', () => {
|
||||
updateAccountPartial({ hasUnreadMentions: false });
|
||||
updateCurrentAccountPartial({ hasUnreadMentions: false });
|
||||
});
|
||||
|
||||
main.on('unreadSpecifiedNote', () => {
|
||||
updateAccountPartial({ hasUnreadSpecifiedNotes: true });
|
||||
updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: true });
|
||||
});
|
||||
|
||||
main.on('readAllUnreadSpecifiedNotes', () => {
|
||||
updateAccountPartial({ hasUnreadSpecifiedNotes: false });
|
||||
updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: false });
|
||||
});
|
||||
|
||||
main.on('readAllAntennas', () => {
|
||||
updateAccountPartial({ hasUnreadAntenna: false });
|
||||
updateCurrentAccountPartial({ hasUnreadAntenna: false });
|
||||
});
|
||||
|
||||
main.on('unreadAntenna', () => {
|
||||
updateAccountPartial({ hasUnreadAntenna: true });
|
||||
updateCurrentAccountPartial({ hasUnreadAntenna: true });
|
||||
sound.playMisskeySfx('antenna');
|
||||
});
|
||||
|
||||
main.on('readAllAnnouncements', () => {
|
||||
updateAccountPartial({ hasUnreadAnnouncement: false });
|
||||
updateCurrentAccountPartial({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
// 個人宛てお知らせが発行されたとき
|
||||
@@ -504,7 +546,7 @@ export async function mainBoot() {
|
||||
post();
|
||||
},
|
||||
'd': () => {
|
||||
store.set('darkMode', !store.state.darkMode);
|
||||
store.set('darkMode', !store.s.darkMode);
|
||||
},
|
||||
's': () => {
|
||||
mainRouter.push('/search');
|
||||
|
||||
@@ -29,7 +29,8 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
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<{
|
||||
announcement: Misskey.entities.Announcement;
|
||||
@@ -51,7 +52,7 @@ async function ok() {
|
||||
|
||||
modal.value?.close();
|
||||
misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
|
||||
updateAccountPartial({
|
||||
updateCurrentAccountPartial({
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,10 +117,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
||||
@@ -158,7 +157,7 @@ async function init() {
|
||||
|
||||
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) {
|
||||
const usersRes = await misskeyApi('users/show', {
|
||||
@@ -170,7 +169,7 @@ async function init() {
|
||||
|
||||
users.value.set(user.id, {
|
||||
...user,
|
||||
token: accounts.find(a => a.id === user.id)!.token,
|
||||
token: accounts.find(a => a.user.id === user.id)!.token,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ const emojiDb = computed(() => {
|
||||
url: char2path(x.char),
|
||||
}));
|
||||
|
||||
for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
|
||||
for (const [emoji, keywords] of Object.entries(index)) {
|
||||
for (const k of keywords) {
|
||||
unicodeEmojiDB.push({
|
||||
@@ -155,7 +155,7 @@ function complete(type: string, value: any) {
|
||||
emit('done', { type, value });
|
||||
emit('closed');
|
||||
if (type === 'emoji') {
|
||||
let recents = store.state.recentlyUsedEmojis;
|
||||
let recents = store.s.recentlyUsedEmojis;
|
||||
recents = recents.filter((emoji: any) => emoji !== value);
|
||||
recents.unshift(value);
|
||||
store.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
@@ -238,7 +238,7 @@ function exec() {
|
||||
} else if (props.type === 'emoji') {
|
||||
if (!props.q || props.q === '') {
|
||||
// 最近使った絵文字をサジェスト
|
||||
emojis.value = store.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
||||
emojis.value = store.s.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<button
|
||||
v-if="!link"
|
||||
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"
|
||||
:name="name"
|
||||
:value="value"
|
||||
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</button>
|
||||
<MkA
|
||||
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 ?? '#'"
|
||||
:behavior="linkBehavior"
|
||||
@mousedown="onMousedown"
|
||||
@@ -57,6 +57,7 @@ const props = defineProps<{
|
||||
name?: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
iconOnly?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -147,6 +148,11 @@ function onMousedown(evt: MouseEvent): void {
|
||||
background: var(--MI_THEME-buttonHoverBg);
|
||||
}
|
||||
|
||||
&.iconOnly {
|
||||
padding: 7px;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
&.small {
|
||||
font-size: 90%;
|
||||
padding: 6px 12px;
|
||||
@@ -220,28 +226,28 @@ function onMousedown(evt: MouseEvent): void {
|
||||
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
|
||||
|
||||
&: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 {
|
||||
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 {
|
||||
font-weight: bold;
|
||||
color: #ff2a2a;
|
||||
color: var(--MI_THEME-error);
|
||||
|
||||
&.primary {
|
||||
color: #fff;
|
||||
background: #ff2a2a;
|
||||
background: var(--MI_THEME-error);
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: #ff4242;
|
||||
background: hsl(from var(--MI_THEME-error) h s calc(l + 10));
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: #d42e2e;
|
||||
background: hsl(from var(--MI_THEME-error) h s calc(l - 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ async function requestRender() {
|
||||
|
||||
captchaWidgetId.value = captcha.value.render(elem, {
|
||||
sitekey: props.sitekey,
|
||||
theme: store.state.darkMode ? 'dark' : 'light',
|
||||
theme: store.s.darkMode ? 'dark' : 'light',
|
||||
callback: callback,
|
||||
'expired-callback': () => callback(undefined),
|
||||
'error-callback': () => callback(undefined),
|
||||
|
||||
@@ -55,7 +55,7 @@ import { Chart } from 'chart.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApiGet } from '@/utility/misskey-api.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 { alpha } from '@/utility/color.js';
|
||||
import date from '@/filters/date.js';
|
||||
@@ -161,7 +161,7 @@ const render = () => {
|
||||
chartInstance.destroy();
|
||||
}
|
||||
|
||||
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import number from '@/filters/number.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
||||
@@ -22,7 +22,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const highlighter = await getHighlighter();
|
||||
const darkMode = store.reactiveState.darkMode;
|
||||
const darkMode = store.r.darkMode;
|
||||
const codeLang = ref<BundledLanguage | 'aiscript'>('js');
|
||||
|
||||
const [lightThemeName, darkThemeName] = await Promise.all([
|
||||
@@ -74,10 +74,8 @@ watch(() => props.lang, (to) => {
|
||||
<style module lang="scss">
|
||||
.codeBlockRoot :global(.shiki) {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--MI_THEME-divider);
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
|
||||
color: var(--shiki-fallback);
|
||||
|
||||
@@ -38,7 +38,7 @@ import tinycolor from 'tinycolor2';
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
@@ -44,7 +44,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import * as os from '@/os.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 { deviceKind } from '@/utility/device-kind.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
@@ -245,7 +245,7 @@ function deleteFolder() {
|
||||
folderId: props.folder.id,
|
||||
}).then(() => {
|
||||
if (prefer.s.uploadFolder === props.folder.id) {
|
||||
prefer.set('uploadFolder', null);
|
||||
prefer.commit('uploadFolder', null);
|
||||
}
|
||||
}).catch(err => {
|
||||
switch (err.id) {
|
||||
@@ -266,7 +266,7 @@ function deleteFolder() {
|
||||
}
|
||||
|
||||
function setAsUploadFolder() {
|
||||
prefer.set('uploadFolder', props.folder.id);
|
||||
prefer.commit('uploadFolder', props.folder.id);
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
|
||||
@@ -136,7 +136,7 @@ import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { store } from '@/store.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 { prefer } from '@/preferences.js';
|
||||
|
||||
@@ -166,7 +166,7 @@ const {
|
||||
emojiPickerHeight,
|
||||
} = prefer.r;
|
||||
|
||||
const recentlyUsedEmojis = store.reactiveState.recentlyUsedEmojis;
|
||||
const recentlyUsedEmojis = store.r.recentlyUsedEmojis;
|
||||
|
||||
const recentlyUsedEmojisDef = computed(() => {
|
||||
return recentlyUsedEmojis.value.map(getDef);
|
||||
@@ -319,7 +319,7 @@ watch(q, () => {
|
||||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
|
||||
for (const emoji of emojis) {
|
||||
if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
|
||||
matches.add(emoji);
|
||||
@@ -336,7 +336,7 @@ watch(q, () => {
|
||||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
|
||||
for (const emoji of emojis) {
|
||||
if (index[emoji.char].some(k => k.startsWith(newQ))) {
|
||||
matches.add(emoji);
|
||||
@@ -353,7 +353,7 @@ watch(q, () => {
|
||||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
|
||||
for (const emoji of emojis) {
|
||||
if (index[emoji.char].some(k => k.includes(newQ))) {
|
||||
matches.add(emoji);
|
||||
@@ -429,7 +429,7 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef,
|
||||
|
||||
// 最近使った絵文字更新
|
||||
if (!pinned.value?.includes(key)) {
|
||||
let recents = store.state.recentlyUsedEmojis;
|
||||
let recents = store.s.recentlyUsedEmojis;
|
||||
recents = recents.filter((emoji) => emoji !== key);
|
||||
recents.unshift(key);
|
||||
store.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
|
||||
43
packages/frontend/src/components/MkFeatureBanner.vue
Normal 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>
|
||||
@@ -44,8 +44,7 @@ import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { pleaseLogin } from '@/utility/please-login.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { store } from '@/store.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -121,11 +120,11 @@ async function onClick() {
|
||||
} else {
|
||||
await misskeyApi('following/create', {
|
||||
userId: props.user.id,
|
||||
withReplies: store.state.defaultWithReplies,
|
||||
withReplies: prefer.s.defaultFollowWithReplies,
|
||||
});
|
||||
emit('update:user', {
|
||||
...props.user,
|
||||
withReplies: store.state.defaultWithReplies,
|
||||
withReplies: prefer.s.defaultFollowWithReplies,
|
||||
});
|
||||
hasPendingFollowRequestFromYou.value = true;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Chart } from 'chart.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.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 { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
@@ -106,7 +106,7 @@ async function renderChart() {
|
||||
|
||||
await nextTick();
|
||||
|
||||
const color = store.state.darkMode ? '#b4e900' : '#86b300';
|
||||
const color = store.s.darkMode ? '#b4e900' : '#86b300';
|
||||
|
||||
// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
|
||||
const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
|
||||
|
||||
@@ -88,8 +88,8 @@ import { onMounted, ref, computed, shallowRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
|
||||
import { $i } from '@/i.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import { instance } from '@/instance.js';
|
||||
|
||||
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { url as local } from '@@/js/config.js';
|
||||
import { useTooltip } from '@/utility/use-tooltip.js';
|
||||
import { useTooltip } from '@/use/use-tooltip.js';
|
||||
import * as os from '@/os.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import type { MkABehavior } from '@/components/global/MkA.vue';
|
||||
|
||||
@@ -98,7 +98,7 @@ import * as os from '@/os.js';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import { hms } from '@/filters/hms.js';
|
||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { $i, iAmModerator } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -60,7 +60,7 @@ import bytes from '@/filters/bytes.js';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { $i, iAmModerator } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
||||
@@ -121,7 +121,7 @@ import * as os from '@/os.js';
|
||||
import { exitFullscreen, requestFullscreen } from '@/utility/fullscreen.js';
|
||||
import hasAudio from '@/utility/media-has-audio.js';
|
||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { $i, iAmModerator } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -18,7 +18,7 @@ import { toUnicode } from 'punycode.js';
|
||||
import { computed } from 'vue';
|
||||
import { host as localHost } from '@@/js/config.js';
|
||||
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 { prefer } from '@/preferences.js';
|
||||
|
||||
|
||||
@@ -177,12 +177,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts">
|
||||
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 { Keymap } from '@/utility/hotkey.js';
|
||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { isTouchUsing } from '@/utility/touch.js';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
import { isFocusable } from '@/utility/focus.js';
|
||||
import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js';
|
||||
|
||||
@@ -558,11 +558,11 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
&.danger {
|
||||
--menuFg: #ff2a2a;
|
||||
--menuFg: var(--MI_THEME-error);
|
||||
--menuHoverFg: #fff;
|
||||
--menuHoverBg: #ff4242;
|
||||
--menuHoverBg: var(--MI_THEME-error);
|
||||
--menuActiveFg: #fff;
|
||||
--menuActiveBg: #d42e2e;
|
||||
--menuActiveBg: hsl(from var(--MI_THEME-error) h s calc(l - 10));
|
||||
}
|
||||
|
||||
&.radio {
|
||||
|
||||
@@ -208,12 +208,12 @@ import * as sound from '@/utility/sound.js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import { reactionPicker } from '@/utility/reaction-picker.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 { 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 { useTooltip } from '@/utility/use-tooltip.js';
|
||||
import { useTooltip } from '@/use/use-tooltip.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { getNoteSummary } from '@/utility/get-note-summary.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
|
||||
@@ -238,12 +238,12 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { reactionPicker } from '@/utility/reaction-picker.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 { 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 { useTooltip } from '@/utility/use-tooltip.js';
|
||||
import { useTooltip } from '@/use/use-tooltip.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/utility/show-moved-dialog.js';
|
||||
|
||||
@@ -48,7 +48,7 @@ import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ import { notePage } from '@/filters/note.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { signinRequired } from '@/i.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
@@ -32,19 +32,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import type { PageMetadata } from '@/utility/page-metadata.js';
|
||||
import type { PageMetadata } from '@/page.js';
|
||||
import RouterView from '@/components/global/RouterView.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { popout as _popout } from '@/utility/popout.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
|
||||
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
|
||||
import { openingWindowsCount } from '@/os.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { useRouterFactory } from '@/router/supplier.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { analytics } from '@/analytics.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initialPath: string;
|
||||
@@ -119,7 +120,7 @@ windowRouter.addListener('change', ctx => {
|
||||
|
||||
windowRouter.init();
|
||||
|
||||
provide('router', windowRouter);
|
||||
provide(DI.router, windowRouter);
|
||||
provide('inAppSearchMarkerId', searchMarkerId);
|
||||
provideMetadataReceiver((metadataGetter) => {
|
||||
const info = metadataGetter();
|
||||
|
||||
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<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>
|
||||
|
||||
@@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { signinRequired } from '@/i.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
|
||||
@@ -127,7 +127,8 @@ import { store } from '@/store.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { i18n } from '@/i18n.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 { deepClone } from '@/utility/clone.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
@@ -176,18 +177,18 @@ const text = ref(props.initialText ?? '');
|
||||
const files = ref(props.initialFiles ?? []);
|
||||
const poll = ref<PollEditorModelValue | null>(null);
|
||||
const useCw = ref<boolean>(!!props.initialCw);
|
||||
const showPreview = ref(store.state.showPreview);
|
||||
const showPreview = ref(store.s.showPreview);
|
||||
watch(showPreview, () => store.set('showPreview', showPreview.value));
|
||||
const showAddMfmFunction = ref(prefer.s.enableQuickAddMfmFunction);
|
||||
watch(showAddMfmFunction, () => prefer.set('enableQuickAddMfmFunction', showAddMfmFunction.value));
|
||||
watch(showAddMfmFunction, () => prefer.commit('enableQuickAddMfmFunction', showAddMfmFunction.value));
|
||||
const cw = ref<string | null>(props.initialCw ?? null);
|
||||
const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly));
|
||||
const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility));
|
||||
const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.s.localOnly : prefer.s.defaultNoteLocalOnly));
|
||||
const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.s.visibility : prefer.s.defaultNoteVisibility));
|
||||
const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]);
|
||||
if (props.initialVisibleUsers) {
|
||||
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
|
||||
}
|
||||
const reactionAcceptance = ref(store.state.reactionAcceptance);
|
||||
const reactionAcceptance = ref(store.s.reactionAcceptance);
|
||||
const draghover = ref(false);
|
||||
const quoteId = ref<string | null>(null);
|
||||
const hasNotSpecifiedMentions = ref(false);
|
||||
@@ -265,7 +266,13 @@ const canPost = computed((): boolean => {
|
||||
quoteId.value != null
|
||||
) &&
|
||||
(textLength.value <= maxTextLength.value) &&
|
||||
(cwTextLength.value <= maxCwTextLength) &&
|
||||
(
|
||||
useCw.value ?
|
||||
(
|
||||
cw.value != null && cw.value.trim() !== '' &&
|
||||
cwTextLength.value <= maxCwTextLength
|
||||
) : true
|
||||
) &&
|
||||
(files.value.length <= 16) &&
|
||||
(!poll.value || poll.value.choices.length >= 2);
|
||||
});
|
||||
@@ -744,14 +751,6 @@ function isAnnoying(text: string): boolean {
|
||||
}
|
||||
|
||||
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) {
|
||||
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
|
||||
|
||||
@@ -839,7 +838,7 @@ async function post(ev?: MouseEvent) {
|
||||
|
||||
if (postAccount.value) {
|
||||
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;
|
||||
|
||||
@@ -4,14 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.root" @contextmenu.prevent.stop="showMenu($event, true)">
|
||||
<div :class="$style.body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
@@ -21,24 +22,32 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { ref } from 'vue';
|
||||
import type { PREF_DEF } from '@/preferences/def.js';
|
||||
import * as os from '@/os.js';
|
||||
import { profileManager } from '@/preferences.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
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(() => {
|
||||
isAccountOverrided.value = profileManager.isAccountOverrided(props.k);
|
||||
isAccountOverrided.value = prefer.isAccountOverrided(props.k);
|
||||
isSyncEnabled.value = prefer.isSyncEnabled(props.k);
|
||||
}, 100);
|
||||
os.popupMenu(profileManager.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
|
||||
onClosing: () => {
|
||||
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: () => {
|
||||
window.clearInterval(i);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -48,7 +57,7 @@ function showMenu(ev: MouseEvent) {
|
||||
display: flex;
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
|
||||
@@ -43,7 +43,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkRadio from '@/components/MkRadio.vue';
|
||||
import * as os from '@/os.js';
|
||||
import * as config from '@@/js/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
const text = ref('');
|
||||
const flag = ref(true);
|
||||
|
||||
@@ -42,12 +42,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { $i, getAccounts } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { apiWithDialog, promiseDialog } from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { getAccounts } from '@/accounts.js';
|
||||
|
||||
defineProps<{
|
||||
primary?: boolean;
|
||||
|
||||
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
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';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -26,8 +26,8 @@ import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import { useTooltip } from '@/utility/use-tooltip.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { useTooltip } from '@/use/use-tooltip.js';
|
||||
import { $i } from '@/i.js';
|
||||
import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
@@ -17,7 +17,7 @@ import { onMounted, nextTick, shallowRef, ref } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.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 { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
@@ -75,7 +75,7 @@ async function renderChart() {
|
||||
|
||||
await nextTick();
|
||||
|
||||
const color = store.state.darkMode ? '#b4e900' : '#86b300';
|
||||
const color = store.s.darkMode ? '#b4e900' : '#86b300';
|
||||
|
||||
const getYYYYMMDD = (date: Date) => {
|
||||
const y = date.getFullYear().toString().padStart(2, '0');
|
||||
|
||||
@@ -12,7 +12,7 @@ import { onMounted, shallowRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
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 { alpha } from '@/utility/color.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
@@ -42,7 +42,7 @@ const getDate = (ymd: string) => {
|
||||
onMounted(async () => {
|
||||
let raw = await misskeyApi('retention', { });
|
||||
|
||||
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
|
||||
const color = accent.toHex();
|
||||
|
||||
@@ -67,20 +67,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } 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 { PwResponse } from '@/components/MkSignin.password.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
|
||||
import { login } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
import XInput from '@/components/MkSignin.input.vue';
|
||||
import XPassword from '@/components/MkSignin.password.vue';
|
||||
import type { PwResponse } from '@/components/MkSignin.password.vue';
|
||||
import XTotp from '@/components/MkSignin.totp.vue';
|
||||
import XPasskey from '@/components/MkSignin.passkey.vue';
|
||||
import { login } from '@/accounts.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
|
||||
|
||||
@@ -85,13 +85,13 @@ import * as Misskey from 'misskey-js';
|
||||
import * as config from '@@/js/config.js';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkCaptcha from '@/components/MkCaptcha.vue';
|
||||
import type { Captcha } from '@/components/MkCaptcha.vue';
|
||||
import MkCaptcha from '@/components/MkCaptcha.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { login } from '@/account.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { login } from '@/accounts.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
|
||||
96
packages/frontend/src/components/MkThemePreview.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<svg
|
||||
version="1.1"
|
||||
viewBox="0 0 203.2 152.4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<g fill-rule="evenodd">
|
||||
<rect width="203.2" height="152.4" :fill="themeVariables.bg" stroke-width=".26458" />
|
||||
<rect width="65.498" height="152.4" :fill="themeVariables.panel" stroke-width=".26458" />
|
||||
<rect x="65.498" width="137.7" height="40.892" :fill="themeVariables.acrylicBg" stroke-width=".265" />
|
||||
<path transform="scale(.26458)" d="m439.77 247.19c-43.673 0-78.832 35.157-78.832 78.83v249.98h407.06v-328.81z" :fill="themeVariables.panel" />
|
||||
</g>
|
||||
<circle cx="32.749" cy="83.054" r="21.132" :fill="themeVariables.accentedBg" stroke-dasharray="0.319256, 0.319256" stroke-width=".15963" style="paint-order:stroke fill markers" />
|
||||
<circle cx="136.67" cy="106.76" r="23.876" :fill="themeVariables.fg" fill-opacity="0.5" stroke-dasharray="0.352425, 0.352425" stroke-width=".17621" style="paint-order:stroke fill markers" />
|
||||
<g :fill="themeVariables.fg" fill-rule="evenodd" stroke-width=".26458">
|
||||
<rect x="171.27" y="87.815" width="48.576" height="6.8747" ry="3.4373"/>
|
||||
<rect x="171.27" y="105.09" width="48.576" height="6.875" ry="3.4375"/>
|
||||
<rect x="171.27" y="121.28" width="48.576" height="6.875" ry="3.4375"/>
|
||||
<rect x="171.27" y="137.47" width="48.576" height="6.875" ry="3.4375"/>
|
||||
</g>
|
||||
<path d="m65.498 40.892h137.7" :stroke="themeVariables.divider" stroke-width="0.75" />
|
||||
<g transform="matrix(.60823 0 0 .60823 25.45 75.755)" fill="none" :stroke="themeVariables.accent" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="m0 0h24v24h-24z" fill="none" stroke="none" />
|
||||
<path d="m5 12h-2l9-9 9 9h-2" />
|
||||
<path d="m5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7" />
|
||||
<path d="m9 21v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6" />
|
||||
</g>
|
||||
<g transform="matrix(.61621 0 0 .61621 25.354 117.92)" fill="none" :stroke="themeVariables.fg" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="m0 0h24v24h-24z" fill="none" stroke="none" />
|
||||
<path d="m10 5a2 2 0 1 1 4 0 7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6" />
|
||||
<path d="m9 17v1a3 3 0 0 0 6 0v-1" />
|
||||
</g>
|
||||
<image x="20.948" y="18.388" width="23.602" height="23.602" image-rendering="optimizeSpeed" preserveAspectRatio="xMidYMid meet" v-bind="{ 'xlink:href': instance.iconUrl || '/favicon.ico' }" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { compile } from '@/theme.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
|
||||
const props = defineProps<{
|
||||
theme: Theme;
|
||||
}>();
|
||||
|
||||
const themeVariables = ref<{
|
||||
bg: string;
|
||||
acrylicBg: string;
|
||||
panel: string;
|
||||
fg: string;
|
||||
divider: string;
|
||||
accent: string;
|
||||
accentedBg: string;
|
||||
}>({
|
||||
bg: 'var(--MI_THEME-bg)',
|
||||
acrylicBg: 'var(--MI_THEME-acrylicBg)',
|
||||
panel: 'var(--MI_THEME-panel)',
|
||||
fg: 'var(--MI_THEME-fg)',
|
||||
divider: 'var(--MI_THEME-divider)',
|
||||
accent: 'var(--MI_THEME-accent)',
|
||||
accentedBg: 'var(--MI_THEME-accentedBg)',
|
||||
});
|
||||
|
||||
watch(() => props.theme, (theme) => {
|
||||
if (theme == null) return;
|
||||
|
||||
const _theme = deepClone(theme);
|
||||
|
||||
if (_theme?.base != null) {
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
|
||||
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
|
||||
}
|
||||
|
||||
const compiled = compile(_theme);
|
||||
|
||||
themeVariables.value = {
|
||||
bg: compiled.bg ?? 'var(--MI_THEME-bg)',
|
||||
acrylicBg: compiled.acrylicBg ?? 'var(--MI_THEME-acrylicBg)',
|
||||
panel: compiled.panel ?? 'var(--MI_THEME-panel)',
|
||||
fg: compiled.fg ?? 'var(--MI_THEME-fg)',
|
||||
divider: compiled.divider ?? 'var(--MI_THEME-divider)',
|
||||
accent: compiled.accent ?? 'var(--MI_THEME-accent)',
|
||||
accentedBg: compiled.accentedBg ?? 'var(--MI_THEME-accentedBg)',
|
||||
};
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
@@ -25,7 +25,7 @@ import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ import MkButton from './MkButton.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { iAmAdmin } from '@/account.js';
|
||||
import { iAmAdmin } from '@/i.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string | null;
|
||||
@@ -79,8 +79,8 @@ const adminPermissions = Misskey.permissions.filter(p => p.startsWith('read:admi
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const name = ref(props.initialName);
|
||||
const permissionSwitches = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
|
||||
const permissionSwitchesForAdmin = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
|
||||
const permissionSwitches = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
|
||||
const permissionSwitchesForAdmin = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
|
||||
|
||||
if (props.initialPermissions) {
|
||||
for (const kind of props.initialPermissions) {
|
||||
|
||||
@@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -31,7 +31,7 @@ import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'succeeded'): void;
|
||||
|
||||
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
|
||||
scrolling="no"
|
||||
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
|
||||
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"
|
||||
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${store.s.darkMode ? 'dark' : 'light'}&id=${tweetId}`"
|
||||
></iframe>
|
||||
</div>
|
||||
<div :class="$style.action">
|
||||
|
||||
@@ -39,7 +39,7 @@ import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import number from '@/filters/number.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
|
||||
import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
@@ -65,7 +65,7 @@ import { getUserMenu } from '@/utility/get-user-menu.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
|
||||
import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { store } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { instance } from '@/instance.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -128,7 +128,7 @@ async function ok() {
|
||||
dialogEl.value?.close();
|
||||
|
||||
// 最近使ったユーザー更新
|
||||
let recents = store.state.recentlyUsedUsers;
|
||||
let recents = store.s.recentlyUsedUsers;
|
||||
recents = recents.filter(x => x !== selected.value?.id);
|
||||
recents.unshift(selected.value.id);
|
||||
store.set('recentlyUsedUsers', recents.splice(0, 16));
|
||||
@@ -141,7 +141,7 @@ function cancel() {
|
||||
|
||||
onMounted(() => {
|
||||
misskeyApi('users/show', {
|
||||
userIds: store.state.recentlyUsedUsers,
|
||||
userIds: store.s.recentlyUsedUsers,
|
||||
}).then(foundUsers => {
|
||||
let _users = foundUsers;
|
||||
_users = _users.filter((u) => {
|
||||
|
||||
@@ -39,7 +39,7 @@ import FormSlot from '@/components/form/slot.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { chooseFileFromPc } from '@/utility/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { signinRequired } from '@/i.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ const emit = defineEmits<{
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const page = ref(store.state.accountSetupWizard);
|
||||
const page = ref(store.s.accountSetupWizard);
|
||||
|
||||
watch(page, () => {
|
||||
store.set('accountSetupWizard', page.value);
|
||||
|
||||
@@ -19,7 +19,7 @@ import gradient from 'chartjs-plugin-gradient';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { misskeyApi } from '@/utility/misskey-api.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 { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
@@ -59,7 +59,7 @@ async function renderChart() {
|
||||
|
||||
await nextTick();
|
||||
|
||||
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
|
||||
|
||||
@@ -47,7 +47,7 @@ import { instance } from '@/instance.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { store } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
type Ad = (typeof instance)['ads'][number];
|
||||
@@ -67,7 +67,7 @@ const choseAd = (): Ad | null => {
|
||||
return props.specify;
|
||||
}
|
||||
|
||||
const allAds = instance.ads.map(ad => store.state.mutedAds.includes(ad.id) ? {
|
||||
const allAds = instance.ads.map(ad => store.s.mutedAds.includes(ad.id) ? {
|
||||
...ad,
|
||||
ratio: 0,
|
||||
} : ad);
|
||||
@@ -112,7 +112,7 @@ const shouldHide = ref(!prefer.s.forceShowAds && $i && $i.policies.canHideAds &&
|
||||
|
||||
function reduceFrequency(): void {
|
||||
if (chosen.value == null) return;
|
||||
if (store.state.mutedAds.includes(chosen.value.id)) return;
|
||||
if (store.s.mutedAds.includes(chosen.value.id)) return;
|
||||
store.push('mutedAds', chosen.value.id);
|
||||
os.success();
|
||||
chosen.value = choseAd();
|
||||
|
||||
@@ -35,7 +35,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.spacer, store.reactiveState.darkMode.value ? $style.dark : $style.light]"></div>
|
||||
<div :class="[$style.spacer, store.r.darkMode.value ? $style.dark : $style.light]"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -43,14 +43,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { scrollToTop } from '@@/js/scroll.js';
|
||||
import XTabs from './MkPageHeader.tabs.vue';
|
||||
import type { Tab } from './MkPageHeader.tabs.vue';
|
||||
import { scrollToTop } from '@@/js/scroll.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { injectReactiveMetadata } from '@/utility/page-metadata.js';
|
||||
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
|
||||
import type { PageHeaderItem } from '@/types/page-header.js';
|
||||
import type { PageMetadata } from '@/utility/page-metadata.js';
|
||||
import type { PageMetadata } from '@/page.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { injectReactiveMetadata } from '@/page.js';
|
||||
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
overridePageMetadata?: PageMetadata;
|
||||
@@ -114,7 +115,7 @@ let ro: ResizeObserver | null;
|
||||
|
||||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
globalEvents.on('themeChanging', calcBg);
|
||||
|
||||
if (el.value && el.value.parentElement) {
|
||||
narrow.value = el.value.parentElement.offsetWidth < 500;
|
||||
@@ -128,7 +129,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
globalEvents.off('themeChanged', calcBg);
|
||||
globalEvents.off('themeChanging', calcBg);
|
||||
if (ro) ro.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@ import { defineAsyncComponent, ref } from 'vue';
|
||||
import { toUnicode as decodePunycode } from 'punycode.js';
|
||||
import { url as local } from '@@/js/config.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 type { MkABehavior } from '@/components/global/MkA.vue';
|
||||
|
||||
|
||||
@@ -24,20 +24,21 @@ import type { IRouter, Resolved, RouteDef } from '@/nirax.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import MkLoadingPage from '@/pages/_loading_.vue';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
const props = defineProps<{
|
||||
router?: IRouter;
|
||||
nested?: boolean;
|
||||
}>();
|
||||
|
||||
const router = props.router ?? inject('router');
|
||||
const router = props.router ?? inject(DI.router);
|
||||
|
||||
if (router == null) {
|
||||
throw new Error('no router provided');
|
||||
}
|
||||
|
||||
const currentDepth = inject('routerCurrentDepth', 0);
|
||||
provide('routerCurrentDepth', currentDepth + 1);
|
||||
const currentDepth = inject(DI.routerCurrentDepth, 0);
|
||||
provide(DI.routerCurrentDepth, currentDepth + 1);
|
||||
|
||||
function resolveNested(current: Resolved, d = 0): Resolved | null {
|
||||
if (!props.nested) return current;
|
||||
|
||||
@@ -90,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue';
|
||||
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 { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
|
||||
import type { Size } from '@/components/grid/grid.js';
|
||||
|
||||
@@ -3,13 +3,23 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { throttle } from 'throttle-debounce';
|
||||
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 { 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 { 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 = {
|
||||
name: string;
|
||||
@@ -53,127 +63,132 @@ export type Column = {
|
||||
soundSetting?: SoundStore;
|
||||
};
|
||||
|
||||
export const loadDeck = async () => {
|
||||
let deck;
|
||||
const _currentProfile = prefer.s['deck.profiles'].find(p => p.name === prefer.s['deck.profile']);
|
||||
const __currentProfile = _currentProfile ? deepClone(_currentProfile) : null;
|
||||
export const columns = ref(__currentProfile ? __currentProfile.columns : []);
|
||||
export const layout = ref(__currentProfile ? __currentProfile.layout : []);
|
||||
|
||||
try {
|
||||
deck = await misskeyApi('i/registry/get', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: store.state['deck.profile'],
|
||||
});
|
||||
} catch (err) {
|
||||
if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
|
||||
// 後方互換性のため
|
||||
if (store.state['deck.profile'] === 'default') {
|
||||
saveDeck();
|
||||
return;
|
||||
}
|
||||
if (prefer.s['deck.profile'] == null) {
|
||||
addProfile('Main');
|
||||
}
|
||||
|
||||
store.set('deck.columns', []);
|
||||
store.set('deck.layout', []);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
export function forceSaveCurrentDeckProfile() {
|
||||
const currentProfile = prefer.s['deck.profiles'].find(p => p.name === prefer.s['deck.profile']);
|
||||
if (currentProfile == null) return;
|
||||
|
||||
store.set('deck.columns', deck.columns);
|
||||
store.set('deck.layout', deck.layout);
|
||||
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);
|
||||
}
|
||||
|
||||
export const saveCurrentDeckProfile = () => {
|
||||
forceSaveCurrentDeckProfile();
|
||||
};
|
||||
|
||||
export async function forceSaveDeck() {
|
||||
await misskeyApi('i/registry/set', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: store.state['deck.profile'],
|
||||
value: {
|
||||
columns: store.reactiveState['deck.columns'].value,
|
||||
layout: store.reactiveState['deck.layout'].value,
|
||||
},
|
||||
});
|
||||
function switchProfile(profile: DeckProfile) {
|
||||
prefer.commit('deck.profile', profile.name);
|
||||
const currentProfile = deepClone(profile);
|
||||
columns.value = currentProfile.columns;
|
||||
layout.value = currentProfile.layout;
|
||||
forceSaveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
|
||||
export const saveDeck = throttle(1000, () => {
|
||||
forceSaveDeck();
|
||||
});
|
||||
function addProfile(name: string) {
|
||||
if (name.trim() === '') return;
|
||||
if (prefer.s['deck.profiles'].find(p => p.name === name)) return;
|
||||
|
||||
export async function getProfiles(): Promise<string[]> {
|
||||
return await misskeyApi('i/registry/keys', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
});
|
||||
const newProfile: DeckProfile = {
|
||||
id: uuid(),
|
||||
name,
|
||||
columns: [],
|
||||
layout: [],
|
||||
};
|
||||
prefer.commit('deck.profiles', [...prefer.s['deck.profiles'], newProfile]);
|
||||
switchProfile(newProfile);
|
||||
}
|
||||
|
||||
export async function deleteProfile(key: string): Promise<void> {
|
||||
return await misskeyApi('i/registry/remove', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: key,
|
||||
});
|
||||
function createFirstProfile() {
|
||||
addProfile('Main');
|
||||
}
|
||||
|
||||
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) {
|
||||
if (column.name === undefined) column.name = null;
|
||||
store.push('deck.columns', column);
|
||||
store.push('deck.layout', [column.id]);
|
||||
saveDeck();
|
||||
columns.value.push(column);
|
||||
layout.value.push([column.id]);
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function removeColumn(id: Column['id']) {
|
||||
store.set('deck.columns', store.state['deck.columns'].filter(c => c.id !== id));
|
||||
store.set('deck.layout', store.state['deck.layout']
|
||||
.map(ids => ids.filter(_id => _id !== id))
|
||||
.filter(ids => ids.length > 0));
|
||||
saveDeck();
|
||||
columns.value = columns.value.filter(c => c.id !== id);
|
||||
layout.value = layout.value.map(ids => ids.filter(_id => _id !== id)).filter(ids => ids.length > 0);
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function swapColumn(a: Column['id'], b: Column['id']) {
|
||||
const aX = store.state['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1);
|
||||
const aY = store.state['deck.layout'][aX].findIndex(id => id === a);
|
||||
const bX = store.state['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1);
|
||||
const bY = store.state['deck.layout'][bX].findIndex(id => id === b);
|
||||
const layout = deepClone(store.state['deck.layout']);
|
||||
layout[aX][aY] = b;
|
||||
layout[bX][bY] = a;
|
||||
store.set('deck.layout', layout);
|
||||
saveDeck();
|
||||
const aX = layout.value.findIndex(ids => ids.indexOf(a) !== -1);
|
||||
const aY = layout.value[aX].findIndex(id => id === a);
|
||||
const bX = layout.value.findIndex(ids => ids.indexOf(b) !== -1);
|
||||
const bY = layout.value[bX].findIndex(id => id === b);
|
||||
const newLayout = deepClone(layout.value);
|
||||
newLayout[aX][aY] = b;
|
||||
newLayout[bX][bY] = a;
|
||||
layout.value = newLayout;
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function swapLeftColumn(id: Column['id']) {
|
||||
const layout = deepClone(store.state['deck.layout']);
|
||||
store.state['deck.layout'].some((ids, i) => {
|
||||
const newLayout = deepClone(layout.value);
|
||||
layout.value.some((ids, i) => {
|
||||
if (ids.includes(id)) {
|
||||
const left = store.state['deck.layout'][i - 1];
|
||||
const left = layout.value[i - 1];
|
||||
if (left) {
|
||||
layout[i - 1] = store.state['deck.layout'][i];
|
||||
layout[i] = left;
|
||||
store.set('deck.layout', layout);
|
||||
newLayout[i - 1] = layout.value[i];
|
||||
newLayout[i] = left;
|
||||
layout.value = newLayout;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
saveDeck();
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function swapRightColumn(id: Column['id']) {
|
||||
const layout = deepClone(store.state['deck.layout']);
|
||||
store.state['deck.layout'].some((ids, i) => {
|
||||
const newLayout = deepClone(layout.value);
|
||||
layout.value.some((ids, i) => {
|
||||
if (ids.includes(id)) {
|
||||
const right = store.state['deck.layout'][i + 1];
|
||||
const right = layout.value[i + 1];
|
||||
if (right) {
|
||||
layout[i + 1] = store.state['deck.layout'][i];
|
||||
layout[i] = right;
|
||||
store.set('deck.layout', layout);
|
||||
newLayout[i + 1] = layout.value[i];
|
||||
newLayout[i] = right;
|
||||
layout.value = newLayout;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
saveDeck();
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function swapUpColumn(id: Column['id']) {
|
||||
const layout = deepClone(store.state['deck.layout']);
|
||||
const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id));
|
||||
const ids = deepClone(store.state['deck.layout'][idsIndex]);
|
||||
const newLayout = deepClone(layout.value);
|
||||
const idsIndex = layout.value.findIndex(ids => ids.includes(id));
|
||||
const ids = deepClone(layout.value[idsIndex]);
|
||||
ids.some((x, i) => {
|
||||
if (x === id) {
|
||||
const up = ids[i - 1];
|
||||
@@ -181,20 +196,20 @@ export function swapUpColumn(id: Column['id']) {
|
||||
ids[i - 1] = id;
|
||||
ids[i] = up;
|
||||
|
||||
layout[idsIndex] = ids;
|
||||
store.set('deck.layout', layout);
|
||||
newLayout[idsIndex] = ids;
|
||||
layout.value = newLayout;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
saveDeck();
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function swapDownColumn(id: Column['id']) {
|
||||
const layout = deepClone(store.state['deck.layout']);
|
||||
const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id));
|
||||
const ids = deepClone(store.state['deck.layout'][idsIndex]);
|
||||
const newLayout = deepClone(layout.value);
|
||||
const idsIndex = layout.value.findIndex(ids => ids.includes(id));
|
||||
const ids = deepClone(layout.value[idsIndex]);
|
||||
ids.some((x, i) => {
|
||||
if (x === id) {
|
||||
const down = ids[i + 1];
|
||||
@@ -202,105 +217,137 @@ export function swapDownColumn(id: Column['id']) {
|
||||
ids[i + 1] = id;
|
||||
ids[i] = down;
|
||||
|
||||
layout[idsIndex] = ids;
|
||||
store.set('deck.layout', layout);
|
||||
newLayout[idsIndex] = ids;
|
||||
layout.value = newLayout;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
saveDeck();
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function stackLeftColumn(id: Column['id']) {
|
||||
let layout = deepClone(store.state['deck.layout']);
|
||||
const i = store.state['deck.layout'].findIndex(ids => ids.includes(id));
|
||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||
layout[i - 1].push(id);
|
||||
layout = layout.filter(ids => ids.length > 0);
|
||||
store.set('deck.layout', layout);
|
||||
saveDeck();
|
||||
let newLayout = deepClone(layout.value);
|
||||
const i = layout.value.findIndex(ids => ids.includes(id));
|
||||
newLayout = newLayout.map(ids => ids.filter(_id => _id !== id));
|
||||
newLayout[i - 1].push(id);
|
||||
newLayout = newLayout.filter(ids => ids.length > 0);
|
||||
layout.value = newLayout;
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function popRightColumn(id: Column['id']) {
|
||||
let layout = deepClone(store.state['deck.layout']);
|
||||
const i = store.state['deck.layout'].findIndex(ids => ids.includes(id));
|
||||
const affected = layout[i];
|
||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||
layout.splice(i + 1, 0, [id]);
|
||||
layout = layout.filter(ids => ids.length > 0);
|
||||
store.set('deck.layout', layout);
|
||||
let newLayout = deepClone(layout.value);
|
||||
const i = layout.value.findIndex(ids => ids.includes(id));
|
||||
const affected = newLayout[i];
|
||||
newLayout = newLayout.map(ids => ids.filter(_id => _id !== id));
|
||||
newLayout.splice(i + 1, 0, [id]);
|
||||
newLayout = newLayout.filter(ids => ids.length > 0);
|
||||
layout.value = newLayout;
|
||||
|
||||
const columns = deepClone(store.state['deck.columns']);
|
||||
for (const column of columns) {
|
||||
const newColumns = deepClone(columns.value);
|
||||
for (const column of newColumns) {
|
||||
if (affected.includes(column.id)) {
|
||||
column.active = true;
|
||||
}
|
||||
}
|
||||
store.set('deck.columns', columns);
|
||||
columns.value = newColumns;
|
||||
|
||||
saveDeck();
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
const columns = deepClone(store.state['deck.columns']);
|
||||
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
|
||||
const column = deepClone(store.state['deck.columns'][columnIndex]);
|
||||
const newColumns = deepClone(columns.value);
|
||||
const columnIndex = columns.value.findIndex(c => c.id === id);
|
||||
const column = deepClone(columns.value[columnIndex]);
|
||||
if (column == null) return;
|
||||
if (column.widgets == null) column.widgets = [];
|
||||
column.widgets.unshift(widget);
|
||||
columns[columnIndex] = column;
|
||||
store.set('deck.columns', columns);
|
||||
saveDeck();
|
||||
newColumns[columnIndex] = column;
|
||||
columns.value = newColumns;
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
const columns = deepClone(store.state['deck.columns']);
|
||||
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
|
||||
const column = deepClone(store.state['deck.columns'][columnIndex]);
|
||||
const newColumns = deepClone(columns.value);
|
||||
const columnIndex = columns.value.findIndex(c => c.id === id);
|
||||
const column = deepClone(columns.value[columnIndex]);
|
||||
if (column == null) return;
|
||||
if (column.widgets == null) column.widgets = [];
|
||||
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
||||
columns[columnIndex] = column;
|
||||
store.set('deck.columns', columns);
|
||||
saveDeck();
|
||||
newColumns[columnIndex] = column;
|
||||
columns.value = newColumns;
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
||||
const columns = deepClone(store.state['deck.columns']);
|
||||
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
|
||||
const column = deepClone(store.state['deck.columns'][columnIndex]);
|
||||
const newColumns = deepClone(columns.value);
|
||||
const columnIndex = columns.value.findIndex(c => c.id === id);
|
||||
const column = deepClone(columns.value[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = widgets;
|
||||
columns[columnIndex] = column;
|
||||
store.set('deck.columns', columns);
|
||||
saveDeck();
|
||||
newColumns[columnIndex] = column;
|
||||
columns.value = newColumns;
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
|
||||
const columns = deepClone(store.state['deck.columns']);
|
||||
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
|
||||
const column = deepClone(store.state['deck.columns'][columnIndex]);
|
||||
const newColumns = deepClone(columns.value);
|
||||
const columnIndex = columns.value.findIndex(c => c.id === id);
|
||||
const column = deepClone(columns.value[columnIndex]);
|
||||
if (column == null) return;
|
||||
if (column.widgets == null) column.widgets = [];
|
||||
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
||||
...w,
|
||||
data: widgetData,
|
||||
} : w);
|
||||
columns[columnIndex] = column;
|
||||
store.set('deck.columns', columns);
|
||||
saveDeck();
|
||||
newColumns[columnIndex] = column;
|
||||
columns.value = newColumns;
|
||||
saveCurrentDeckProfile();
|
||||
}
|
||||
|
||||
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
||||
const columns = deepClone(store.state['deck.columns']);
|
||||
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
|
||||
const currentColumn = deepClone(store.state['deck.columns'][columnIndex]);
|
||||
const newColumns = deepClone(columns.value);
|
||||
const columnIndex = columns.value.findIndex(c => c.id === id);
|
||||
const currentColumn = deepClone(columns.value[columnIndex]);
|
||||
if (currentColumn == null) return;
|
||||
for (const [k, v] of Object.entries(column)) {
|
||||
currentColumn[k] = v;
|
||||
}
|
||||
columns[columnIndex] = currentColumn;
|
||||
store.set('deck.columns', columns);
|
||||
saveDeck();
|
||||
newColumns[columnIndex] = currentColumn;
|
||||
columns.value = newColumns;
|
||||
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);
|
||||
}
|
||||
|
||||
12
packages/frontend/src/di.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { InjectionKey, Ref } from 'vue';
|
||||
import type { IRouter } from '@/nirax.js';
|
||||
|
||||
export const DI = {
|
||||
routerCurrentDepth: Symbol() as InjectionKey<number>,
|
||||
router: Symbol() as InjectionKey<IRouter>,
|
||||
};
|
||||
@@ -5,17 +5,32 @@
|
||||
|
||||
import type { Directive } from 'vue';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
|
||||
const handlerMap = new WeakMap<any, any>();
|
||||
|
||||
export default {
|
||||
mounted(src, binding, vn) {
|
||||
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
|
||||
function calc() {
|
||||
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
|
||||
|
||||
const myBg = window.getComputedStyle(src).backgroundColor;
|
||||
const myBg = window.getComputedStyle(src).backgroundColor;
|
||||
|
||||
if (parentBg === myBg) {
|
||||
src.style.borderColor = 'var(--MI_THEME-divider)';
|
||||
} else {
|
||||
src.style.borderColor = myBg;
|
||||
if (parentBg === myBg) {
|
||||
src.style.borderColor = 'var(--MI_THEME-divider)';
|
||||
} else {
|
||||
src.style.borderColor = myBg;
|
||||
}
|
||||
}
|
||||
|
||||
handlerMap.set(src, calc);
|
||||
|
||||
calc();
|
||||
|
||||
globalEvents.on('themeChanged', calc);
|
||||
},
|
||||
|
||||
unmounted(src, binding, vn) {
|
||||
globalEvents.off('themeChanged', handlerMap.get(src));
|
||||
},
|
||||
} as Directive;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { EventEmitter } from 'eventemitter3';
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export const globalEvents = new EventEmitter<{
|
||||
themeChanging: () => void;
|
||||
themeChanged: () => void;
|
||||
clientNotification: (notification: Misskey.entities.Notification) => void;
|
||||
requestClearPageCache: () => void;
|
||||
|
||||
34
packages/frontend/src/i.ts
Normal 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;
|
||||
}
|
||||
@@ -9,7 +9,6 @@ export type Keys = (
|
||||
'instance' |
|
||||
'instanceCachedAt' |
|
||||
'account' |
|
||||
'accounts' |
|
||||
'latestDonationInfoShownAt' |
|
||||
'neverShowDonationInfo' |
|
||||
'neverShowLocalOnlyInfo' |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { computed, reactive } from 'vue';
|
||||
import { clearCache } from './utility/clear-cache.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
|
||||
import { lookup } from '@/utility/lookup.js';
|
||||
|
||||