Compare commits
32 commits
dependabot
...
develop
Author | SHA1 | Date | |
---|---|---|---|
Mizah | a858ee31c6 | ||
Mizah | 0bd7ed8191 | ||
Mizah | ee574ae154 | ||
Mizah | a505f36252 | ||
Mizah | a516383c66 | ||
Mizah | 1613dafc39 | ||
Mizah | 8457fa9b3b | ||
Mizah | 2e51e779e7 | ||
Mizah | 7f6b486976 | ||
Mizah | 8c1508fae4 | ||
Mizah | aeb568664d | ||
Mizah | ecb990fb77 | ||
a11b77a415 | |||
1bc4f400c0 | |||
458c72c153 | |||
6bd3ed2074 | |||
31e5f0bd09 | |||
e0a83e9c9e | |||
1496700b37 | |||
00cbf9fe80 | |||
cf09aa21f0 | |||
9f7d41eb47 | |||
4a62051ce7 | |||
3a421837bf | |||
a4c5ce1413 | |||
e75b62f3f5 | |||
5b60ae810b | |||
98b4717c45 | |||
8a4ce16e90 | |||
794cb9ffe2 | |||
0b976064ca | |||
bca690f256 |
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,8 +1,10 @@
|
||||||
## 2024.10.2
|
## 2024.11.0
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- Feat: コンテンツの表示にログインを必須にできるように
|
- Feat: コンテンツの表示にログインを必須にできるように
|
||||||
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
|
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
|
||||||
|
- Enhance: 依存関係の更新
|
||||||
|
- Enhance: l10nの更新
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
|
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
|
||||||
|
@ -15,7 +17,8 @@
|
||||||
- どのアカウントで認証しようとしているのかがわかるように
|
- どのアカウントで認証しようとしているのかがわかるように
|
||||||
- 認証するアカウントを切り替えられるように
|
- 認証するアカウントを切り替えられるように
|
||||||
- Enhance: Self-XSS防止用の警告を追加
|
- Enhance: Self-XSS防止用の警告を追加
|
||||||
- Enhance: カタルーニャ語 (ca-ES) に対応
|
- Enhance: カタルーニャ語 (ca-ES) に対応
|
||||||
|
- Enhance: 個別お知らせページではMetaタグを出力するように
|
||||||
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
||||||
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
||||||
|
@ -24,19 +27,25 @@
|
||||||
- Fix: リンク切れを修正
|
- Fix: リンク切れを修正
|
||||||
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
|
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
|
||||||
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
|
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
|
||||||
|
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
|
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
|
||||||
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
|
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
|
||||||
|
- Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように
|
||||||
|
- Fix: フォロワーへのメッセージの絵文字をemojisに含めるように
|
||||||
- Fix: Nested proxy requestsを検出した際にブロックするように
|
- Fix: Nested proxy requestsを検出した際にブロックするように
|
||||||
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
|
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
|
||||||
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
|
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
|
||||||
- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
|
- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
|
||||||
|
- Fix: ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
|
||||||
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
|
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
|
||||||
|
- Fix: User Webhookテスト機能のMock Payloadを修正
|
||||||
|
|
||||||
### Misskey.js
|
### Misskey.js
|
||||||
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
||||||
|
|
|
@ -83,6 +83,10 @@ One should not add property that has defined before by other implementation, or
|
||||||
## Reviewers guide
|
## Reviewers guide
|
||||||
Be willing to comment on the good points and not just the things you want fixed 💯
|
Be willing to comment on the good points and not just the things you want fixed 💯
|
||||||
|
|
||||||
|
読んでおくといいやつ
|
||||||
|
- https://blog.lacolaco.net/posts/1e2cf439b3c2/
|
||||||
|
- https://konifar-zatsu.hatenadiary.jp/entry/2024/11/05/192421
|
||||||
|
|
||||||
### Review perspective
|
### Review perspective
|
||||||
- Scope
|
- Scope
|
||||||
- Are the goals of the PR clear?
|
- Are the goals of the PR clear?
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2024.10.2-alpha.2",
|
"version": "2024.11.0-alpha.0",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -407,7 +407,9 @@ export class MfmService {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
const { username, host, acct } = node.props;
|
const { username, host, acct } = node.props;
|
||||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
|
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
|
||||||
a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`);
|
a.setAttribute('href', remoteUserInfo
|
||||||
|
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
|
||||||
|
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
|
||||||
a.className = 'u-url mention';
|
a.className = 'u-url mention';
|
||||||
a.textContent = acct;
|
a.textContent = acct;
|
||||||
return a;
|
return a;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
import type { MiWebhook, WebhookEventTypes, webhookEventTypes } from '@/models/Webhook.js';
|
||||||
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -35,6 +35,7 @@ import type {
|
||||||
} from './QueueModule.js';
|
} from './QueueModule.js';
|
||||||
import type httpSignature from '@peertube/http-signature';
|
import type httpSignature from '@peertube/http-signature';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
import { type UserWebhookPayload } from './UserWebhookService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
|
@ -468,10 +469,10 @@ export class QueueService {
|
||||||
* @see UserWebhookDeliverProcessorService
|
* @see UserWebhookDeliverProcessorService
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public userWebhookDeliver(
|
public userWebhookDeliver<T extends WebhookEventTypes>(
|
||||||
webhook: MiWebhook,
|
webhook: MiWebhook,
|
||||||
type: typeof webhookEventTypes[number],
|
type: T,
|
||||||
content: unknown,
|
content: UserWebhookPayload<T>,
|
||||||
opts?: { attempts?: number },
|
opts?: { attempts?: number },
|
||||||
) {
|
) {
|
||||||
const data: UserWebhookDeliverJobData = {
|
const data: UserWebhookDeliverJobData = {
|
||||||
|
|
|
@ -6,11 +6,23 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { type WebhooksRepository } from '@/models/_.js';
|
import { type WebhooksRepository } from '@/models/_.js';
|
||||||
import { MiWebhook } from '@/models/Webhook.js';
|
import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
|
export type UserWebhookPayload<T extends WebhookEventTypes> =
|
||||||
|
T extends 'note' | 'reply' | 'renote' |'mention' ? {
|
||||||
|
note: Packed<'Note'>,
|
||||||
|
} :
|
||||||
|
T extends 'follow' | 'unfollow' ? {
|
||||||
|
user: Packed<'UserDetailedNotMe'>,
|
||||||
|
} :
|
||||||
|
T extends 'followed' ? {
|
||||||
|
user: Packed<'UserLite'>,
|
||||||
|
} : never;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserWebhookService implements OnApplicationShutdown {
|
export class UserWebhookService implements OnApplicationShutdown {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWeb
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
import { Packed } from '@/misc/json-schema.js';
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
||||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||||
|
|
||||||
|
@ -306,10 +306,10 @@ export class WebhookTestService {
|
||||||
* - 送信対象イベント(on)に関する設定
|
* - 送信対象イベント(on)に関する設定
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async testUserWebhook(
|
public async testUserWebhook<T extends WebhookEventTypes>(
|
||||||
params: {
|
params: {
|
||||||
webhookId: MiWebhook['id'],
|
webhookId: MiWebhook['id'],
|
||||||
type: WebhookEventTypes,
|
type: T,
|
||||||
override?: Partial<Omit<MiWebhook, 'id'>>,
|
override?: Partial<Omit<MiWebhook, 'id'>>,
|
||||||
},
|
},
|
||||||
sender: MiUser | null,
|
sender: MiUser | null,
|
||||||
|
@ -321,7 +321,7 @@ export class WebhookTestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhook = webhooks[0];
|
const webhook = webhooks[0];
|
||||||
const send = (contents: unknown) => {
|
const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
|
||||||
const merged = {
|
const merged = {
|
||||||
...webhook,
|
...webhook,
|
||||||
...params.override,
|
...params.override,
|
||||||
|
@ -329,7 +329,7 @@ export class WebhookTestService {
|
||||||
|
|
||||||
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||||
// また、Jobの試行回数も1回だけ.
|
// また、Jobの試行回数も1回だけ.
|
||||||
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const dummyNote1 = generateDummyNote({
|
const dummyNote1 = generateDummyNote({
|
||||||
|
@ -361,33 +361,40 @@ export class WebhookTestService {
|
||||||
|
|
||||||
switch (params.type) {
|
switch (params.type) {
|
||||||
case 'note': {
|
case 'note': {
|
||||||
send(toPackedNote(dummyNote1));
|
send('note', { note: toPackedNote(dummyNote1) });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'reply': {
|
case 'reply': {
|
||||||
send(toPackedNote(dummyReply1));
|
send('reply', { note: toPackedNote(dummyReply1) });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'renote': {
|
case 'renote': {
|
||||||
send(toPackedNote(dummyRenote1));
|
send('renote', { note: toPackedNote(dummyRenote1) });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'mention': {
|
case 'mention': {
|
||||||
send(toPackedNote(dummyMention1));
|
send('mention', { note: toPackedNote(dummyMention1) });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'follow': {
|
case 'follow': {
|
||||||
send(toPackedUserDetailedNotMe(dummyUser1));
|
send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'followed': {
|
case 'followed': {
|
||||||
send(toPackedUserLite(dummyUser2));
|
send('followed', { user: toPackedUserLite(dummyUser2) });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'unfollow': {
|
case 'unfollow': {
|
||||||
send(toPackedUserDetailedNotMe(dummyUser3));
|
send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// まだ実装されていない (#9485)
|
||||||
|
case 'reaction': return;
|
||||||
|
default: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const _exhaustiveAssertion: never = params.type;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,5 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function sqlLikeEscape(s: string) {
|
export function sqlLikeEscape(s: string) {
|
||||||
return s.replace(/([%_])/g, '\\$1');
|
return s.replace(/([\\%_])/g, '\\$1');
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IActivity } from '@/core/activitypub/type.js';
|
import { IActivity } from '@/core/activitypub/type.js';
|
||||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
|
|
||||||
|
@ -486,6 +487,16 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// リモートだったらリダイレクト
|
||||||
|
if (user.host != null) {
|
||||||
|
if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
|
||||||
|
reply.code(500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reply.redirect(user.uri, 301);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
|
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
|
||||||
|
@ -654,19 +665,20 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: userId,
|
id: userId,
|
||||||
host: IsNull(),
|
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.userInfo(request, reply, user);
|
return await this.userInfo(request, reply, user);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
|
const acct = Acct.parse(request.params.acct);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
usernameLower: request.params.user.toLowerCase(),
|
usernameLower: acct.username,
|
||||||
host: IsNull(),
|
host: acct.host ?? IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -465,6 +465,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const newName = updates.name === undefined ? user.name : updates.name;
|
const newName = updates.name === undefined ? user.name : updates.name;
|
||||||
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
|
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
|
||||||
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
|
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
|
||||||
|
const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
|
||||||
|
|
||||||
if (newName != null) {
|
if (newName != null) {
|
||||||
let hasProhibitedWords = false;
|
let hasProhibitedWords = false;
|
||||||
|
@ -494,6 +495,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newFollowedMessage != null) {
|
||||||
|
const tokens = mfm.parse(newFollowedMessage);
|
||||||
|
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
|
||||||
|
}
|
||||||
|
|
||||||
updates.emojis = emojis;
|
updates.emojis = emojis;
|
||||||
updates.tags = tags;
|
updates.tags = tags;
|
||||||
|
|
||||||
|
|
|
@ -42,13 +42,26 @@ import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
|
||||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type {
|
||||||
|
AnnouncementsRepository,
|
||||||
|
ChannelsRepository,
|
||||||
|
ClipsRepository,
|
||||||
|
FlashsRepository,
|
||||||
|
GalleryPostsRepository,
|
||||||
|
MiMeta,
|
||||||
|
NotesRepository,
|
||||||
|
PagesRepository,
|
||||||
|
ReversiGamesRepository,
|
||||||
|
UserProfilesRepository,
|
||||||
|
UsersRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||||
|
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
||||||
import { FeedService } from './FeedService.js';
|
import { FeedService } from './FeedService.js';
|
||||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||||
import { ClientLoggerService } from './ClientLoggerService.js';
|
import { ClientLoggerService } from './ClientLoggerService.js';
|
||||||
|
@ -103,6 +116,9 @@ export class ClientServerService {
|
||||||
@Inject(DI.reversiGamesRepository)
|
@Inject(DI.reversiGamesRepository)
|
||||||
private reversiGamesRepository: ReversiGamesRepository,
|
private reversiGamesRepository: ReversiGamesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.announcementsRepository)
|
||||||
|
private announcementsRepository: AnnouncementsRepository,
|
||||||
|
|
||||||
private flashEntityService: FlashEntityService,
|
private flashEntityService: FlashEntityService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
@ -112,6 +128,7 @@ export class ClientServerService {
|
||||||
private clipEntityService: ClipEntityService,
|
private clipEntityService: ClipEntityService,
|
||||||
private channelEntityService: ChannelEntityService,
|
private channelEntityService: ChannelEntityService,
|
||||||
private reversiGameEntityService: ReversiGameEntityService,
|
private reversiGameEntityService: ReversiGameEntityService,
|
||||||
|
private announcementEntityService: AnnouncementEntityService,
|
||||||
private urlPreviewService: UrlPreviewService,
|
private urlPreviewService: UrlPreviewService,
|
||||||
private feedService: FeedService,
|
private feedService: FeedService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
|
@ -776,6 +793,24 @@ export class ClientServerService {
|
||||||
return await renderBase(reply);
|
return await renderBase(reply);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 個別お知らせページ
|
||||||
|
fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => {
|
||||||
|
const announcement = await this.announcementsRepository.findOneBy({
|
||||||
|
id: request.params.announcementId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (announcement) {
|
||||||
|
const _announcement = await this.announcementEntityService.pack(announcement);
|
||||||
|
reply.header('Cache-Control', 'public, max-age=3600');
|
||||||
|
return await reply.view('announcement', {
|
||||||
|
announcement: _announcement,
|
||||||
|
...await this.generateCommonPugData(this.meta),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return await renderBase(reply);
|
||||||
|
}
|
||||||
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region noindex pages
|
//#region noindex pages
|
||||||
|
|
21
packages/backend/src/server/web/views/announcement.pug
Normal file
21
packages/backend/src/server/web/views/announcement.pug
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
extends ./base
|
||||||
|
|
||||||
|
block vars
|
||||||
|
- const title = announcement.title;
|
||||||
|
- const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
|
||||||
|
- const url = `${config.url}/announcements/${announcement.id}`;
|
||||||
|
|
||||||
|
block title
|
||||||
|
= `${title} | ${instanceName}`
|
||||||
|
|
||||||
|
block desc
|
||||||
|
meta(name='description' content=description)
|
||||||
|
|
||||||
|
block og
|
||||||
|
meta(property='og:type' content='article')
|
||||||
|
meta(property='og:title' content= title)
|
||||||
|
meta(property='og:description' content= description)
|
||||||
|
meta(property='og:url' content= url)
|
||||||
|
if announcement.imageUrl
|
||||||
|
meta(property='og:image' content=announcement.imageUrl)
|
||||||
|
meta(property='twitter:card' content='summary_large_image')
|
|
@ -2,6 +2,7 @@ block vars
|
||||||
|
|
||||||
block loadClientEntry
|
block loadClientEntry
|
||||||
- const entry = config.frontendEntry;
|
- const entry = config.frontendEntry;
|
||||||
|
- const baseUrl = config.url;
|
||||||
|
|
||||||
doctype html
|
doctype html
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ html
|
||||||
link(rel='icon' href= icon || '/favicon.ico')
|
link(rel='icon' href= icon || '/favicon.ico')
|
||||||
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
||||||
link(rel='manifest' href='/manifest.json')
|
link(rel='manifest' href='/manifest.json')
|
||||||
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
|
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
|
||||||
link(rel='prefetch' href=serverErrorImageUrl)
|
link(rel='prefetch' href=serverErrorImageUrl)
|
||||||
link(rel='prefetch' href=infoImageUrl)
|
link(rel='prefetch' href=infoImageUrl)
|
||||||
link(rel='prefetch' href=notFoundImageUrl)
|
link(rel='prefetch' href=notFoundImageUrl)
|
||||||
|
|
|
@ -230,6 +230,7 @@ describe('Webリソース', () => {
|
||||||
path: path('xxxxxxxxxx'),
|
path: path('xxxxxxxxxx'),
|
||||||
type: HTML,
|
type: HTML,
|
||||||
}));
|
}));
|
||||||
|
test.todo('HTMLとしてGETできる。(リモートユーザーでもリダイレクトせず)');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
|
@ -249,6 +250,7 @@ describe('Webリソース', () => {
|
||||||
path: path('xxxxxxxxxx'),
|
path: path('xxxxxxxxxx'),
|
||||||
accept,
|
accept,
|
||||||
}));
|
}));
|
||||||
|
test.todo('はオリジナルにリダイレクトされる。(リモートユーザー)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { beforeAll, describe, jest } from '@jest/globals';
|
import { beforeAll, describe, jest } from '@jest/globals';
|
||||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
import { UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
@ -122,7 +122,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('note');
|
expect(calls[1]).toBe('note');
|
||||||
expect((calls[2] as any).id).toBe('dummy-note-1');
|
expect((calls[2] as UserWebhookPayload<'note'>).note.id).toBe('dummy-note-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reply', async () => {
|
test('reply', async () => {
|
||||||
|
@ -131,7 +131,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('reply');
|
expect(calls[1]).toBe('reply');
|
||||||
expect((calls[2] as any).id).toBe('dummy-reply-1');
|
expect((calls[2] as UserWebhookPayload<'reply'>).note.id).toBe('dummy-reply-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renote', async () => {
|
test('renote', async () => {
|
||||||
|
@ -140,7 +140,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('renote');
|
expect(calls[1]).toBe('renote');
|
||||||
expect((calls[2] as any).id).toBe('dummy-renote-1');
|
expect((calls[2] as UserWebhookPayload<'renote'>).note.id).toBe('dummy-renote-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('mention', async () => {
|
test('mention', async () => {
|
||||||
|
@ -149,7 +149,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('mention');
|
expect(calls[1]).toBe('mention');
|
||||||
expect((calls[2] as any).id).toBe('dummy-mention-1');
|
expect((calls[2] as UserWebhookPayload<'mention'>).note.id).toBe('dummy-mention-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('follow', async () => {
|
test('follow', async () => {
|
||||||
|
@ -158,7 +158,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('follow');
|
expect(calls[1]).toBe('follow');
|
||||||
expect((calls[2] as any).id).toBe('dummy-user-1');
|
expect((calls[2] as UserWebhookPayload<'follow'>).user.id).toBe('dummy-user-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('followed', async () => {
|
test('followed', async () => {
|
||||||
|
@ -167,7 +167,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('followed');
|
expect(calls[1]).toBe('followed');
|
||||||
expect((calls[2] as any).id).toBe('dummy-user-2');
|
expect((calls[2] as UserWebhookPayload<'followed'>).user.id).toBe('dummy-user-2');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unfollow', async () => {
|
test('unfollow', async () => {
|
||||||
|
@ -176,7 +176,7 @@ describe('WebhookTestService', () => {
|
||||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||||
expect(calls[1]).toBe('unfollow');
|
expect(calls[1]).toBe('unfollow');
|
||||||
expect((calls[2] as any).id).toBe('dummy-user-3');
|
expect((calls[2] as UserWebhookPayload<'unfollow'>).user.id).toBe('dummy-user-3');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('NoSuchWebhookError', () => {
|
describe('NoSuchWebhookError', () => {
|
||||||
|
|
|
@ -22,23 +22,66 @@ type Account = Misskey.entities.MeDetailed & { token: string };
|
||||||
const accountData = miLocalStorage.getItem('account');
|
const accountData = miLocalStorage.getItem('account');
|
||||||
|
|
||||||
// TODO: 外部からはreadonlyに
|
// TODO: 外部からはreadonlyに
|
||||||
|
/**
|
||||||
|
* Reactive state for the current account. "I" as in "I am logged in".
|
||||||
|
* Initialized from local storage if available, otherwise null.
|
||||||
|
*
|
||||||
|
* @type {Account | null}
|
||||||
|
*/
|
||||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the current account is a moderator.
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
|
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the current account is an administrator.
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
export const iAmAdmin = $i != null && $i.isAdmin;
|
export const iAmAdmin = $i != null && $i.isAdmin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether it is necessary to sign in; checks if the current
|
||||||
|
* account is null and throws an error if so.
|
||||||
|
*
|
||||||
|
* @throws {Error} If the current account is null
|
||||||
|
* @returns {Account} The current account
|
||||||
|
*/
|
||||||
export function signinRequired() {
|
export function signinRequired() {
|
||||||
if ($i == null) throw new Error('signin required');
|
if ($i == null) throw new Error('signin required');
|
||||||
return $i;
|
return $i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the current number of notes from the current account.
|
||||||
|
*
|
||||||
|
* Note: This appears to only be used for the "notes1" achievement.
|
||||||
|
*
|
||||||
|
* Also, separating it like this might cause counts to get out-of-sync.
|
||||||
|
*/
|
||||||
export let notesCount = $i == null ? 0 : $i.notesCount;
|
export let notesCount = $i == null ? 0 : $i.notesCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the number of notes by one.
|
||||||
|
*
|
||||||
|
* Documentation TODO: What about $i.notesCount? Why not increment that?
|
||||||
|
*/
|
||||||
export function incNotesCount() {
|
export function incNotesCount() {
|
||||||
notesCount++;
|
notesCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function signout() {
|
export async function signout() {
|
||||||
if (!$i) return;
|
|
||||||
|
// If we're not signed in, there's nothing to do.
|
||||||
|
if (!$i) {
|
||||||
|
// Error log:
|
||||||
|
console.error('signout() called when not signed in');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
waiting();
|
waiting();
|
||||||
miLocalStorage.removeItem('account');
|
miLocalStorage.removeItem('account');
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { updateI18n, i18n } from '@/i18n.js';
|
||||||
import { $i, refreshAccount, login } from '@/account.js';
|
import { $i, refreshAccount, login } from '@/account.js';
|
||||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||||
import { fetchInstance, instance } from '@/instance.js';
|
import { fetchInstance, instance } from '@/instance.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
|
||||||
import { reloadChannel } from '@/scripts/unison-reload.js';
|
import { reloadChannel } from '@/scripts/unison-reload.js';
|
||||||
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
|
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
|
||||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||||
|
@ -185,6 +185,10 @@ export async function common(createVue: () => App<Element>) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
|
||||||
|
updateDeviceKind(kind);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
||||||
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
|
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
|
@ -118,7 +118,7 @@ import { hms } from '@/filters/hms.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
|
import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js';
|
||||||
import hasAudio from '@/scripts/media-has-audio.js';
|
import hasAudio from '@/scripts/media-has-audio.js';
|
||||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||||
import { $i, iAmModerator } from '@/account.js';
|
import { $i, iAmModerator } from '@/account.js';
|
||||||
|
@ -334,26 +334,21 @@ function togglePlayPause() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullscreen() {
|
function toggleFullscreen() {
|
||||||
if (isFullscreenNotSupported && videoEl.value) {
|
if (playerEl.value == null || videoEl.value == null) return;
|
||||||
if (isFullscreen.value) {
|
if (isFullscreen.value) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
exitFullscreen({
|
||||||
//@ts-ignore
|
videoEl: videoEl.value,
|
||||||
videoEl.value.webkitExitFullscreen();
|
});
|
||||||
isFullscreen.value = false;
|
isFullscreen.value = false;
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
requestFullscreen({
|
||||||
//@ts-ignore
|
videoEl: videoEl.value,
|
||||||
videoEl.value.webkitEnterFullscreen();
|
playerEl: playerEl.value,
|
||||||
isFullscreen.value = true;
|
options: {
|
||||||
}
|
navigationUI: 'hide',
|
||||||
} else if (playerEl.value) {
|
},
|
||||||
if (isFullscreen.value) {
|
});
|
||||||
document.exitFullscreen();
|
isFullscreen.value = true;
|
||||||
isFullscreen.value = false;
|
|
||||||
} else {
|
|
||||||
playerEl.value.requestFullscreen({ navigationUI: 'hide' });
|
|
||||||
isFullscreen.value = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -454,8 +449,10 @@ watch(loop, (to) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(hide, (to) => {
|
watch(hide, (to) => {
|
||||||
if (to && isFullscreen.value) {
|
if (videoEl.value && to && isFullscreen.value) {
|
||||||
document.exitFullscreen();
|
exitFullscreen({
|
||||||
|
videoEl: videoEl.value,
|
||||||
|
});
|
||||||
isFullscreen.value = false;
|
isFullscreen.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -277,7 +277,7 @@ async function onSubmit(): Promise<void> {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res && res.ok) {
|
||||||
if (res.status === 204 || instance.emailRequiredForSignup) {
|
if (res.status === 204 || instance.emailRequiredForSignup) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
@ -295,6 +295,8 @@ async function onSubmit(): Promise<void> {
|
||||||
await login(resJson.token);
|
await login(resJson.token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
onSignupApiError();
|
||||||
}
|
}
|
||||||
|
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A typesafe enum of keys for localStorage.
|
||||||
|
*/
|
||||||
export type Keys =
|
export type Keys =
|
||||||
'v' |
|
'v' |
|
||||||
'lastVersion' |
|
'lastVersion' |
|
||||||
|
@ -44,16 +47,45 @@ export type Keys =
|
||||||
// セッション毎に廃棄されるLocalStorage代替(セーフモードなどで使用できそう)
|
// セッション毎に廃棄されるLocalStorage代替(セーフモードなどで使用できそう)
|
||||||
//const safeSessionStorage = new Map<Keys, string>();
|
//const safeSessionStorage = new Map<Keys, string>();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility object for interacting with the browser's localStorage.
|
||||||
|
*
|
||||||
|
* It's mostly a small wrapper around window.localStorage, but it validates
|
||||||
|
* keys with a typesafe enum, and provides a few convenience methods for JSON.
|
||||||
|
*/
|
||||||
export const miLocalStorage = {
|
export const miLocalStorage = {
|
||||||
|
/**
|
||||||
|
* Retrieves an item from localStorage.
|
||||||
|
* @param {Keys} key - The key of the item to retrieve.
|
||||||
|
* @returns {string | null} The value of the item, or null if the item does not exist.
|
||||||
|
*/
|
||||||
getItem: (key: Keys): string | null => {
|
getItem: (key: Keys): string | null => {
|
||||||
return window.localStorage.getItem(key);
|
return window.localStorage.getItem(key);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores an item in localStorage.
|
||||||
|
* @param {Keys} key - The key of the item to store.
|
||||||
|
* @param {string} value - The value of the item to store.
|
||||||
|
*/
|
||||||
setItem: (key: Keys, value: string): void => {
|
setItem: (key: Keys, value: string): void => {
|
||||||
window.localStorage.setItem(key, value);
|
window.localStorage.setItem(key, value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an item from localStorage.
|
||||||
|
* @param {Keys} key - The key of the item to remove.
|
||||||
|
*/
|
||||||
removeItem: (key: Keys): void => {
|
removeItem: (key: Keys): void => {
|
||||||
window.localStorage.removeItem(key);
|
window.localStorage.removeItem(key);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an item from localStorage and parses it as JSON.
|
||||||
|
* @param {Keys} key - The key of the item to retrieve.
|
||||||
|
* @returns {any | undefined} The parsed value of the item, or undefined if the item does not exist.
|
||||||
|
*/
|
||||||
getItemAsJson: (key: Keys): any | undefined => {
|
getItemAsJson: (key: Keys): any | undefined => {
|
||||||
const item = miLocalStorage.getItem(key);
|
const item = miLocalStorage.getItem(key);
|
||||||
if (item === null) {
|
if (item === null) {
|
||||||
|
@ -61,6 +93,12 @@ export const miLocalStorage = {
|
||||||
}
|
}
|
||||||
return JSON.parse(item);
|
return JSON.parse(item);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores an item in localStorage as a JSON string.
|
||||||
|
* @param {Keys} key - The key of the item to store.
|
||||||
|
* @param {any} value - The value of the item to store.
|
||||||
|
*/
|
||||||
setItemAsJson: (key: Keys, value: any): void => {
|
setItemAsJson: (key: Keys, value: any): void => {
|
||||||
miLocalStorage.setItem(key, JSON.stringify(value));
|
miLocalStorage.setItem(key, JSON.stringify(value));
|
||||||
},
|
},
|
||||||
|
|
|
@ -136,7 +136,16 @@ export function promiseDialog<T extends Promise<any>>(
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counter for generating unique popup IDs.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
let popupIdCount = 0;
|
let popupIdCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reactive list of the currently opened popups. This is used in a Vue component
|
||||||
|
* in a v-for loop to render the popups.
|
||||||
|
*/
|
||||||
export const popups = ref<{
|
export const popups = ref<{
|
||||||
id: number;
|
id: number;
|
||||||
component: Component;
|
component: Component;
|
||||||
|
@ -144,12 +153,23 @@ export const popups = ref<{
|
||||||
events: Record<string, any>;
|
events: Record<string, any>;
|
||||||
}[]>([]);
|
}[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing z-index values for different priority levels.
|
||||||
|
*/
|
||||||
const zIndexes = {
|
const zIndexes = {
|
||||||
veryLow: 500000,
|
veryLow: 500000,
|
||||||
low: 1000000,
|
low: 1000000,
|
||||||
middle: 2000000,
|
middle: 2000000,
|
||||||
high: 3000000,
|
high: 3000000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims a z-index value for a given priority level.
|
||||||
|
* Increments the z-index value for the specified priority by 100 and returns the new value.
|
||||||
|
*
|
||||||
|
* @param {keyof typeof zIndexes} [priority='low'] - The priority level for which to claim a z-index.
|
||||||
|
* @returns {number} The new z-index value for the specified priority.
|
||||||
|
*/
|
||||||
export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
|
export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
|
||||||
zIndexes[priority] += 100;
|
zIndexes[priority] += 100;
|
||||||
return zIndexes[priority];
|
return zIndexes[priority];
|
||||||
|
@ -177,6 +197,15 @@ type EmitsExtractor<T> = {
|
||||||
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
|
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a popup with the specified component, props, and events.
|
||||||
|
*
|
||||||
|
* @template T - The type of the component.
|
||||||
|
* @param {T} component - The Vue component to display in the popup.
|
||||||
|
* @param {ComponentProps<T>} props - The props to pass to the component.
|
||||||
|
* @param {ComponentEmit<T>} [events={}] - The events to bind to the component.
|
||||||
|
* @returns {{ dispose: () => void }} An object containing a dispose function to close the popup.
|
||||||
|
*/
|
||||||
export function popup<T extends Component>(
|
export function popup<T extends Component>(
|
||||||
component: T,
|
component: T,
|
||||||
props: ComponentProps<T>,
|
props: ComponentProps<T>,
|
||||||
|
@ -184,13 +213,18 @@ export function popup<T extends Component>(
|
||||||
): { dispose: () => void } {
|
): { dispose: () => void } {
|
||||||
markRaw(component);
|
markRaw(component);
|
||||||
|
|
||||||
|
// Generate a unique ID for this popup.
|
||||||
const id = ++popupIdCount;
|
const id = ++popupIdCount;
|
||||||
|
|
||||||
|
// On disposal, remove this popup from the list of open popups.
|
||||||
const dispose = () => {
|
const dispose = () => {
|
||||||
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
|
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
popups.value = popups.value.filter(p => p.id !== id);
|
popups.value = popups.value.filter(p => p.id !== id);
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bundle the component, props, and events into a state object.
|
||||||
const state = {
|
const state = {
|
||||||
component,
|
component,
|
||||||
props,
|
props,
|
||||||
|
@ -198,13 +232,19 @@ export function popup<T extends Component>(
|
||||||
id,
|
id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add the popup to the list of open popups.
|
||||||
popups.value.push(state);
|
popups.value.push(state);
|
||||||
|
|
||||||
|
// Return a function that can be called to close the popup.
|
||||||
return {
|
return {
|
||||||
dispose,
|
dispose,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the page with the given path in a pop-up window.
|
||||||
|
* @param path The path of the page to open.
|
||||||
|
*/
|
||||||
export function pageWindow(path: string) {
|
export function pageWindow(path: string) {
|
||||||
const { dispose } = popup(MkPageWindow, {
|
const { dispose } = popup(MkPageWindow, {
|
||||||
initialPath: path,
|
initialPath: path,
|
||||||
|
@ -213,6 +253,11 @@ export function pageWindow(path: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a toast message to the user.
|
||||||
|
*
|
||||||
|
* @param {string} message - The message to display in the toast.
|
||||||
|
*/
|
||||||
export function toast(message: string) {
|
export function toast(message: string) {
|
||||||
const { dispose } = popup(MkToast, {
|
const { dispose } = popup(MkToast, {
|
||||||
message,
|
message,
|
||||||
|
@ -221,6 +266,15 @@ export function toast(message: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an alert dialog to the user.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The properties for the alert dialog.
|
||||||
|
* @param {'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'} [props.type] - The type of the alert.
|
||||||
|
* @param {string} [props.title] - The title of the alert dialog.
|
||||||
|
* @param {string} [props.text] - The text content of the alert dialog.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the alert dialog is closed.
|
||||||
|
*/
|
||||||
export function alert(props: {
|
export function alert(props: {
|
||||||
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
|
@ -272,6 +272,9 @@ const patronsWithIcon = [{
|
||||||
}, {
|
}, {
|
||||||
name: 'Yatoigawa',
|
name: 'Yatoigawa',
|
||||||
icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
|
icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
|
||||||
|
}, {
|
||||||
|
name: '秋瀬カヲル',
|
||||||
|
icon: 'https://assets.misskey-hub.net/patrons/0f22aeb866484f4fa51db6721e3f9847.jpg',
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const patrons = [
|
const patrons = [
|
||||||
|
@ -380,6 +383,7 @@ const patrons = [
|
||||||
'ケモナーのケシン',
|
'ケモナーのケシン',
|
||||||
'こまつぶり',
|
'こまつぶり',
|
||||||
'まゆつな空高',
|
'まゆつな空高',
|
||||||
|
'asata',
|
||||||
];
|
];
|
||||||
|
|
||||||
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||||
|
|
|
@ -103,7 +103,7 @@ const headerActions = computed(() => []);
|
||||||
const headerTabs = computed(() => []);
|
const headerTabs = computed(() => []);
|
||||||
|
|
||||||
definePageMetadata(() => ({
|
definePageMetadata(() => ({
|
||||||
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
|
title: announcement.value ? announcement.value.title : i18n.ts.announcements,
|
||||||
icon: 'ti ti-speakerphone',
|
icon: 'ti ti-speakerphone',
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -62,7 +62,7 @@ function accepted() {
|
||||||
state.value = 'accepted';
|
state.value = 'accepted';
|
||||||
if (session.value && session.value.app.callbackUrl) {
|
if (session.value && session.value.app.callbackUrl) {
|
||||||
const url = new URL(session.value.app.callbackUrl);
|
const url = new URL(session.value.app.callbackUrl);
|
||||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
|
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(url.protocol)) throw new Error('invalid url');
|
||||||
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
|
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ async function onAccept(token: string) {
|
||||||
|
|
||||||
if (props.callback && props.callback !== '') {
|
if (props.callback && props.callback !== '') {
|
||||||
const cbUrl = new URL(props.callback);
|
const cbUrl = new URL(props.callback);
|
||||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||||
cbUrl.searchParams.set('session', props.session);
|
cbUrl.searchParams.set('session', props.session);
|
||||||
location.href = cbUrl.toString();
|
location.href = cbUrl.toString();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
|
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.switchBox">
|
<div :class="$style.switchBox">
|
||||||
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
<MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
|
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.switchBox">
|
<div :class="$style.switchBox">
|
||||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
|
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
|
||||||
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
|
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
|
||||||
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
|
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
|
||||||
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
<MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||||
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
|
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
|
@ -3,22 +3,22 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defaultStore } from '@/store.js';
|
export type DeviceKind = 'smartphone' | 'tablet' | 'desktop';
|
||||||
|
|
||||||
await defaultStore.ready;
|
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
|
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
|
||||||
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
|
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
|
||||||
|
|
||||||
const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
|
export const DEFAULT_DEVICE_KIND: DeviceKind = (
|
||||||
// navigator.platform may be deprecated but this check is still required
|
isSmartphone
|
||||||
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
|
? 'smartphone'
|
||||||
const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
|
: isTablet
|
||||||
|
? 'tablet'
|
||||||
|
: 'desktop'
|
||||||
|
);
|
||||||
|
|
||||||
export const isFullscreenNotSupported = isIPhone || isIos;
|
export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND;
|
||||||
|
|
||||||
export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind
|
export function updateDeviceKind(kind: DeviceKind | null) {
|
||||||
: isSmartphone ? 'smartphone'
|
deviceKind = kind ?? DEFAULT_DEVICE_KIND;
|
||||||
: isTablet ? 'tablet'
|
}
|
||||||
: 'desktop';
|
|
||||||
|
|
46
packages/frontend/src/scripts/fullscreen.ts
Normal file
46
packages/frontend/src/scripts/fullscreen.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||||
|
|
||||||
|
type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & {
|
||||||
|
webkitEnterFullscreen?(): void;
|
||||||
|
webkitExitFullscreen?(): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>;
|
||||||
|
|
||||||
|
type RequestFullscreenProps = {
|
||||||
|
readonly videoEl: VideoEl;
|
||||||
|
readonly playerEl: PlayerEl;
|
||||||
|
readonly options?: FullscreenOptions | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExitFullscreenProps = {
|
||||||
|
readonly videoEl: VideoEl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => {
|
||||||
|
if (playerEl.requestFullscreen != null) {
|
||||||
|
playerEl.requestFullscreen(options ?? undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (videoEl.webkitEnterFullscreen != null) {
|
||||||
|
videoEl.webkitEnterFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (document.exitFullscreen != null) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (videoEl.webkitExitFullscreen != null) {
|
||||||
|
videoEl.webkitExitFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
|
@ -26,6 +26,7 @@ if (window.Cypress) {
|
||||||
console.log('Cypress detected. It will use localStorage.');
|
console.log('Cypress detected. It will use localStorage.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for the availability of indexedDB.
|
||||||
if (idbAvailable) {
|
if (idbAvailable) {
|
||||||
await iset('idb-test', 'test')
|
await iset('idb-test', 'test')
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
@ -37,16 +38,36 @@ if (idbAvailable) {
|
||||||
console.error('indexedDB is unavailable. It will use localStorage.');
|
console.error('indexedDB is unavailable. It will use localStorage.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value from indexedDB (or localStorage as a fallback).
|
||||||
|
*
|
||||||
|
* @param key The key of the item to retrieve.
|
||||||
|
*
|
||||||
|
* @returns The value of the item.
|
||||||
|
*/
|
||||||
export async function get(key: string) {
|
export async function get(key: string) {
|
||||||
if (idbAvailable) return iget(key);
|
if (idbAvailable) return iget(key);
|
||||||
return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
|
return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a value in indexedDB (or localStorage as a fallback).
|
||||||
|
*
|
||||||
|
* @param {string} key - The key of the item to set.
|
||||||
|
* @param {any} val - The value of the item to set.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the value has been set.`
|
||||||
|
*/
|
||||||
export async function set(key: string, val: any) {
|
export async function set(key: string, val: any) {
|
||||||
if (idbAvailable) return iset(key, val);
|
if (idbAvailable) return iset(key, val);
|
||||||
return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
|
return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a value from indexedDB (or localStorage as a fallback).
|
||||||
|
*
|
||||||
|
* @param {string} key - The key of the item to delete.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the value has been deleted.
|
||||||
|
*/
|
||||||
export async function del(key: string) {
|
export async function del(key: string) {
|
||||||
if (idbAvailable) return idel(key);
|
if (idbAvailable) return idel(key);
|
||||||
return miLocalStorage.removeItem(`${PREFIX}${key}`);
|
return miLocalStorage.removeItem(`${PREFIX}${key}`);
|
||||||
|
|
|
@ -8,8 +8,9 @@ import * as Misskey from 'misskey-js';
|
||||||
import { hemisphere } from '@@/js/intl-const.js';
|
import { hemisphere } from '@@/js/intl-const.js';
|
||||||
import lightTheme from '@@/themes/l-light.json5';
|
import lightTheme from '@@/themes/l-light.json5';
|
||||||
import darkTheme from '@@/themes/d-green-lime.json5';
|
import darkTheme from '@@/themes/d-green-lime.json5';
|
||||||
import { miLocalStorage } from './local-storage.js';
|
|
||||||
import type { SoundType } from '@/scripts/sound.js';
|
import type { SoundType } from '@/scripts/sound.js';
|
||||||
|
import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { Storage } from '@/pizzax.js';
|
import { Storage } from '@/pizzax.js';
|
||||||
import type { Ast } from '@syuilo/aiscript';
|
import type { Ast } from '@syuilo/aiscript';
|
||||||
|
|
||||||
|
@ -207,7 +208,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
|
|
||||||
overridedDeviceKind: {
|
overridedDeviceKind: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: null as null | 'smartphone' | 'tablet' | 'desktop',
|
default: null as DeviceKind | null,
|
||||||
},
|
},
|
||||||
serverDisconnectedBehavior: {
|
serverDisconnectedBehavior: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
@ -263,11 +264,11 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
useBlurEffectForModal: {
|
useBlurEffectForModal: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない
|
default: DEFAULT_DEVICE_KIND === 'desktop',
|
||||||
},
|
},
|
||||||
useBlurEffect: {
|
useBlurEffect: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない
|
default: DEFAULT_DEVICE_KIND === 'desktop',
|
||||||
},
|
},
|
||||||
showFixedPostForm: {
|
showFixedPostForm: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2024.10.2-alpha.2",
|
"version": "2024.11.0-alpha.0",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
|
Loading…
Reference in a new issue