Merge remote-branch 'misskey/develop'
This commit is contained in:
commit
0a9071f1e2
2
.github/workflows/ok-to-test.yml
vendored
2
.github/workflows/ok-to-test.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
# See app.yml for an example app manifest
|
# See app.yml for an example app manifest
|
||||||
- name: Generate token
|
- name: Generate token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.DEPLOYBOT_APP_ID }}
|
app_id: ${{ secrets.DEPLOYBOT_APP_ID }}
|
||||||
private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }}
|
private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }}
|
||||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -22,24 +22,32 @@
|
||||||
- お知らせのアイコンを設定可能に
|
- お知らせのアイコンを設定可能に
|
||||||
- チャンネルをセンシティブ指定できるようになりました
|
- チャンネルをセンシティブ指定できるようになりました
|
||||||
- センシティブチャンネルのNoteのReNoteはデフォルトでHome TLに流れるようになりました
|
- センシティブチャンネルのNoteのReNoteはデフォルトでHome TLに流れるようになりました
|
||||||
|
- センシティブチャンネルのノートはユーザープロフィールに表示されません
|
||||||
- 二要素認証のバックアップコードが生成されるようになりました ref. https://github.com/MisskeyIO/misskey/pull/121
|
- 二要素認証のバックアップコードが生成されるようになりました ref. https://github.com/MisskeyIO/misskey/pull/121
|
||||||
- 二要素認証でパスキーをサポートするようになりました
|
- 二要素認証でパスキーをサポートするようになりました
|
||||||
|
- 通知をテストできるようになりました
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- プロフィールにその人が作ったPlayの一覧出せるように
|
- プロフィールにその人が作ったPlayの一覧出せるように
|
||||||
- メニューのスイッチの動作を改善
|
- メニューのスイッチの動作を改善
|
||||||
- 絵文字ピッカーの検索の表示件数を100件に増加
|
- 絵文字ピッカーの検索の表示件数を100件に増加
|
||||||
- 投稿フォームのプレビューの表示状態を記憶するように
|
- 投稿フォームのプレビューの表示状態を記憶するように
|
||||||
- ノート詳細ページ読み込み時のパフォーマンスを改善
|
|
||||||
- AiScriptからMisskeyサーバーAPIを呼び出す際の制限を撤廃
|
- AiScriptからMisskeyサーバーAPIを呼び出す際の制限を撤廃
|
||||||
|
- Playで直接投稿フォームを埋め込めるように(`Ui:C:postForm`)
|
||||||
- Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように
|
- Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように
|
||||||
- Enhance: 自分が押したリアクションのデザインを改善
|
- Enhance: 自分が押したリアクションのデザインを改善
|
||||||
- Enhance: ノート検索にローカルのみ検索可能なオプションの追加
|
- Enhance: ノート検索にローカルのみ検索可能なオプションの追加
|
||||||
- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように
|
- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように
|
||||||
- Enhance: Renote自体を通報できるように
|
- Enhance: Renote自体を通報できるように
|
||||||
|
- Enhance: データセーバーモードの強化
|
||||||
- Enhance: Renoteを管理者権限で削除可能に
|
- Enhance: Renoteを管理者権限で削除可能に
|
||||||
- `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました
|
- `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました
|
||||||
- Playの操作を行うAPI TokenをAPIコンソールから発行できるように
|
- Playの操作を行うAPI TokenをAPIコンソールから発行できるように
|
||||||
|
- リアクションの表示サイズをより大きくできるように
|
||||||
|
- ノート詳細ページ読み込み時のパフォーマンスを改善
|
||||||
|
- タイムラインでリスト/アンテナ選択時のパフォーマンスを改善
|
||||||
|
- 「Moderation note」、「Add moderation note」をローカライズできるように
|
||||||
|
- 新しい実績を追加
|
||||||
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
|
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
|
||||||
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
|
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
|
||||||
- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正
|
- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正
|
||||||
|
@ -49,13 +57,14 @@
|
||||||
- Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正
|
- Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように
|
|
||||||
- cacheRemoteFilesの初期値はfalseになりました
|
- cacheRemoteFilesの初期値はfalseになりました
|
||||||
- ファイルアップロード時等にファイル名の拡張子を修正する関数(correctFilename)の挙動を改善
|
- ファイルアップロード時等にファイル名の拡張子を修正する関数(correctFilename)の挙動を改善
|
||||||
- Webhookのペイロードにサーバーのurlが含まれるようになりました
|
- Webhookのペイロードにサーバーのurlが含まれるようになりました
|
||||||
- Webhook設定でsecretを空に出来るように
|
- Webhook設定でsecretを空に出来るように
|
||||||
- 使われていないアンテナの自動停止を設定可能に
|
- 使われていないアンテナの自動停止を設定可能に
|
||||||
|
- nodeinfo 2.1対応
|
||||||
- 自分へのメンション一覧を取得する際のパフォーマンスを向上
|
- 自分へのメンション一覧を取得する際のパフォーマンスを向上
|
||||||
|
- Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように
|
||||||
- Fix: 一部のfeatured noteを照会できない問題を修正
|
- Fix: 一部のfeatured noteを照会できない問題を修正
|
||||||
- Fix: muteがapiからのuser list timeline取得で機能しない問題を修正
|
- Fix: muteがapiからのuser list timeline取得で機能しない問題を修正
|
||||||
- Fix: ジョブキュー管理画面の認証を回避できる問題を修正
|
- Fix: ジョブキュー管理画面の認証を回避できる問題を修正
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname } from 'node:path';
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
import * as ts from 'typescript';
|
import ts from 'typescript';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
function createMembers(record) {
|
function createMembers(record) {
|
||||||
return Object.entries(record)
|
return Object.entries(record)
|
||||||
|
|
12
locales/index.d.ts
vendored
12
locales/index.d.ts
vendored
|
@ -462,6 +462,8 @@ export interface Locale {
|
||||||
"totpDescription": string;
|
"totpDescription": string;
|
||||||
"moderator": string;
|
"moderator": string;
|
||||||
"moderation": string;
|
"moderation": string;
|
||||||
|
"moderationNote": string;
|
||||||
|
"addModerationNote": string;
|
||||||
"nUsersMentioned": string;
|
"nUsersMentioned": string;
|
||||||
"securityKeyAndPasskey": string;
|
"securityKeyAndPasskey": string;
|
||||||
"securityKey": string;
|
"securityKey": string;
|
||||||
|
@ -1088,7 +1090,7 @@ export interface Locale {
|
||||||
"enableChartsForRemoteUser": string;
|
"enableChartsForRemoteUser": string;
|
||||||
"enableChartsForFederatedInstances": string;
|
"enableChartsForFederatedInstances": string;
|
||||||
"showClipButtonInNoteFooter": string;
|
"showClipButtonInNoteFooter": string;
|
||||||
"largeNoteReactions": string;
|
"reactionsDisplaySize": string;
|
||||||
"noteIdOrUrl": string;
|
"noteIdOrUrl": string;
|
||||||
"video": string;
|
"video": string;
|
||||||
"videos": string;
|
"videos": string;
|
||||||
|
@ -1622,6 +1624,10 @@ export interface Locale {
|
||||||
"description": string;
|
"description": string;
|
||||||
"flavor": string;
|
"flavor": string;
|
||||||
};
|
};
|
||||||
|
"_smashTestNotificationButton": {
|
||||||
|
"title": string;
|
||||||
|
"description": string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"_role": {
|
"_role": {
|
||||||
|
@ -2389,6 +2395,10 @@ export interface Locale {
|
||||||
"unreadAntennaNote": string;
|
"unreadAntennaNote": string;
|
||||||
"emptyPushNotificationMessage": string;
|
"emptyPushNotificationMessage": string;
|
||||||
"achievementEarned": string;
|
"achievementEarned": string;
|
||||||
|
"testNotification": string;
|
||||||
|
"checkNotificationBehavior": string;
|
||||||
|
"sendTestNotification": string;
|
||||||
|
"notificationWillBeDisplayedLikeThis": string;
|
||||||
"_types": {
|
"_types": {
|
||||||
"all": string;
|
"all": string;
|
||||||
"follow": string;
|
"follow": string;
|
||||||
|
|
|
@ -459,6 +459,8 @@ totp: "認証アプリ"
|
||||||
totpDescription: "認証アプリを使ってワンタイムパスワードを入力"
|
totpDescription: "認証アプリを使ってワンタイムパスワードを入力"
|
||||||
moderator: "モデレーター"
|
moderator: "モデレーター"
|
||||||
moderation: "モデレーション"
|
moderation: "モデレーション"
|
||||||
|
moderationNote: "モデレーションノート"
|
||||||
|
addModerationNote: "モデレーションノートを追加する"
|
||||||
nUsersMentioned: "{n}人が投稿"
|
nUsersMentioned: "{n}人が投稿"
|
||||||
securityKeyAndPasskey: "セキュリティキー・パスキー"
|
securityKeyAndPasskey: "セキュリティキー・パスキー"
|
||||||
securityKey: "セキュリティキー"
|
securityKey: "セキュリティキー"
|
||||||
|
@ -1085,7 +1087,7 @@ retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大するこ
|
||||||
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
||||||
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
||||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||||
largeNoteReactions: "ノートのリアクションを大きく表示"
|
reactionsDisplaySize: "リアクションの表示サイズ"
|
||||||
noteIdOrUrl: "ノートIDまたはURL"
|
noteIdOrUrl: "ノートIDまたはURL"
|
||||||
video: "動画"
|
video: "動画"
|
||||||
videos: "動画"
|
videos: "動画"
|
||||||
|
@ -1546,6 +1548,9 @@ _achievements:
|
||||||
title: "Brain Diver"
|
title: "Brain Diver"
|
||||||
description: "Brain Diverへのリンクを投稿した"
|
description: "Brain Diverへのリンクを投稿した"
|
||||||
flavor: "CherryPick-CherryPick La-Tu-Ma"
|
flavor: "CherryPick-CherryPick La-Tu-Ma"
|
||||||
|
_smashTestNotificationButton:
|
||||||
|
title: "テスト過剰"
|
||||||
|
description: "通知のテストをごく短時間のうちに連続して行った"
|
||||||
|
|
||||||
_role:
|
_role:
|
||||||
new: "ロールの作成"
|
new: "ロールの作成"
|
||||||
|
@ -2302,6 +2307,10 @@ _notification:
|
||||||
unreadAntennaNote: "アンテナ {name}"
|
unreadAntennaNote: "アンテナ {name}"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||||
achievementEarned: "実績を獲得"
|
achievementEarned: "実績を獲得"
|
||||||
|
testNotification: "通知テスト"
|
||||||
|
checkNotificationBehavior: "通知の表示を確かめる"
|
||||||
|
sendTestNotification: "テスト通知を送信する"
|
||||||
|
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
|
||||||
|
|
||||||
_types:
|
_types:
|
||||||
all: "すべて"
|
all: "すべて"
|
||||||
|
|
|
@ -85,6 +85,7 @@ export const ACHIEVEMENT_TYPES = [
|
||||||
'setNameToSyuilo',
|
'setNameToSyuilo',
|
||||||
'cookieClicked',
|
'cookieClicked',
|
||||||
'brainDiver',
|
'brainDiver',
|
||||||
|
'smashTestNotificationButton',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
|
@ -35,6 +35,7 @@ export type MiNotification = {
|
||||||
* groupInvited - グループに招待された
|
* groupInvited - グループに招待された
|
||||||
* achievementEarned - 実績を獲得
|
* achievementEarned - 実績を獲得
|
||||||
* app - アプリ通知
|
* app - アプリ通知
|
||||||
|
* test - テスト通知(サーバー側)
|
||||||
*/
|
*/
|
||||||
type: typeof notificationTypes[number];
|
type: typeof notificationTypes[number];
|
||||||
|
|
||||||
|
|
|
@ -35,18 +35,18 @@ export class NodeinfoServerService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getLinks() {
|
public getLinks() {
|
||||||
return [/* (awaiting release) {
|
return [{
|
||||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
|
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
|
||||||
href: config.url + nodeinfo2_1path
|
href: this.config.url + nodeinfo2_1path,
|
||||||
}, */{
|
}, {
|
||||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||||
href: this.config.url + nodeinfo2_0path,
|
href: this.config.url + nodeinfo2_0path,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||||
const nodeinfo2 = async () => {
|
const nodeinfo2 = async (version: number) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const notesChart = await this.notesChart.getChart('hour', 1, null);
|
const notesChart = await this.notesChart.getChart('hour', 1, null);
|
||||||
|
@ -73,7 +73,8 @@ export class NodeinfoServerService {
|
||||||
|
|
||||||
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
|
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
|
||||||
|
|
||||||
return {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const document: any = {
|
||||||
software: {
|
software: {
|
||||||
/*
|
/*
|
||||||
* ソフトウェアの名前を変更すると、一部の独自機能が使用できなくなったり、CherryPickとして認識されないなどの不利益が発生する場合があります。
|
* ソフトウェアの名前を変更すると、一部の独自機能が使用できなくなったり、CherryPickとして認識されないなどの不利益が発生する場合があります。
|
||||||
|
@ -81,7 +82,6 @@ export class NodeinfoServerService {
|
||||||
*/
|
*/
|
||||||
name: 'cherrypick',
|
name: 'cherrypick',
|
||||||
version: this.config.version,
|
version: this.config.version,
|
||||||
repository: meta.repositoryUrl,
|
|
||||||
},
|
},
|
||||||
protocols: ['activitypub'],
|
protocols: ['activitypub'],
|
||||||
services: {
|
services: {
|
||||||
|
@ -120,23 +120,36 @@ export class NodeinfoServerService {
|
||||||
themeColor: meta.themeColor ?? '#86b300',
|
themeColor: meta.themeColor ?? '#86b300',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
if (version >= 21) {
|
||||||
|
document.software.repository = meta.repositoryUrl;
|
||||||
|
document.software.homepage = meta.repositoryUrl;
|
||||||
|
}
|
||||||
|
return document;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
||||||
|
|
||||||
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
||||||
const base = await cache.fetch(() => nodeinfo2());
|
const base = await cache.fetch(() => nodeinfo2(21));
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=600');
|
reply
|
||||||
|
.type(
|
||||||
|
'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"',
|
||||||
|
)
|
||||||
|
.header('Cache-Control', 'public, max-age=600');
|
||||||
return { version: '2.1', ...base };
|
return { version: '2.1', ...base };
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get(nodeinfo2_0path, async (request, reply) => {
|
fastify.get(nodeinfo2_0path, async (request, reply) => {
|
||||||
const base = await cache.fetch(() => nodeinfo2());
|
const base = await cache.fetch(() => nodeinfo2(20));
|
||||||
|
|
||||||
delete (base as any).software.repository;
|
delete (base as any).software.repository;
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=600');
|
reply
|
||||||
|
.type(
|
||||||
|
'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"',
|
||||||
|
)
|
||||||
|
.header('Cache-Control', 'public, max-age=600');
|
||||||
return { version: '2.0', ...base };
|
return { version: '2.0', ...base };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -299,6 +299,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
|
||||||
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||||
import * as ep___notifications_create from './endpoints/notifications/create.js';
|
import * as ep___notifications_create from './endpoints/notifications/create.js';
|
||||||
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
|
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
|
||||||
|
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
|
||||||
import * as ep___pagePush from './endpoints/page-push.js';
|
import * as ep___pagePush from './endpoints/page-push.js';
|
||||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||||
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
||||||
|
@ -676,6 +677,7 @@ const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep__
|
||||||
const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
|
const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
|
||||||
const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
|
const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
|
||||||
const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
|
const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
|
||||||
|
const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default };
|
||||||
const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
|
const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
|
||||||
const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
|
const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
|
||||||
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
|
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
|
||||||
|
@ -1057,6 +1059,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$notes_userListTimeline,
|
$notes_userListTimeline,
|
||||||
$notifications_create,
|
$notifications_create,
|
||||||
$notifications_markAllAsRead,
|
$notifications_markAllAsRead,
|
||||||
|
$notifications_testNotification,
|
||||||
$pagePush,
|
$pagePush,
|
||||||
$pages_create,
|
$pages_create,
|
||||||
$pages_delete,
|
$pages_delete,
|
||||||
|
|
|
@ -299,6 +299,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
|
||||||
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||||
import * as ep___notifications_create from './endpoints/notifications/create.js';
|
import * as ep___notifications_create from './endpoints/notifications/create.js';
|
||||||
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
|
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
|
||||||
|
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
|
||||||
import * as ep___pagePush from './endpoints/page-push.js';
|
import * as ep___pagePush from './endpoints/page-push.js';
|
||||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||||
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
||||||
|
@ -673,6 +674,7 @@ const eps = [
|
||||||
['notes/user-list-timeline', ep___notes_userListTimeline],
|
['notes/user-list-timeline', ep___notes_userListTimeline],
|
||||||
['notifications/create', ep___notifications_create],
|
['notifications/create', ep___notifications_create],
|
||||||
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
|
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
|
||||||
|
['notifications/test-notification', ep___notifications_testNotification],
|
||||||
['page-push', ep___pagePush],
|
['page-push', ep___pagePush],
|
||||||
['pages/create', ep___pages_create],
|
['pages/create', ep___pages_create],
|
||||||
['pages/delete', ep___pages_delete],
|
['pages/delete', ep___pages_delete],
|
||||||
|
|
|
@ -14,6 +14,11 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:notifications',
|
kind: 'write:notifications',
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 60,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['notifications'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:notifications',
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 60,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private notificationService: NotificationService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, user) => {
|
||||||
|
this.notificationService.createNotification(user.id, 'test', {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -80,9 +80,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('note.reply', 'reply')
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel')
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb.orWhere('note.channelId IS NULL');
|
||||||
|
qb.orWhere('channel.isSensitive = false');
|
||||||
|
}));
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
if (me) {
|
if (me) {
|
||||||
this.queryService.generateMutedUserQuery(query, me, user);
|
this.queryService.generateMutedUserQuery(query, me, user);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app'] as const;
|
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app', 'test'] as const;
|
||||||
export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const;
|
export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const;
|
||||||
|
|
||||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||||
|
|
|
@ -1985,6 +1985,10 @@ export type Endpoints = {
|
||||||
};
|
};
|
||||||
res: null;
|
res: null;
|
||||||
};
|
};
|
||||||
|
'notifications/test-notification': {
|
||||||
|
req: NoParams;
|
||||||
|
res: null;
|
||||||
|
};
|
||||||
'notifications/mark-all-as-read': {
|
'notifications/mark-all-as-read': {
|
||||||
req: NoParams;
|
req: NoParams;
|
||||||
res: null;
|
res: null;
|
||||||
|
@ -2705,6 +2709,8 @@ type Notification_2 = {
|
||||||
header?: string | null;
|
header?: string | null;
|
||||||
body: string;
|
body: string;
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
|
} | {
|
||||||
|
type: 'test';
|
||||||
});
|
});
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -2920,7 +2926,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
||||||
//
|
//
|
||||||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:658:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:659:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -562,6 +562,7 @@ export type Endpoints = {
|
||||||
|
|
||||||
// notifications
|
// notifications
|
||||||
'notifications/create': { req: { body: string; header?: string | null; icon?: string | null; }; res: null; };
|
'notifications/create': { req: { body: string; header?: string | null; icon?: string | null; }; res: null; };
|
||||||
|
'notifications/test-notification': { req: NoParams; res: null; };
|
||||||
'notifications/mark-all-as-read': { req: NoParams; res: null; };
|
'notifications/mark-all-as-read': { req: NoParams; res: null; };
|
||||||
|
|
||||||
// page-push
|
// page-push
|
||||||
|
|
|
@ -263,6 +263,8 @@ export type Notification = {
|
||||||
header?: string | null;
|
header?: string | null;
|
||||||
body: string;
|
body: string;
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
|
} | {
|
||||||
|
type: 'test';
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MessagingMessage = {
|
export type MessagingMessage = {
|
||||||
|
|
|
@ -5,8 +5,9 @@
|
||||||
|
|
||||||
import * as Misskey from 'cherrypick-js';
|
import * as Misskey from 'cherrypick-js';
|
||||||
import { Cache } from '@/scripts/cache';
|
import { Cache } from '@/scripts/cache';
|
||||||
|
import { api } from '@/os';
|
||||||
|
|
||||||
export const clipsCache = new Cache<Misskey.entities.Clip[]>(Infinity);
|
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => api('clips/list'));
|
||||||
export const rolesCache = new Cache(Infinity);
|
export const rolesCache = new Cache(1000 * 60 * 30, () => api('admin/roles/list'));
|
||||||
export const userListsCache = new Cache<Misskey.entities.UserList[]>(Infinity);
|
export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => api('users/lists/list'));
|
||||||
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(Infinity);
|
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => api('antennas/list'));
|
||||||
|
|
|
@ -38,6 +38,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
|
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
|
||||||
|
<div v-else-if="c.type === 'postForm'" :class="$style.postForm">
|
||||||
|
<MkPostForm
|
||||||
|
fixed
|
||||||
|
:instant="true"
|
||||||
|
:initialText="c.form.text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
|
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
|
||||||
<template #label>{{ c.title }}</template>
|
<template #label>{{ c.title }}</template>
|
||||||
<template v-for="child in c.children" :key="child">
|
<template v-for="child in c.children" :key="child">
|
||||||
|
@ -62,6 +69,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import { AsUiComponent } from '@/scripts/aiscript/ui';
|
import { AsUiComponent } from '@/scripts/aiscript/ui';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
component: AsUiComponent;
|
component: AsUiComponent;
|
||||||
|
@ -114,4 +122,9 @@ function openPostForm() {
|
||||||
.fontMonospace {
|
.fontMonospace {
|
||||||
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
|
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.postForm {
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,33 +5,40 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
|
<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
|
||||||
<a
|
<component
|
||||||
:class="$style.imageContainer"
|
:is="disableImageLink ? 'div' : 'a'"
|
||||||
:href="image.url"
|
v-bind="disableImageLink ? {
|
||||||
:title="image.name"
|
title: image.name,
|
||||||
|
class: $style.imageContainer,
|
||||||
|
} : {
|
||||||
|
title: image.name,
|
||||||
|
class: $style.imageContainer,
|
||||||
|
href: image.url,
|
||||||
|
style: 'cursor: zoom-in;'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<ImgWithBlurhash
|
<ImgWithBlurhash
|
||||||
:hash="image.blurhash"
|
:hash="image.blurhash"
|
||||||
:src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
|
:src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
|
||||||
:forceBlurhash="hide"
|
:forceBlurhash="hide"
|
||||||
:cover="hide"
|
:cover="hide || cover"
|
||||||
:alt="image.comment || image.name"
|
:alt="image.comment || image.name"
|
||||||
:title="image.comment || image.name"
|
:title="image.comment || image.name"
|
||||||
:width="image.properties.width"
|
:width="image.properties.width"
|
||||||
:height="image.properties.height"
|
:height="image.properties.height"
|
||||||
:style="hide ? 'filter: brightness(0.5);' : null"
|
:style="hide ? 'filter: brightness(0.5);' : null"
|
||||||
/>
|
/>
|
||||||
</a>
|
</component>
|
||||||
<template v-if="hide">
|
<template v-if="hide">
|
||||||
<div :class="$style.hiddenText">
|
<div :class="$style.hiddenText">
|
||||||
<div :class="$style.hiddenTextWrapper">
|
<div :class="$style.hiddenTextWrapper">
|
||||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
||||||
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
|
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
|
||||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
<span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else-if="controls">
|
||||||
<div :class="$style.indicators">
|
<div :class="$style.indicators">
|
||||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||||
|
@ -54,10 +61,17 @@ import { i18n } from '@/i18n';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { iAmModerator } from '@/account';
|
import { iAmModerator } from '@/account';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
image: Misskey.entities.DriveFile;
|
image: Misskey.entities.DriveFile;
|
||||||
raw?: boolean;
|
raw?: boolean;
|
||||||
}>();
|
cover?: boolean;
|
||||||
|
disableImageLink?: boolean;
|
||||||
|
controls?: boolean;
|
||||||
|
}>(), {
|
||||||
|
cover: false,
|
||||||
|
disableImageLink: false,
|
||||||
|
controls: true,
|
||||||
|
});
|
||||||
|
|
||||||
let hide = $ref(true);
|
let hide = $ref(true);
|
||||||
let darkMode: boolean = $ref(defaultStore.state.darkMode);
|
let darkMode: boolean = $ref(defaultStore.state.darkMode);
|
||||||
|
@ -70,6 +84,9 @@ const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
|
||||||
);
|
);
|
||||||
|
|
||||||
function onclick() {
|
function onclick() {
|
||||||
|
if (!props.controls) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (hide) {
|
if (hide) {
|
||||||
hide = false;
|
hide = false;
|
||||||
}
|
}
|
||||||
|
@ -117,6 +134,7 @@ function showMenu(ev: MouseEvent) {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide {
|
.hide {
|
||||||
|
@ -167,7 +185,6 @@ function showMenu(ev: MouseEvent) {
|
||||||
|
|
||||||
.imageContainer {
|
.imageContainer {
|
||||||
display: block;
|
display: block;
|
||||||
cursor: zoom-in;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.head">
|
<div :class="$style.head">
|
||||||
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
||||||
|
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
||||||
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
|
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
|
||||||
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
|
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
|
||||||
<div
|
<div
|
||||||
|
@ -49,6 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
||||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||||
|
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||||
<span v-else>{{ notification.header }}</span>
|
<span v-else>{{ notification.header }}</span>
|
||||||
<MkTime v-if="withTime && defaultStore.state.enableAbsoluteTime" :time="notification.createdAt" :class="$style.headerTime" mode="absolute"/>
|
<MkTime v-if="withTime && defaultStore.state.enableAbsoluteTime" :time="notification.createdAt" :class="$style.headerTime" mode="absolute"/>
|
||||||
|
@ -101,6 +103,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectGroupInvitation()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectGroupInvitation()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span>
|
||||||
<span v-else-if="notification.type === 'app'" :class="$style.text">
|
<span v-else-if="notification.type === 'app'" :class="$style.text">
|
||||||
<Mfm :text="notification.body" :nowrap="false"/>
|
<Mfm :text="notification.body" :nowrap="false"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -123,6 +126,7 @@ import { i18n } from '@/i18n';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
import { infoImageUrl } from '@/instance';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
|
|
|
@ -5,7 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
|
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
|
||||||
<div v-if="page.eyeCatchingImage" class="thumbnail" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
|
<div v-if="page.eyeCatchingImage" class="thumbnail">
|
||||||
|
<MediaImage
|
||||||
|
:image="page.eyeCatchingImage"
|
||||||
|
:disableImageLink="true"
|
||||||
|
:controls="false"
|
||||||
|
:cover="true"
|
||||||
|
:class="$style.eyeCatchingImageRoot"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<h1 :title="page.title">{{ page.title }}</h1>
|
<h1 :title="page.title">{{ page.title }}</h1>
|
||||||
|
@ -23,12 +31,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as Misskey from 'cherrypick-js';
|
import * as Misskey from 'cherrypick-js';
|
||||||
import { userName } from '@/filters/user';
|
import { userName } from '@/filters/user';
|
||||||
|
import MediaImage from '@/components/MkMediaImage.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
page: Misskey.entities.Page;
|
page: Misskey.entities.Page;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.eyeCatchingImageRoot {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
border-radius: var(--radius) var(--radius) 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vhpxefrj {
|
.vhpxefrj {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -39,32 +57,15 @@ const props = defineProps<{
|
||||||
}
|
}
|
||||||
|
|
||||||
> .thumbnail {
|
> .thumbnail {
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
background-position: center;
|
|
||||||
background-size: cover;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> button {
|
|
||||||
font-size: 3.5em;
|
|
||||||
opacity: 0.7;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
font-size: 4em;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& + article {
|
& + article {
|
||||||
left: 100px;
|
border-radius: 0 0 var(--radius) var(--radius);
|
||||||
width: calc(100% - 100px);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> article {
|
> article {
|
||||||
|
background-color: var(--panel);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
|
||||||
> header {
|
> header {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
|
@ -671,6 +671,8 @@ function onDrop(ev): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveDraft() {
|
function saveDraft() {
|
||||||
|
if (props.instant) return;
|
||||||
|
|
||||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
||||||
|
|
||||||
draftData[draftKey] = {
|
draftData[draftKey] = {
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="buttonEl"
|
ref="buttonEl"
|
||||||
v-ripple="canToggle"
|
v-ripple="canToggle"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: (canToggle || alternative), [$style.large]: defaultStore.state.largeNoteReactions }]"
|
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: (canToggle || alternative), [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
>
|
>
|
||||||
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||||
|
@ -135,10 +135,12 @@ useTooltip(buttonEl, async (showing) => {
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
height: 30px;
|
align-items: center;
|
||||||
|
height: 38px;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
font-size: 1.35em;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
|
||||||
&.canToggle {
|
&.canToggle {
|
||||||
|
@ -153,15 +155,24 @@ useTooltip(buttonEl, async (showing) => {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.large {
|
&.small {
|
||||||
height: 42px;
|
height: 30px;
|
||||||
font-size: 1.5em;
|
font-size: 1em;
|
||||||
padding: 4px 14px;
|
|
||||||
// border-radius: 6px;
|
|
||||||
|
|
||||||
> .count {
|
> .count {
|
||||||
font-size: 0.7em;
|
font-size: 0.9em;
|
||||||
line-height: 32px;
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
height: 46px;
|
||||||
|
font-size: 1.8em;
|
||||||
|
padding: 4px 16px;
|
||||||
|
|
||||||
|
> .count {
|
||||||
|
font-size: 0.6em;
|
||||||
|
line-height: 50px;
|
||||||
margin: 0 0 0 8px;
|
margin: 0 0 0 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,7 +194,7 @@ useTooltip(buttonEl, async (showing) => {
|
||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
line-height: 22px;
|
line-height: 32px;
|
||||||
margin: 0 0 0 5px;
|
margin: 0 0 0 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||||
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
|
<div v-if="thumbnail" :class="$style.thumbnail" :style="defaultStore.state.enableDataSaverMode ? '' : `background-image: url('${thumbnail}')`">
|
||||||
</div>
|
</div>
|
||||||
<article :class="$style.body">
|
<article :class="$style.body">
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
|
@ -260,6 +260,7 @@ onUnmounted(() => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
background-color: var(--bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -5,20 +5,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ImgWithBlurhash v-if="image" style="max-width: 100%;" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
|
<MediaImage
|
||||||
|
v-if="image"
|
||||||
|
:image="image"
|
||||||
|
:disableImageLink="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as Misskey from 'cherrypick-js';
|
import * as Misskey from 'cherrypick-js';
|
||||||
import { ImageBlock } from './block.type';
|
import { ImageBlock } from './block.type';
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import MediaImage from '@/components/MkMediaImage.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
block: ImageBlock,
|
block: ImageBlock,
|
||||||
page: Misskey.entities.Page,
|
page: Misskey.entities.Page,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const image = props.page.attachedFiles.find(x => x.id === props.block.fileId);
|
const image = ref<Misskey.entities.DriveFile>(props.page.attachedFiles.find(x => x.id === props.block.fileId));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }">
|
<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }" class="_gaps_s">
|
||||||
<XBlock v-for="child in page.content" :key="child.id" :page="page" :block="child" :h="2"/>
|
<XBlock v-for="child in page.content" :key="child.id" :page="page" :block="child" :h="2"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -42,4 +42,12 @@ export const miLocalStorage = {
|
||||||
getItem: (key: Keys): string | null => window.localStorage.getItem(key),
|
getItem: (key: Keys): string | null => window.localStorage.getItem(key),
|
||||||
setItem: (key: Keys, value: string): void => window.localStorage.setItem(key, value),
|
setItem: (key: Keys, value: string): void => window.localStorage.setItem(key, value),
|
||||||
removeItem: (key: Keys): void => window.localStorage.removeItem(key),
|
removeItem: (key: Keys): void => window.localStorage.removeItem(key),
|
||||||
|
getItemAsJson: (key: Keys): any | undefined => {
|
||||||
|
const item = miLocalStorage.getItem(key);
|
||||||
|
if (item === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return JSON.parse(item);
|
||||||
|
},
|
||||||
|
setItemAsJson: (key: Keys, value: any): void => window.localStorage.setItem(key, JSON.stringify(value)),
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkTextarea v-model="moderationNote" manualSave>
|
<MkTextarea v-model="moderationNote" manualSave>
|
||||||
<template #label>Moderation note</template>
|
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
|
|
|
@ -26,11 +26,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { onActivated } from 'vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { antennasCache } from '@/cache';
|
import { antennasCache } from '@/cache';
|
||||||
import { api } from '@/os';
|
|
||||||
import { onActivated } from 'vue';
|
|
||||||
import { infoImageUrl } from '@/instance';
|
import { infoImageUrl } from '@/instance';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
|
|
||||||
|
@ -39,7 +38,7 @@ const router = useRouter();
|
||||||
const antennas = $computed(() => antennasCache.value.value ?? []);
|
const antennas = $computed(() => antennasCache.value.value ?? []);
|
||||||
|
|
||||||
function fetch() {
|
function fetch() {
|
||||||
antennasCache.fetch(() => api('antennas/list'));
|
antennasCache.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch();
|
fetch();
|
||||||
|
@ -68,7 +67,7 @@ definePageMetadata({
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
antennasCache.fetch(() => api('antennas/list'));
|
antennasCache.fetch();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ import { $i } from '@/account';
|
||||||
const items = $computed(() => userListsCache.value.value ?? []);
|
const items = $computed(() => userListsCache.value.value ?? []);
|
||||||
|
|
||||||
function fetch() {
|
function fetch() {
|
||||||
userListsCache.fetch(() => os.api('users/lists/list'));
|
userListsCache.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch();
|
fetch();
|
||||||
|
|
|
@ -16,7 +16,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
<div class="banner">
|
<div class="banner">
|
||||||
<img v-if="page.eyeCatchingImageId" :src="page.eyeCatchingImage.url"/>
|
<MkMediaImage
|
||||||
|
v-if="page.eyeCatchingImageId"
|
||||||
|
:image="page.eyeCatchingImage"
|
||||||
|
:cover="true"
|
||||||
|
:disableImageLink="true"
|
||||||
|
class="thumbnail"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<XPage :page="page"/>
|
<XPage :page="page"/>
|
||||||
|
@ -56,8 +62,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||||
<template #icon><i class="ti ti-clock"></i></template>
|
<template #icon><i class="ti ti-clock"></i></template>
|
||||||
<template #header>{{ i18n.ts.recentPosts }}</template>
|
<template #header>{{ i18n.ts.recentPosts }}</template>
|
||||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
|
<MkPagination v-slot="{items}" :pagination="otherPostsPagination" :class="$style.relatedPagesRoot" class="_gaps">
|
||||||
<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_margin"/>
|
<MkPagePreview v-for="page in items" :key="page.id" :page="page" :class="$style.relatedPagesItem"/>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</MkContainer>
|
</MkContainer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,6 +80,7 @@ import XPage from '@/components/page/page.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { url } from '@/config';
|
import { url } from '@/config';
|
||||||
|
import MkMediaImage from '@/components/MkMediaImage.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
@ -204,11 +211,14 @@ definePageMetadata(computed(() => page ? {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .banner {
|
> .banner {
|
||||||
> img {
|
> .thumbnail {
|
||||||
// TODO: 良い感じのアスペクト比で表示
|
// TODO: 良い感じのアスペクト比で表示
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 150px;
|
height: auto;
|
||||||
|
aspect-ratio: 3/1;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,3 +289,13 @@ definePageMetadata(computed(() => page ? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.relatedPagesRoot {
|
||||||
|
padding: var(--margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relatedPagesItem > article {
|
||||||
|
background-color: var(--panelHighlight) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -41,7 +41,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
|
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
|
||||||
<MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch>
|
<MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch>
|
||||||
<MkSwitch v-model="showTranslateButtonInNote">{{ i18n.ts.showTranslateButtonInNote }} <span class="_beta">CherryPick</span></MkSwitch>
|
<MkSwitch v-model="showTranslateButtonInNote">{{ i18n.ts.showTranslateButtonInNote }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||||
<MkSwitch v-model="largeNoteReactions">{{ i18n.ts.largeNoteReactions }}</MkSwitch>
|
|
||||||
<MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
|
<MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
|
||||||
<MkSwitch v-model="collapseDefault">{{ i18n.ts.collapseDefault }} <span class="_beta">CherryPick</span></MkSwitch>
|
<MkSwitch v-model="collapseDefault">{{ i18n.ts.collapseDefault }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||||
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
|
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
|
||||||
|
@ -57,6 +56,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
|
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
|
||||||
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
|
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
|
||||||
<MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch>
|
<MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch>
|
||||||
|
<MkRadios v-model="reactionsDisplaySize">
|
||||||
|
<template #label>{{ i18n.ts.reactionsDisplaySize }}</template>
|
||||||
|
<option value="small">{{ i18n.ts.small }}</option>
|
||||||
|
<option value="medium">{{ i18n.ts.medium }}</option>
|
||||||
|
<option value="large">{{ i18n.ts.large }}</option>
|
||||||
|
</MkRadios>
|
||||||
<MkSwitch v-model="enableAbsoluteTime">{{ i18n.ts.enableAbsoluteTime }} <span class="_beta">CherryPick</span></MkSwitch>
|
<MkSwitch v-model="enableAbsoluteTime">{{ i18n.ts.enableAbsoluteTime }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||||
<MkSwitch v-model="enableMarkByDate" :disabled="defaultStore.state.enableAbsoluteTime">{{ i18n.ts.enableMarkByDate }} <span class="_beta">CherryPick</span></MkSwitch>
|
<MkSwitch v-model="enableMarkByDate" :disabled="defaultStore.state.enableAbsoluteTime">{{ i18n.ts.enableMarkByDate }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||||
<MkSwitch v-model="showSubNoteFooterButton">{{ i18n.ts.showSubNoteFooterButton }}<template #caption>{{ i18n.ts.showSubNoteFooterButtonDescription }}</template> <span class="_beta">CherryPick</span></MkSwitch>
|
<MkSwitch v-model="showSubNoteFooterButton">{{ i18n.ts.showSubNoteFooterButton }}<template #caption>{{ i18n.ts.showSubNoteFooterButtonDescription }}</template> <span class="_beta">CherryPick</span></MkSwitch>
|
||||||
|
@ -103,6 +108,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
|
<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
|
||||||
<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
|
<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
|
<MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
@ -255,6 +262,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import * as Misskey from 'cherrypick-js';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
|
@ -271,6 +279,8 @@ import { unisonReload } from '@/scripts/unison-reload';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { miLocalStorage } from '@/local-storage';
|
import { miLocalStorage } from '@/local-storage';
|
||||||
|
import { globalEvents } from '@/events';
|
||||||
|
import { claimAchievement } from '@/scripts/achievements';
|
||||||
import { eventBus } from '@/scripts/cherrypick/eventBus';
|
import { eventBus } from '@/scripts/cherrypick/eventBus';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
|
||||||
|
@ -297,7 +307,7 @@ const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDev
|
||||||
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
|
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
|
||||||
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
|
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
|
||||||
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
|
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
|
||||||
const largeNoteReactions = computed(defaultStore.makeGetterSetter('largeNoteReactions'));
|
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
|
||||||
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
||||||
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
||||||
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
||||||
|
@ -378,6 +388,7 @@ watch([
|
||||||
instanceTicker,
|
instanceTicker,
|
||||||
overridedDeviceKind,
|
overridedDeviceKind,
|
||||||
mediaListWithOneImageAppearance,
|
mediaListWithOneImageAppearance,
|
||||||
|
reactionsDisplaySize,
|
||||||
enableDataSaverMode,
|
enableDataSaverMode,
|
||||||
enableAbsoluteTime,
|
enableAbsoluteTime,
|
||||||
enableMarkByDate,
|
enableMarkByDate,
|
||||||
|
@ -414,6 +425,32 @@ function removeEmojiIndex(lang: string) {
|
||||||
os.promiseDialog(main());
|
os.promiseDialog(main());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let smashCount = 0;
|
||||||
|
let smashTimer: number | null = null;
|
||||||
|
function testNotification(): void {
|
||||||
|
const notification: Misskey.entities.Notification = {
|
||||||
|
id: Math.random().toString(),
|
||||||
|
createdAt: new Date().toUTCString(),
|
||||||
|
isRead: false,
|
||||||
|
type: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
globalEvents.emit('clientNotification', notification);
|
||||||
|
|
||||||
|
// セルフ通知破壊 実績関連
|
||||||
|
smashCount++;
|
||||||
|
if (smashCount >= 10) {
|
||||||
|
claimAchievement('smashTestNotificationButton');
|
||||||
|
smashCount = 0;
|
||||||
|
}
|
||||||
|
if (smashTimer) {
|
||||||
|
clearTimeout(smashTimer);
|
||||||
|
}
|
||||||
|
smashTimer = window.setTimeout(() => {
|
||||||
|
smashCount = 0;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (fontSizeBefore.value == null) {
|
if (fontSizeBefore.value == null) {
|
||||||
fontSizeBefore.value = fontSize.value as string;
|
fontSizeBefore.value = fontSize.value as string;
|
||||||
|
|
|
@ -13,6 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<FormLink @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
|
<FormLink @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
<FormSection>
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<FormLink @click="testNotification">{{ i18n.ts._notification.sendTestNotification }}</FormLink>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts.pushNotification }}</template>
|
<template #label>{{ i18n.ts.pushNotification }}</template>
|
||||||
|
|
||||||
|
@ -88,6 +93,10 @@ function onChangeSendReadMessage(v: boolean) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testNotification(): void {
|
||||||
|
os.api('notifications/test-notification');
|
||||||
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
|
|
@ -57,8 +57,9 @@ import { i18n } from '@/i18n';
|
||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { eventBus } from '@/scripts/cherrypick/eventBus';
|
import { antennasCache, userListsCache } from '@/cache';
|
||||||
import { miLocalStorage } from '@/local-storage';
|
import { miLocalStorage } from '@/local-storage';
|
||||||
|
import { eventBus } from '@/scripts/cherrypick/eventBus';
|
||||||
import { deviceKind } from '@/scripts/device-kind';
|
import { deviceKind } from '@/scripts/device-kind';
|
||||||
import { unisonReload } from '@/scripts/unison-reload';
|
import { unisonReload } from '@/scripts/unison-reload';
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@ function top(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseList(ev: MouseEvent): Promise<void> {
|
async function chooseList(ev: MouseEvent): Promise<void> {
|
||||||
const lists = await os.api('users/lists/list');
|
const lists = await userListsCache.fetch();
|
||||||
const items = lists.map(list => ({
|
const items = lists.map(list => ({
|
||||||
type: 'link' as const,
|
type: 'link' as const,
|
||||||
text: list.name,
|
text: list.name,
|
||||||
|
@ -122,7 +123,7 @@ async function chooseList(ev: MouseEvent): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
||||||
const antennas = await os.api('antennas/list');
|
const antennas = await antennasCache.fetch();
|
||||||
const items = antennas.map(antenna => ({
|
const items = antennas.map(antenna => ({
|
||||||
type: 'link' as const,
|
type: 'link' as const,
|
||||||
text: antenna.name,
|
text: antenna.name,
|
||||||
|
|
|
@ -59,10 +59,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div v-if="iAmModerator" class="moderationNote">
|
<div v-if="iAmModerator" class="moderationNote">
|
||||||
<MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave>
|
<MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave>
|
||||||
<template #label>Moderation note</template>
|
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<MkButton class="moderationNoteButton" small @click="editModerationNote = true">Add moderation note</MkButton>
|
<MkButton class="moderationNoteButton" small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}">
|
<div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}">
|
||||||
|
|
|
@ -81,6 +81,7 @@ export const ACHIEVEMENT_TYPES = [
|
||||||
'setNameToSyuilo',
|
'setNameToSyuilo',
|
||||||
'cookieClicked',
|
'cookieClicked',
|
||||||
'brainDiver',
|
'brainDiver',
|
||||||
|
'smashTestNotificationButton',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const ACHIEVEMENT_BADGES = {
|
export const ACHIEVEMENT_BADGES = {
|
||||||
|
@ -454,6 +455,11 @@ export const ACHIEVEMENT_BADGES = {
|
||||||
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
|
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
|
||||||
frame: 'bronze',
|
frame: 'bronze',
|
||||||
},
|
},
|
||||||
|
'smashTestNotificationButton': {
|
||||||
|
img: '/fluent-emoji/1f514.png',
|
||||||
|
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
|
||||||
|
frame: 'bronze',
|
||||||
|
},
|
||||||
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
|
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
|
||||||
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
|
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
|
||||||
img: string;
|
img: string;
|
||||||
|
|
|
@ -124,7 +124,14 @@ export type AsUiPostFormButton = AsUiComponentBase & {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton;
|
export type AsUiPostForm = AsUiComponentBase & {
|
||||||
|
type: 'postForm';
|
||||||
|
form?: {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm;
|
||||||
|
|
||||||
export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
|
export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -462,6 +469,27 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostForm, 'id' | 'type'> {
|
||||||
|
utils.assertObject(def);
|
||||||
|
|
||||||
|
const form = def.value.get('form');
|
||||||
|
if (form) utils.assertObject(form);
|
||||||
|
|
||||||
|
const getForm = () => {
|
||||||
|
const text = form!.value.get('text');
|
||||||
|
utils.assertString(text);
|
||||||
|
return {
|
||||||
|
text: text.value,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
form: form ? getForm() : {
|
||||||
|
text: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
|
export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
|
||||||
const instances = {};
|
const instances = {};
|
||||||
|
|
||||||
|
@ -569,5 +597,9 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
|
||||||
'Ui:C:postFormButton': values.FN_NATIVE(([def, id], opts) => {
|
'Ui:C:postFormButton': values.FN_NATIVE(([def, id], opts) => {
|
||||||
return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.call);
|
return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.call);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
'Ui:C:postForm': values.FN_NATIVE(([def, id], opts) => {
|
||||||
|
return createComponentInstance('postForm', def, id, getPostFormOptions, opts.call);
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,11 @@ export class Cache<T> {
|
||||||
private cachedAt: number | null = null;
|
private cachedAt: number | null = null;
|
||||||
public value = ref<T | undefined>();
|
public value = ref<T | undefined>();
|
||||||
private lifetime: number;
|
private lifetime: number;
|
||||||
|
private fetcher: () => Promise<T>;
|
||||||
|
|
||||||
constructor(lifetime: Cache<never>['lifetime']) {
|
constructor(lifetime: Cache<never>['lifetime'], fetcher: () => Promise<T>) {
|
||||||
this.lifetime = lifetime;
|
this.lifetime = lifetime;
|
||||||
|
this.fetcher = fetcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(value: T): void {
|
public set(value: T): void {
|
||||||
|
@ -35,51 +37,17 @@ export class Cache<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
|
||||||
*/
|
*/
|
||||||
public async fetch(fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
public async fetch(): Promise<T> {
|
||||||
const cachedValue = this.get();
|
const cachedValue = this.get();
|
||||||
if (cachedValue !== undefined) {
|
if (cachedValue !== undefined) {
|
||||||
if (validator) {
|
// Cache HIT
|
||||||
if (validator(cachedValue)) {
|
return cachedValue;
|
||||||
// Cache HIT
|
|
||||||
return cachedValue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Cache HIT
|
|
||||||
return cachedValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache MISS
|
// Cache MISS
|
||||||
const value = await fetcher();
|
const value = await this.fetcher();
|
||||||
this.set(value);
|
this.set(value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
|
||||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
|
||||||
*/
|
|
||||||
public async fetchMaybe(fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
|
|
||||||
const cachedValue = this.get();
|
|
||||||
if (cachedValue !== undefined) {
|
|
||||||
if (validator) {
|
|
||||||
if (validator(cachedValue)) {
|
|
||||||
// Cache HIT
|
|
||||||
return cachedValue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Cache HIT
|
|
||||||
return cachedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache MISS
|
|
||||||
const value = await fetcher();
|
|
||||||
if (value !== undefined) {
|
|
||||||
this.set(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export async function getNoteClipMenu(props: {
|
||||||
|
|
||||||
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
||||||
|
|
||||||
const clips = await clipsCache.fetch(() => os.api('clips/list'));
|
const clips = await clipsCache.fetch();
|
||||||
return [...clips.map(clip => ({
|
return [...clips.map(clip => ({
|
||||||
text: clip.name,
|
text: clip.name,
|
||||||
action: () => {
|
action: () => {
|
||||||
|
|
|
@ -218,7 +218,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||||
icon: 'ti ti-list',
|
icon: 'ti ti-list',
|
||||||
text: i18n.ts.addToList,
|
text: i18n.ts.addToList,
|
||||||
children: async () => {
|
children: async () => {
|
||||||
const lists = await userListsCache.fetch(() => os.api('users/lists/list'));
|
const lists = await userListsCache.fetch();
|
||||||
return lists.map(list => {
|
return lists.map(list => {
|
||||||
const isListed = ref(list.userIds.includes(user.id));
|
const isListed = ref(list.userIds.includes(user.id));
|
||||||
cleanups.push(watch(isListed, () => {
|
cleanups.push(watch(isListed, () => {
|
||||||
|
@ -251,7 +251,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||||
icon: 'ti ti-antenna',
|
icon: 'ti ti-antenna',
|
||||||
text: i18n.ts.addToAntenna,
|
text: i18n.ts.addToAntenna,
|
||||||
children: async () => {
|
children: async () => {
|
||||||
const antennas = await antennasCache.fetch(() => os.api('antennas/list'));
|
const antennas = await antennasCache.fetch();
|
||||||
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
|
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
|
||||||
return antennas.filter((a) => a.src === 'users').map(antenna => ({
|
return antennas.filter((a) => a.src === 'users').map(antenna => ({
|
||||||
text: antenna.name,
|
text: antenna.name,
|
||||||
|
@ -282,7 +282,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||||
icon: 'ti ti-badges',
|
icon: 'ti ti-badges',
|
||||||
text: i18n.ts.roles,
|
text: i18n.ts.roles,
|
||||||
children: async () => {
|
children: async () => {
|
||||||
const roles = await rolesCache.fetch(() => os.api('admin/roles/list'));
|
const roles = await rolesCache.fetch();
|
||||||
|
|
||||||
return roles.filter(r => r.target === 'manual').map(r => ({
|
return roles.filter(r => r.target === 'manual').map(r => ({
|
||||||
text: r.name,
|
text: r.name,
|
||||||
|
|
|
@ -341,9 +341,9 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
largeNoteReactions: {
|
reactionsDisplaySize: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: 'small' as 'small' | 'medium' | 'large',
|
||||||
},
|
},
|
||||||
forceShowAds: {
|
forceShowAds: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
|
|
@ -56,6 +56,7 @@ import { $i } from '@/account';
|
||||||
import { useStream } from '@/stream';
|
import { useStream } from '@/stream';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
import { globalEvents } from '@/events';
|
||||||
|
|
||||||
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
|
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
|
||||||
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
|
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
|
||||||
|
@ -64,11 +65,13 @@ const dev = _DEV_;
|
||||||
|
|
||||||
let notifications = $ref<Misskey.entities.Notification[]>([]);
|
let notifications = $ref<Misskey.entities.Notification[]>([]);
|
||||||
|
|
||||||
function onNotification(notification) {
|
function onNotification(notification: Misskey.entities.Notification, isClient: boolean = false) {
|
||||||
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
||||||
|
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
useStream().send('readNotification');
|
if (!isClient) {
|
||||||
|
useStream().send('readNotification');
|
||||||
|
}
|
||||||
|
|
||||||
notifications.unshift(notification);
|
notifications.unshift(notification);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
|
@ -86,6 +89,7 @@ function onNotification(notification) {
|
||||||
if ($i) {
|
if ($i) {
|
||||||
const connection = useStream().useChannel('main', null, 'UI');
|
const connection = useStream().useChannel('main', null, 'UI');
|
||||||
connection.on('notification', onNotification);
|
connection.on('notification', onNotification);
|
||||||
|
globalEvents.on('clientNotification', notification => onNotification(notification, true));
|
||||||
|
|
||||||
//#region Listen message from SW
|
//#region Listen message from SW
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { type UserConfig, defineConfig } from 'vite';
|
||||||
import ReactivityTransform from '@vue-macros/reactivity-transform/vite';
|
import ReactivityTransform from '@vue-macros/reactivity-transform/vite';
|
||||||
|
|
||||||
import locales from '../../locales';
|
import locales from '../../locales';
|
||||||
import generateDTS from '../../locales/generateDTS';
|
|
||||||
import meta from '../../package.json';
|
import meta from '../../package.json';
|
||||||
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name';
|
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name';
|
||||||
import pluginJson5 from './vite.json5';
|
import pluginJson5 from './vite.json5';
|
||||||
|
@ -67,10 +66,6 @@ export function getConfig(): UserConfig {
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
{
|
|
||||||
name: 'locale:generateDTS',
|
|
||||||
buildStart: generateDTS,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import postcss from 'postcss';
|
||||||
import * as terser from 'terser';
|
import * as terser from 'terser';
|
||||||
|
|
||||||
import locales from '../locales/index.js';
|
import locales from '../locales/index.js';
|
||||||
|
import generateDTS from '../locales/generateDTS.js';
|
||||||
import meta from '../package.json' assert { type: "json" };
|
import meta from '../package.json' assert { type: "json" };
|
||||||
|
|
||||||
async function copyFrontendFonts() {
|
async function copyFrontendFonts() {
|
||||||
|
@ -21,6 +22,8 @@ async function copyFrontendTablerIcons() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyFrontendLocales() {
|
async function copyFrontendLocales() {
|
||||||
|
generateDTS();
|
||||||
|
|
||||||
await fs.mkdir('./built/_frontend_dist_/locales', { recursive: true });
|
await fs.mkdir('./built/_frontend_dist_/locales', { recursive: true });
|
||||||
|
|
||||||
const v = { '_version_': meta.version };
|
const v = { '_version_': meta.version };
|
||||||
|
|
Loading…
Reference in a new issue