1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2025-01-24 10:43:58 +09:00

Merge pull request #555 from kokonect-link/develop

Release: 4.14.1
This commit is contained in:
NoriDev 2025-01-02 20:28:40 +09:00 committed by GitHub
commit 17a0eeaf67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
66 changed files with 879 additions and 169 deletions

View File

@ -15,6 +15,8 @@ on:
jobs:
build:
# chromatic is not likely to be available for fork repositories, so we disable for fork repositories.
if: github.repository == 'kokonect-link/cherrypick'
runs-on: ubuntu-latest
env:

View File

@ -1,3 +1,25 @@
## 2024.11.1
### General
-
### Client
- Enhance: PC画面でチャンネルが複数列で表示されるように
(Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
- Enhance: 照会に失敗した場合、その理由を表示するように
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
- Fix: サーバー情報メニューに区切り線が不足していたのを修正
- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正
- Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803)
- Fix: 絵文字管理画面で一部の絵文字が表示されない問題を修正
### Server
- Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 )
- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737)
## 2024.11.0
### Note
@ -37,7 +59,7 @@
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
- Fix: リンク切れを修正
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
- Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
- Fix: 画面幅が狭い環境でデザインが崩れる問題を修正

View File

@ -23,6 +23,30 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE
# 릴리스 노트
이 문서는 CherryPick의 변경 사항만 포함합니다.
## 4.14.1
출시일: 2025/1/2<br>
기반 Misskey 버전: 2024.11.1-alpha.0<br>
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024111](CHANGELOG.md#2024111) 문서를 참고하십시오.
### General
- Feat: 번역 서비스 추가 지원 (kokonect-link/cherrypick#551)
- [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) 지원 추가
- 외부 서비스를 이용하는 번역 서비스는 요금제에 따른 API 제한이 적용되므로, 자체 호스팅으로 구동할 수 있는 번역 API를 사용할 수 있음
- Feat: 검색 엔진 사용자화 (kokonect-link/cherrypick#554)
- MFC 구문 중 `[검색]`, `[search]`, `[検索]`를 사용했을 때, 사용할 검색 엔진을 지정할 수 있음
- Enhance: 예약된 노트 게시에 실패할 경우 사용자에게 알림 ([penginn-net/kokonect@a0e47980](https://github.com/penginn-net/kokonect/commit/a0e47980470b49e79e84ff3b7ccaf2b4502928c8))
### Client
- Fix: 노트 상세 페이지에서 사용자 이름이 중복으로 표시될 수 있음
- Fix: 커스텀 이모지 관리 역할이 있는 사용자가 원격 서버의 이모지를 가져올 수 없음
- Fix: 신고 알림을 받을 이메일 주소 설정을 저장할 수 없음 (kokonect-link/cherrypick#540)
- Fix: 네비게이션 바에 배너 이미지가 표시되지 않을 수 있음 (kokonect-link/cherrypick#545)
- Fix: 고정된 리스트 타임라인을 사용할 수 없음 (kokonect-link/cherrypick#546)
- Fix: 버블 타임라인이 비활성화 상태면 버블 타임라인의 호스트 목록을 수정할 수 없음 (kokonect-link/cherrypick#544)
- Fix: 특정 환경에서 리액션을 변경할 수 없을 수 있음 (kokonect-link/cherrypick#549)
---
## 4.14.0
출시일: 2024/11/26<br>
기반 Misskey 버전: 2024.11.0<br>

View File

@ -38,3 +38,9 @@ services:
# volumes:
# - ./meili_data:/meili_data
# translator:
# restart: always
# image: libretranslate/libretranslate:latest
# healthcheck:
# test: ['CMD-SHELL', './venv/bin/python scripts/healthcheck.py']

View File

@ -92,6 +92,12 @@ services:
# volumes:
# - ./meili_data:/meili_data
# translator:
# restart: always
# image: libretranslate/libretranslate:latest
# healthcheck:
# test: ['CMD-SHELL', './venv/bin/python scripts/healthcheck.py']
networks:
internal_network:
internal: true

View File

@ -2820,12 +2820,19 @@ _notification:
achievementEarned: "Achievement unlocked"
exportCompleted: "The export has been completed"
login: "Sign In"
scheduleNote: "Scheduled note posting failed"
test: "Notification test"
app: "Notifications from linked apps"
_actions:
followBack: "followed you back"
reply: "Reply"
renote: "Renote"
_scheduleNote:
unknown: "The cause is unknown"
renoteTargetNotFound: "Renote target note not found"
channelTargetNotFound: "Channel not found"
replyTargetNotFound: "Reply target note not found"
invalidFilesCount: "No attachments"
_deck:
alwaysShowMainColumn: "Always show main column"
columnAlign: "Align columns"
@ -3163,3 +3170,10 @@ _scheduledNoteDelete:
_getQRCode:
title: "Scan QR Code"
description: "Can scan or share the QR code below."
_searchSite:
title: "Search Engine"
description: "Change search engine that used in search MFC."
otherSearchEngine: "Other search engines"
otherDescription: "Use other search engine"
query: "Query"
queryDescription: "Input query scheme for search engine. For example, if https://www.google.com/search?q=test, input 'q'."

111
locales/index.d.ts vendored
View File

@ -10989,6 +10989,10 @@ export interface Locale extends ILocale {
*
*/
"login": string;
/**
* 稿
*/
"scheduleNote": string;
/**
*
*/
@ -11012,6 +11016,28 @@ export interface Locale extends ILocale {
*/
"renote": string;
};
"_scheduleNote": {
/**
*
*/
"unknown": string;
/**
*
*/
"renoteTargetNotFound": string;
/**
*
*/
"channelTargetNotFound": string;
/**
*
*/
"replyTargetNotFound": string;
/**
*
*/
"invalidFilesCount": string;
};
};
"_deck": {
/**
@ -12089,6 +12115,65 @@ export interface Locale extends ILocale {
*/
"sent": string;
};
"_remoteLookupErrors": {
"_federationNotAllowed": {
/**
*
*/
"title": string;
/**
*
*
*/
"description": string;
};
"_uriInvalid": {
/**
* URIが不正です
*/
"title": string;
/**
* URIに問題がありますURIに使用できない文字を入力していないか確認してください
*/
"description": string;
};
"_requestFailed": {
/**
*
*/
"title": string;
/**
* URIや存在しないURIを入力していないか確認してください
*/
"description": string;
};
"_responseInvalid": {
/**
*
*/
"title": string;
/**
*
*/
"description": string;
};
"_responseInvalidIdHostNotMatch": {
/**
* URIのドメインと最終的に得られたURIのドメインとが異なりますURIを使用して照会し直してください
*/
"description": string;
};
"_noSuchObject": {
/**
*
*/
"title": string;
/**
* URIをもう一度お確かめください
*/
"description": string;
};
};
"_abuse": {
"_resolver": {
/**
@ -12277,6 +12362,32 @@ export interface Locale extends ILocale {
*/
"description": string;
};
"_searchSite": {
/**
*
*/
"title": string;
/**
* MFCの検索構文で検索できるサイトを変更します
*/
"description": string;
/**
*
*/
"otherSearchEngine": string;
/**
* 使
*/
"otherDescription": string;
/**
*
*/
"query": string;
/**
* 使(: https://www.google.com/search?q=test の場合qを入れる)
*/
"queryDescription": string;
};
}
declare const locales: {
[lang: string]: Locale;

View File

@ -2895,6 +2895,7 @@ _notification:
achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した"
login: "ログイン"
scheduleNote: "予約投稿に失敗"
test: "通知のテスト"
app: "連携アプリからの通知"
@ -2903,6 +2904,13 @@ _notification:
reply: "返信"
renote: "リノート"
_scheduleNote:
unknown: "原因は不明です"
renoteTargetNotFound: "引用元がありません"
channelTargetNotFound: "対象のチャンネルがありません"
replyTargetNotFound: "返信先がありません"
invalidFilesCount: "添付ファイルがありません"
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"
@ -3213,6 +3221,25 @@ _followRequest:
recieved: "受け取った申請"
sent: "送った申請"
_remoteLookupErrors:
_federationNotAllowed:
title: "このサーバーとは通信できません"
description: "このサーバーとの通信が無効化されているか、このサーバーをブロックしている・ブロックされている可能性があります。\nサーバー管理者にお問い合わせください。"
_uriInvalid:
title: "URIが不正です"
description: "入力されたURIに問題があります。URIに使用できない文字を入力していないか確認してください。"
_requestFailed:
title: "リクエストに失敗しました"
description: "このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。"
_responseInvalid:
title: "レスポンスが不正です"
description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。"
_responseInvalidIdHostNotMatch:
description: "入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。"
_noSuchObject:
title: "見つかりません"
description: "要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。"
_abuse:
_resolver:
1hour: "一時間"
@ -3270,3 +3297,11 @@ _scheduledNoteDelete:
_getQRCode:
title: "QRコードをスキャンする"
description: "以下のQRコードをスキャンまたは共有できます。"
_searchSite:
title: "検索エンジン"
description: "MFCの検索構文で検索できるサイトを変更します。"
otherSearchEngine: "その他の検索エンジン"
otherDescription: "その他の検索エンジンを使用します。"
query: "検索クエリ"
queryDescription: "検索エンジンが使用するクエリを入力します。(例: https://www.google.com/search?q=test の場合qを入れる)"

View File

@ -2823,12 +2823,19 @@ _notification:
achievementEarned: "도전 과제 획득"
exportCompleted: "내보내기를 완료함"
login: "로그인"
scheduleNote: "게시가 예약된 노트의 게시가 실패함"
test: "알림 테스트"
app: "연동된 앱을 통한 알림"
_actions:
followBack: "팔로우"
reply: "답글"
renote: "리노트"
_scheduleNote:
unknown: "알 수 없는 오류가 발생했어요"
renoteTargetNotFound: "인용할 대상이 없어요"
channelTargetNotFound: "해당 채널이 존재하지 않아요"
replyTargetNotFound: "답장할 대상이 없어요"
invalidFilesCount: "첨부 파일이 없어요"
_deck:
alwaysShowMainColumn: "메인 칼럼 항상 표시"
columnAlign: "칼럼 정렬"
@ -3170,3 +3177,10 @@ _scheduledNoteDelete:
_getQRCode:
title: "QR 코드 스캔하기"
description: "아래 QR 코드를 스캔하거나 공유할 수 있어요."
_searchSite:
title: "검색 엔진"
description: "MFC의 검색 구문을 사용했을 때 사용되는 검색 엔진을 변경해요."
otherSearchEngine: "사용자 지정 검색 엔진"
otherDescription: "검색 엔진을 직접 지정할 수 있어요."
query: "검색 쿼리"
queryDescription: "검색 엔진이 사용할 쿼리를 입력해 주세요. (예: https://www.google.com/search?q=test 의 경우 q를 입력)"

View File

@ -1,7 +1,7 @@
{
"name": "cherrypick",
"version": "4.14.0",
"basedMisskeyVersion": "2024.11.0",
"version": "4.14.1",
"basedMisskeyVersion": "2024.11.1-alpha.0",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class LibreTranslate1734793052000 {
name = 'LibreTranslate1734793052000'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "libreTranslateEndPoint" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "libreTranslateApiKey" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "libreTranslateEndPoint"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "libreTranslateApiKey"`);
}
}

View File

@ -51,4 +51,4 @@ const promises = Array
connectToPostgres(),
]);
await Promise.allSettled(promises);
await Promise.all(promises);

View File

@ -53,7 +53,7 @@ export class S3Service {
? `${ objectStorageUseSSL ? 'https' : 'http' }://${ objectStorageEndpoint }`
: `${ objectStorageUseSSL ? 'https' : 'http' }://example.com`; // dummy url to select http(s) agent
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !objectStorageUseSSL);
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !objectStorageUseSSL, true);
const handlerOption: NodeHttpHandlerOptions = {};
if (objectStorageUseSSL) {
handlerOption.httpsAgent = agent as https.Agent;

View File

@ -189,14 +189,12 @@ export class WebAuthnService {
*/
@bindThis
public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> {
const challenge = await this.redisClient.get(`webauthn:challenge:${context}`);
const challenge = await this.redisClient.getdel(`webauthn:challenge:${context}`);
if (!challenge) {
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`);
}
await this.redisClient.del(`webauthn:challenge:${context}`);
const key = await this.userSecurityKeysRepository.findOneBy({
id: response.id,
});

View File

@ -15,6 +15,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
@ -66,7 +67,7 @@ export class Resolver {
if (isCollectionOrOrderedCollection(collection)) {
return collection;
} else {
throw new Error(`unrecognized collection type: ${collection.type}`);
throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `unrecognized collection type: ${collection.type}`);
}
}
@ -80,15 +81,15 @@ export class Resolver {
// URLs with fragment parts cannot be resolved correctly because
// the fragment part does not get transmitted over HTTP(S).
// Avoid strange behaviour by not trying to resolve these at all.
throw new Error(`cannot resolve URL with fragment: ${value}`);
throw new IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `cannot resolve URL with fragment: ${value}`);
}
if (this.history.has(value)) {
throw new Error('cannot resolve already resolved one');
throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', 'cannot resolve already resolved one');
}
if (this.history.size > this.recursionLimit) {
throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
}
this.history.add(value);
@ -99,7 +100,7 @@ export class Resolver {
}
if (!this.utilityService.isFederationAllowedHost(host)) {
throw new Error('Instance is blocked');
throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', 'Instance is blocked');
}
if (this.config.signToActivityPubGet && !this.user) {
@ -115,7 +116,7 @@ export class Resolver {
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
) {
throw new Error('invalid response');
throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response');
}
// HttpRequestService / ApRequestService have already checked that
@ -123,11 +124,11 @@ export class Resolver {
// object after redirects; here we double-check that no redirects
// bounced between hosts
if (object.id == null) {
throw new Error('invalid AP object: missing id');
throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', 'invalid AP object: missing id');
}
if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) {
throw new Error(`invalid AP object ${value}: id ${object.id} has different host`);
throw new IdentifiableError('fd93c2fa-69a8-440f-880b-bf178e0ec877', `invalid AP object ${value}: id ${object.id} has different host`);
}
return object;
@ -136,7 +137,7 @@ export class Resolver {
@bindThis
private resolveLocal(url: string): Promise<IObject> {
const parsed = this.apDbResolverService.parseUri(url);
if (!parsed.local) throw new Error('resolveLocal: not local');
if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', 'resolveLocal: not local');
switch (parsed.type) {
case 'notes':
@ -165,7 +166,7 @@ export class Resolver {
case 'follows':
return this.followRequestsRepository.findOneBy({ id: parsed.id })
.then(async followRequest => {
if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID');
if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', 'resolveLocal: invalid follow request ID');
const [follower, followee] = await Promise.all([
this.usersRepository.findOneBy({
id: followRequest.followerId,
@ -177,12 +178,12 @@ export class Resolver {
}),
]);
if (follower == null || followee == null) {
throw new Error('resolveLocal: follower or followee does not exist');
throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', 'resolveLocal: follower or followee does not exist');
}
return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
});
default:
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `resolveLocal: type ${parsed.type} unhandled`);
}
}
}

View File

@ -202,6 +202,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'login' ? {
ip: notification.userIp,
} : {}),
...(notification.type === 'scheduleNote' ? {
errorType: notification.errorType,
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader,

View File

@ -401,6 +401,18 @@ export class MiMeta {
})
public ctav3Glossary: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public libreTranslateEndPoint: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public libreTranslateApiKey: string | null;
@Column('varchar', {
length: 1024,
nullable: true,

View File

@ -98,6 +98,11 @@ export type MiNotification = {
id: string;
createdAt: string;
userIp: string;
} | {
type: 'scheduleNote';
id: string;
createdAt: string;
errorType: string;
} | {
type: 'app';
id: string;

View File

@ -336,6 +336,20 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['scheduleNote'],
},
errorType: {
type: 'string',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {

View File

@ -9,6 +9,7 @@ import { bindThis } from '@/decorators.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import type { ChannelsRepository, DriveFilesRepository, MiDriveFile, NoteScheduleRepository, NotesRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { ScheduleNotePostJobData } from '../types.js';
@ -32,6 +33,7 @@ export class ScheduleNotePostProcessorService {
private noteCreateService: NoteCreateService,
private queueLoggerService: QueueLoggerService,
private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('schedule-note-post');
}
@ -72,6 +74,25 @@ export class ScheduleNotePostProcessorService {
//キューに積んだときは有った物が消滅してたら予約投稿をキャンセルする
this.logger.warn('cancel schedule note');
await this.noteScheduleRepository.remove(data);
if (data.userId && me) { //ユーザーが特定できる場合に失敗を通知
let errorType = 'unknown';
if (note.renote && !renote) {
errorType = 'renoteTargetNotFound';
}
if (note.reply && !reply) {
errorType = 'replyTargetNotFound';
}
if (note.channel && !channel) {
errorType = 'channelTargetNotFound';
}
if (note.files.length !== files.length) {
errorType = 'invalidFilesCount';
}
this.notificationService.createNotification(data.userId, 'scheduleNote', {
errorType,
});
}
return;
}
await this.noteCreateService.create(me, {

View File

@ -468,6 +468,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
libreTranslateEndPoint: {
type: 'string',
optional: false, nullable: true,
},
libreTranslateApiKey: {
type: 'string',
optional: false, nullable: true,
},
defaultDarkTheme: {
type: 'string',
optional: false, nullable: true,
@ -768,6 +776,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ctav3Location: instance.ctav3Location,
ctav3Model: instance.ctav3Model,
ctav3Glossary: instance.ctav3Glossary,
libreTranslateEndPoint: instance.libreTranslateEndPoint,
libreTranslateApiKey: instance.libreTranslateApiKey,
enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableVerifymailApi: instance.enableVerifymailApi,

View File

@ -107,6 +107,8 @@ export const paramDef = {
ctav3Location: { type: 'string', nullable: true },
ctav3Model: { type: 'string', nullable: true },
ctav3Glossary: { type: 'string', nullable: true },
libreTranslateEndPoint: { type: 'string', nullable: true },
libreTranslateApiKey: { type: 'string', nullable: true },
enableEmail: { type: 'boolean' },
email: { type: 'string', nullable: true },
smtpSecure: { type: 'boolean' },
@ -657,6 +659,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.ctav3Glossary = ps.ctav3Glossary;
}
if (ps.libreTranslateEndPoint !== undefined) {
set.libreTranslateEndPoint = ps.libreTranslateEndPoint;
}
if (ps.libreTranslateApiKey !== undefined) {
set.libreTranslateApiKey = ps.libreTranslateApiKey;
}
if (ps.enableIpLogging !== undefined) {
set.enableIpLogging = ps.enableIpLogging;
}

View File

@ -18,6 +18,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -32,6 +33,31 @@ export const meta = {
},
errors: {
federationNotAllowed: {
message: 'Federation for this host is not allowed.',
code: 'FEDERATION_NOT_ALLOWED',
id: '974b799e-1a29-4889-b706-18d4dd93e266',
},
uriInvalid: {
message: 'URI is invalid.',
code: 'URI_INVALID',
id: '1a5eab56-e47b-48c2-8d5e-217b897d70db',
},
requestFailed: {
message: 'Request failed.',
code: 'REQUEST_FAILED',
id: '81b539cf-4f57-4b29-bc98-032c33c0792e',
},
responseInvalid: {
message: 'Response from remote server is invalid.',
code: 'RESPONSE_INVALID',
id: '70193c39-54f3-4813-82f0-70a680f7495b',
},
responseInvalidIdHostNotMatch: {
message: 'Requested URI and response URI host does not match.',
code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH',
id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a',
},
noSuchObject: {
message: 'No such object.',
code: 'NO_SUCH_OBJECT',
@ -110,7 +136,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
*/
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
if (!this.utilityService.isFederationAllowedUri(uri)) return null;
if (!this.utilityService.isFederationAllowedUri(uri)) {
throw new ApiError(meta.errors.federationNotAllowed);
}
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
@ -125,7 +153,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri) as any;
const object = await resolver.resolve(uri).catch((err) => {
if (err instanceof IdentifiableError) {
switch (err.id) {
// resolve
case 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2':
throw new ApiError(meta.errors.uriInvalid);
case '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5':
case 'd592da9f-822f-4d91-83d7-4ceefabcf3d2':
throw new ApiError(meta.errors.requestFailed);
case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
throw new ApiError(meta.errors.federationNotAllowed);
case '72180409-793c-4973-868e-5a118eb5519b':
case 'ad2dc287-75c1-44c4-839d-3d2e64576675':
throw new ApiError(meta.errors.responseInvalid);
case 'fd93c2fa-69a8-440f-880b-bf178e0ec877':
throw new ApiError(meta.errors.responseInvalidIdHostNotMatch);
// resolveLocal
case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':
throw new ApiError(meta.errors.uriInvalid);
case 'a9d946e5-d276-47f8-95fb-f04230289bb0':
case '06ae3170-1796-4d93-a697-2611ea6d83b6':
throw new ApiError(meta.errors.noSuchObject);
case '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0':
throw new ApiError(meta.errors.responseInvalid);
}
}
throw new ApiError(meta.errors.requestFailed);
});
if (object.id == null) {
throw new ApiError(meta.errors.responseInvalid);
}
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
// これはDBに存在する可能性があるため再度DB検索

View File

@ -83,6 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
'deepl',
'google_no_api',
'ctav3',
'Libretranslate',
];
if (this.serverSettings.translatorType == null || !translatorServices.includes(this.serverSettings.translatorType)) {
@ -119,6 +120,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
else if (this.serverSettings.ctav3ProjectId == null) return Promise.resolve(204);
else if (this.serverSettings.ctav3Location == null) return Promise.resolve(204);
translationResult = await this.apiCloudTranslationAdvanced(poll.choices, targetLang, this.serverSettings.ctav3SaKey, this.serverSettings.ctav3ProjectId, this.serverSettings.ctav3Location, this.serverSettings.ctav3Model, this.serverSettings.ctav3Glossary, this.serverSettings.translatorType);
} else if (this.serverSettings.translatorType === 'Libretranslate') {
const endPoint = this.serverSettings.libreTranslateEndPoint;
if (endPoint === null) throw new Error('libreTranslateEndPoint is null');
translationResult = await this.translateLibretranslate(poll.choices, targetLang, endPoint, this.serverSettings.libreTranslateApiKey);
} else {
throw new Error('Unsupported translator type');
}
@ -217,4 +222,41 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
translator: provider,
};
}
private async translateLibretranslate(texts: string[], targetLang: string, endpoint: string, apiKey:string | null ) {
const translations = [];
const target = targetLang.split('-')[0];
for (const text of texts) {
const res = await this.httpRequestService.send(endpoint + '/translate', {
method: 'POST',
body: JSON.stringify({
q: text,
source: 'auto',
format: 'text',
target: target,
...(apiKey ? { api_key: apiKey } : { }),
}),
headers: { 'Content-Type': 'application/json' },
});
const json = (await res.json()) as {
translatedText: string,
detectedLanguage: {
confidence: number,
language: string,
}
error: string,
};
translations.push({
translatedText: json.translatedText || '',
sourceLang: json.detectedLanguage.language || '',
});
}
return {
sourceLang: translations[0]?.sourceLang || '',
text: translations.map(choice => choice.translatedText),
translator: 'Libretranslate',
};
}
}

View File

@ -100,6 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
'deepl',
'google_no_api',
'ctav3',
'Libretranslate',
];
if (this.serverSettings.translatorType == null || !translatorServices.includes(this.serverSettings.translatorType)) {
@ -131,6 +132,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
else if (this.serverSettings.ctav3ProjectId == null) return Promise.resolve(204);
else if (this.serverSettings.ctav3Location == null) return Promise.resolve(204);
translationResult = await this.apiCloudTranslationAdvanced((note.cw ? note.cw + '\n' : '') + note.text, targetLang, this.serverSettings.ctav3SaKey, this.serverSettings.ctav3ProjectId, this.serverSettings.ctav3Location, this.serverSettings.ctav3Model, this.serverSettings.ctav3Glossary, this.serverSettings.translatorType);
} else if (this.serverSettings.translatorType === 'Libretranslate') {
const endPoint = this.serverSettings.libreTranslateEndPoint;
if (endPoint === null) throw new Error('libreTranslateEndPoint is null');
translationResult = await this.translateLibretranslate((note.cw ? note.cw + '\n' : '') + note.text, targetLang, endPoint, this.serverSettings.libreTranslateApiKey);
} else {
throw new Error('Unsupported translator type');
}
@ -221,4 +226,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
translator: provider,
};
}
private async translateLibretranslate(text: string, targetLang: string, endpoint: string, apiKey:string | null ) {
const res = await this.httpRequestService.send(endpoint + '/translate', {
method: 'POST',
body: JSON.stringify({
q: text,
source: 'auto',
format: 'text',
target: targetLang.split('-')[0],
...(apiKey ? { api_key: apiKey } : { }),
}),
headers: { 'Content-Type': 'application/json' },
});
const json = (await res.json()) as {
translatedText: string,
detectedLanguage: {
confidence: number,
language: string,
}
error: string,
};
return {
sourceLang: json.detectedLanguage.language,
text: json.translatedText,
translator: 'Libretranslate',
};
}
}

View File

@ -89,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
'deepl',
'google_no_api',
'ctav3',
'Libretranslate',
];
if (this.serverSettings.translatorType == null || !translatorServices.includes(this.serverSettings.translatorType)) {
@ -120,6 +121,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
else if (this.serverSettings.ctav3ProjectId == null) return Promise.resolve(204);
else if (this.serverSettings.ctav3Location == null) return Promise.resolve(204);
translationResult = await this.apiCloudTranslationAdvanced(target.description, targetLang, this.serverSettings.ctav3SaKey, this.serverSettings.ctav3ProjectId, this.serverSettings.ctav3Location, this.serverSettings.ctav3Model, this.serverSettings.ctav3Glossary, this.serverSettings.translatorType);
} else if (this.serverSettings.translatorType === 'Libretranslate') {
const endPoint = this.serverSettings.libreTranslateEndPoint;
if (endPoint === null) throw new Error('libreTranslateEndPoint is null');
translationResult = await this.translateLibretranslate(target.description, targetLang, endPoint, this.serverSettings.libreTranslateApiKey);
} else {
throw new Error('Unsupported translator type');
}
@ -210,4 +215,33 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
translator: provider,
};
}
private async translateLibretranslate(text: string, targetLang: string, endpoint: string, apiKey:string | null ) {
const res = await this.httpRequestService.send(endpoint + '/translate', {
method: 'POST',
body: JSON.stringify({
q: text,
source: 'auto',
format: 'text',
target: targetLang.split('-')[0],
...(apiKey ? { api_key: apiKey } : { }),
}),
headers: { 'Content-Type': 'application/json' },
});
const json = (await res.json()) as {
translatedText: string,
detectedLanguage: {
confidence: number,
language: string,
}
error: string,
};
return {
sourceLang: json.detectedLanguage.language,
text: json.translatedText,
translator: 'Libretranslate',
};
}
}

View File

@ -183,7 +183,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
},
...(endpoint.meta.limit ? {
'429': {
description: 'To many requests',
description: 'Too many requests',
content: {
'application/json': {
schema: {

View File

@ -594,7 +594,10 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noai');
}
const _user = await this.userEntityService.pack(user);
const _user = await this.userEntityService.pack(user, null, {
schema: 'UserDetailed',
userProfile: profile,
});
return await reply.view('user', {
user, profile, me,
@ -877,7 +880,7 @@ export class ClientServerService {
});
if (note == null) return;
if (note.visibility !== 'public') return;
if (['specified', 'followers'].includes(note.visibility)) return;
if (note.userHost != null) return;
const _note = await this.noteEntityService.pack(note, null, { detail: true });

View File

@ -38,6 +38,7 @@ export const notificationTypes = [
'achievementEarned',
'exportCompleted',
'login',
'scheduleNote',
'app',
'test',
] as const;

View File

@ -131,11 +131,7 @@ describe('Note', () => {
rejects(
async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }),
(err: any) => {
/**
* FIXME: this error is not handled
* @see https://github.com/misskey-dev/misskey/issues/12736
*/
strictEqual(err.code, 'INTERNAL_ERROR');
strictEqual(err.code, 'REQUEST_FAILED');
return true;
},
);

View File

@ -3004,7 +3004,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
// @public (undocumented)
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"];
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "scheduleNote"];
// @public (undocumented)
export function nyaize(text: string): string;

View File

@ -1,8 +1,8 @@
{
"type": "module",
"name": "cherrypick-js",
"version": "4.14.0",
"basedMisskeyVersion": "2024.11.0",
"version": "4.14.1",
"basedMisskeyVersion": "2024.11.1-alpha.0",
"description": "CherryPick SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@ -44,7 +44,7 @@ export class APIClient {
credential?: APIClient['credential'];
fetch?: APIClient['fetch'] | null | undefined;
}) {
this.origin = opts.origin;
this.origin = opts.origin.replace(/\/$/, '');
this.credential = opts.credential;
// ネイティブ関数をそのまま変数に代入して使おうとするとChromiumではIllegal invocationエラーが発生するため、
// 環境で実装されているfetchを使う場合は無名関数でラップして使用する

View File

@ -4715,6 +4715,14 @@ export type components = {
/** @enum {string} */
type: 'login';
ip: string;
} | {
/** Format: id */
id: string;
/** Format: date-time */
createdAt: string;
/** @enum {string} */
type: 'scheduleNote';
errorType: string;
} | ({
/** Format: id */
id: string;
@ -5614,6 +5622,8 @@ export type operations = {
backgroundImageUrl: string | null;
deeplAuthKey: string | null;
deeplIsPro: boolean;
libreTranslateEndPoint: string | null;
libreTranslateApiKey: string | null;
defaultDarkTheme: string | null;
defaultLightTheme: string | null;
description: string | null;
@ -10393,6 +10403,8 @@ export type operations = {
ctav3Location?: string | null;
ctav3Model?: string | null;
ctav3Glossary?: string | null;
libreTranslateEndPoint?: string | null;
libreTranslateApiKey?: string | null;
enableEmail?: boolean;
email?: string | null;
smtpSecure?: boolean;
@ -11475,7 +11487,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -11998,7 +12010,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -12065,7 +12077,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -12459,7 +12471,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -12519,7 +12531,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -12642,7 +12654,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -14237,7 +14249,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -15070,7 +15082,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -15417,7 +15429,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -15542,7 +15554,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -16037,7 +16049,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -16510,7 +16522,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -16570,7 +16582,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -16633,7 +16645,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -16692,7 +16704,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -16752,7 +16764,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -17259,7 +17271,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -17534,7 +17546,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -18794,7 +18806,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -18855,7 +18867,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -18906,7 +18918,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -18957,7 +18969,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19008,7 +19020,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19059,7 +19071,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19110,7 +19122,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19161,7 +19173,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19398,7 +19410,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19458,7 +19470,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19517,7 +19529,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19576,7 +19588,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19635,7 +19647,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19667,8 +19679,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote')[];
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'scheduleNote' | 'app' | 'test' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'scheduleNote' | 'app' | 'test' | 'pollVote')[];
};
};
};
@ -19703,7 +19715,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -19735,8 +19747,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'scheduleNote' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'scheduleNote' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
};
};
};
@ -19771,7 +19783,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -20807,7 +20819,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -21055,7 +21067,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -21177,7 +21189,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -21547,7 +21559,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -21936,7 +21948,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -21994,7 +22006,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -22321,7 +22333,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -22489,7 +22501,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -22999,7 +23011,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -23057,7 +23069,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -23137,7 +23149,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -23195,7 +23207,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -24072,7 +24084,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -24294,7 +24306,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -24352,7 +24364,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -24437,7 +24449,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -24756,7 +24768,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -25002,7 +25014,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -25140,7 +25152,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -25278,7 +25290,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -25412,7 +25424,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -25744,7 +25756,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -25811,7 +25823,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -25981,7 +25993,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -26203,7 +26215,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -26753,7 +26765,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -27818,7 +27830,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -28664,7 +28676,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -30094,7 +30106,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
@ -30208,7 +30220,7 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
/** @description Too many requests */
429: {
content: {
'application/json': components['schemas']['Error'];

View File

@ -16,7 +16,7 @@ import type {
UserLite,
} from './autogen/models.js';
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'scheduleNote'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View File

@ -70,6 +70,7 @@ export const notificationTypes = [
'achievementEarned',
'exportCompleted',
'login',
'scheduleNote',
'test',
'app',
] as const;

View File

@ -125,7 +125,9 @@ const bannerStyle = computed(() => {
position: absolute;
top: 16px;
left: 16px;
max-width: calc(100% - 32px);
padding: 12px 16px;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 1.2em;

View File

@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
q: string;
@ -22,8 +23,44 @@ const query = ref(props.q);
const search = () => {
const sp = new URLSearchParams();
sp.append('q', query.value);
window.open(`https://www.google.com/search?${sp.toString()}`, '_blank', 'noopener');
let url = '';
switch (defaultStore.state.searchEngine) {
case 'google':
sp.append('q', query.value);
url = `https://www.google.com/search?${sp.toString()}`;
break;
case 'bing':
sp.append('q', query.value);
url = `https://www.bing.com/search?${sp.toString()}`;
break;
case 'yahoo':
sp.append('p', query.value);
url = `https://search.yahoo.com/search?${sp.toString()}`;
break;
case 'baidu':
// see detail: https://www.jademond.com/magazine/baidu-search-url-parameters/
sp.append('wd', query.value);
url = `https://www.baidu.com/s?${sp.toString()}`;
break;
case 'naver':
sp.append('query', query.value);
url = `https://search.naver.com/search.naver?${sp.toString()}`;
break;
case 'daum':
sp.append('q', query.value);
url = `https://search.daum.net/search?${sp.toString()}`;
break;
case 'duckduckgo':
sp.append('q', query.value);
url = `https://duckduckgo.com/?${sp.toString()}`;
break;
case 'other':
sp.append(defaultStore.state.searchEngineUrlQuery, query.value);
url = `${defaultStore.state.searchEngineUrl}${sp.toString()}`;
break;
}
window.open(url, '_blank', 'noopener');
};
</script>

View File

@ -87,9 +87,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance" @click="showOnRemote"/>
</div>
<div :class="$style.noteHeaderUsernameAndBadgeRoles">
<!--
<div :class="$style.noteHeaderUsername">
<MkAcct :user="appearNote.user"/>
</div>
-->
<div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles">
<img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
</div>

View File

@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'note:grouped'" :class="[$style.icon, $style.icon_noteGroup]"><i class="ti ti-pencil" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'scheduleNote'" :class="[$style.icon, $style.icon_scheduleNote]"><i class="ti ti-alert-triangle" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
@ -64,6 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
<span v-else-if="notification.type === 'scheduleNote'">{{ i18n.ts._notification._types.scheduleNote }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
@ -141,6 +143,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>
</div>
</template>
<span v-else-if="notification.type === 'scheduleNote'" :class="$style.text">{{ i18n.ts._notification._scheduleNote[notification.errorType] }}</span>
<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">
<Mfm :text="notification.body" :nowrap="false"/>
@ -285,7 +288,8 @@ const rejectGroupInvitation = () => {
.icon_reactionGroup,
.icon_reactionGroupHeart,
.icon_renoteGroup,
.icon_noteGroup {
.icon_noteGroup,
.icon_scheduleNote {
display: grid;
align-items: center;
justify-items: center;
@ -312,6 +316,13 @@ const rejectGroupInvitation = () => {
background: var(--eventRenote);
}
.icon_scheduleNote {
width: 100%;
height: 100%;
color: var(--warn);
background: var(--eventOther);
}
.icon_app {
border-radius: 6px;
}

View File

@ -97,7 +97,7 @@ async function toggleReaction(ev: MouseEvent) {
if (oldReaction !== props.reaction) {
misskeyApi('notes/reactions/create', {
noteId: props.note.id,
reaction: `:${reactionName.value}:`,
reaction: props.reaction,
});
}
});
@ -120,7 +120,7 @@ async function toggleReaction(ev: MouseEvent) {
}
function stealReaction(ev: MouseEvent) {
if (!props.note.user.host && $i && !($i.isAdmin ?? $i.policies.canManageCustomEmojis)) return;
if (!props.note.user.host && $i && !($i.isAdmin || $i.policies.canManageCustomEmojis)) return;
os.popupMenu([{
type: 'label',

View File

@ -109,7 +109,7 @@ function onClick(ev: MouseEvent) {
}] : []),
);
if (props.host && $i && ($i.isAdmin ?? $i.policies.canManageCustomEmojis)) {
if (props.host && $i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) {
menuItems.push({
text: i18n.ts.import,
icon: 'ti ti-plus',

View File

@ -230,7 +230,19 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
import { $i } from '@/account.js';
const patronsWithIconWithCherryPick = [];
const patronsWithIconWithCherryPick = [{
name: 'Etone Sabasappugawa',
icon: 'https://s3.kokonect.link/cherrypick/patreons/b3bd97949b664c81857cc7286552c65e.png',
}, {
name: 'okin',
icon: 'https://s3.kokonect.link/cherrypick/patreons/c185756cf04d483b9c7687d98ce1103c.png',
}, {
name: 'Kitty',
icon: 'https://s3.kokonect.link/cherrypick/patreons/5f8e4bac9cf34984bc59875f6d8d5c1d.gif',
}, {
name: 'breadguy',
icon: 'https://s3.kokonect.link/cherrypick/patreons/04cd46fba69c4953949cd1cc15d8c691.jpg',
}];
const patronsWithIconWithMisskey = [{
name: 'カイヤン',
@ -474,6 +486,7 @@ const patronsWithMisskey = [
'こまつぶり',
'まゆつな空高',
'asata',
'ruru',
];
let isKokonect = false;

View File

@ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="deepl">DeepL</option>
<option value="google_no_api">Google Translate(without API)</option>
<option value="ctav3">Cloud Translation - Advanced(v3)</option>
<option value="Libretranslate">Libretranslate</option>
</MkRadios>
<template v-if="provider === 'deepl'">
@ -64,6 +65,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</template>
<template v-else-if="provider === 'Libretranslate'">
<div class="_gaps_m">
<MkInput v-model="libreTranslateEndPoint">
<template #prefix><i class="ti ti-server"></i></template>
<template #label>Api Endpoint</template>
</MkInput>
<MkInput v-model="libreTranslateApiKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>ApiKey</template>
</MkInput>
</div>
</template>
<MkButton primary rounded @click="save_deepl"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
@ -95,6 +108,8 @@ const ctav3ProjectId = ref<string>('');
const ctav3Location = ref<string>('');
const ctav3Model = ref<string>('');
const ctav3Glossary = ref<string>('');
const libreTranslateEndPoint = ref<string>('');
const libreTranslateApiKey = ref<string>('');
async function init() {
const meta = await misskeyApi('admin/meta');
@ -106,6 +121,8 @@ async function init() {
ctav3Location.value = meta.ctav3Location;
ctav3Model.value = meta.ctav3Model;
ctav3Glossary.value = meta.ctav3Glossary;
libreTranslateEndPoint.value = meta.libreTranslateEndPoint;
libreTranslateApiKey.value = meta.libreTranslateApiKey;
}
function save_deepl() {
@ -118,6 +135,8 @@ function save_deepl() {
ctav3Location: ctav3Location.value,
ctav3Model: ctav3Model.value,
ctav3Glossary: ctav3Glossary.value,
libreTranslateEndPoint: libreTranslateEndPoint.value,
libreTranslateApiKey: libreTranslateApiKey.value,
}).then(() => {
fetchInstance(true);
});

View File

@ -151,7 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.bubbleTimeline }}</template>
<div class="_gaps">
<MkTextarea v-if="bubbleTimelineEnabled" v-model="bubbleTimeline">
<MkTextarea v-model="bubbleTimeline">
<template #caption>{{ i18n.ts.bubbleInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_bubbleTimeline">{{ i18n.ts.save }}</MkButton>
@ -196,7 +196,6 @@ const blockedHosts = ref<string>('');
const silencedHosts = ref<string>('');
const mediaSilencedHosts = ref<string>('');
const trustedLinkUrlPatterns = ref<string>('');
const bubbleTimelineEnabled = ref<boolean>(false);
const bubbleTimeline = ref<string>('');
async function init() {
@ -214,7 +213,6 @@ async function init() {
silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n');
bubbleTimelineEnabled.value = meta.policies.btlAvailable;
bubbleTimeline.value = meta.bubbleInstances.join('\n');
}

View File

@ -253,11 +253,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder>
<template #icon><i class="ti ti-exclamation-circle"></i></template>
<template #label>{{ i18n.ts.abuseReports }}</template>
<template v-if="emailToReceiveAbuseReportForm.modified.value" #footer>
<MkFormFooter :form="emailToReceiveAbuseReportForm"/>
</template>
<div class="_gaps_m">
<MkInput v-model="emailToReceiveAbuseReportForm.state.emailToReceiveAbuseReport" type="email">
<template #prefix><i class="ti ti-mail"></i></template>
<template #label>{{ i18n.ts.emailToReceiveAbuseReport }}</template>
<template #label>{{ i18n.ts.emailToReceiveAbuseReport }} <span v-if="emailToReceiveAbuseReportForm.modifiedStates.emailToReceiveAbuseReport" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.emailToReceiveAbuseReportCaption }}</template>
</MkInput>
</div>

View File

@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>
<MkPageHeader v-model:tab="tab" :actions="$i ? headerActions : null" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/>
</template>
<MkSpacer :contentMax="700">
<MkSpacer :contentMax="1200">
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div v-if="tab === 'search'" key="search">
<div v-if="tab === 'search'" key="search" :class="$style.searchRoot">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
<template #prefix><i class="ti ti-search"></i></template>
@ -29,23 +29,31 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-if="tab === 'featured'" key="featured">
<MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'favorites'" key="favorites">
<MkPagination v-slot="{items}" :pagination="favoritesPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'following'" key="following">
<MkPagination v-slot="{items}" :pagination="followingPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'owned'" key="owned">
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="ownedPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
</MkHorizontalSwipe>
@ -88,6 +96,7 @@ onMounted(() => {
const featuredPagination = {
endpoint: 'channels/featured' as const,
limit: 10,
noPaging: true,
};
const favoritesPagination = {
@ -170,3 +179,17 @@ definePageMetadata(() => ({
icon: 'ti ti-device-tv',
}));
</script>
<style lang="scss" module>
.searchRoot {
width: 100%;
max-width: 700px;
margin: 0 auto;
}
.root {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: var(--MI-margin);
}
</style>

View File

@ -46,9 +46,10 @@ import { clipsCache } from '@/cache.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
import { getServerContext } from '@/server-context.js';
import { assertServerContext, serverContext } from '@/server-context.js';
const CTX_CLIP = getServerContext('clip');
// context
const CTX_CLIP = !$i && assertServerContext(serverContext, 'clip') ? serverContext.clip : null;
const props = defineProps<{
clipId: string,

View File

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{items}">
<div class="ldhfsamy">
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
<img :src="`/emoji/${emoji.name}.webp`" class="img" :alt="emoji.name"/>
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.category }}</div>
@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{items}">
<div class="ldhfsamy">
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
<img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" class="img" :alt="emoji.name"/>
<img :src="getProxiedImageUrl(emoji.url, 'emoji')" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.host }}</div>
@ -83,6 +83,7 @@ import FormSplit from '@/components/form/split.vue';
import { selectFile, selectFiles } from '@/scripts/select-file.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';

View File

@ -119,7 +119,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
}, { immediate: true });
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null);
async function changeImage(ev: Event) {
file.value = await selectFile(ev.currentTarget ?? ev.target, null);

View File

@ -117,5 +117,6 @@ definePageMetadata(() => ({
border-radius: var(--MI-radius);
background-color: var(--MI_THEME-panel);
overflow-x: scroll;
white-space: nowrap;
}
</style>

View File

@ -50,6 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'cherrypick-js';
import { host } from '@@/js/config.js';
import type { Paging } from '@/components/MkPagination.vue';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import MkNotes from '@/components/MkNotes.vue';
@ -62,9 +63,11 @@ import { dateString } from '@/filters/date.js';
import MkClipPreview from '@/components/MkClipPreview.vue';
import { defaultStore } from '@/store.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { getServerContext } from '@/server-context.js';
import { serverContext, assertServerContext } from '@/server-context.js';
import { $i } from '@/account.js';
const CTX_NOTE = getServerContext('note');
// context
const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
const props = defineProps<{
noteId: string;
@ -140,7 +143,12 @@ function fetchNote() {
}).catch(err => {
if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
pleaseLogin({
path: '/',
message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
openOnRemote: {
type: 'lookup',
url: `https://${host}/notes/${props.noteId}`,
},
});
}
error.value = err;

View File

@ -152,6 +152,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="S">{{ i18n.ts._hemisphere.S }}</option>
<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
</MkRadios>
<MkSelect v-model="searchEngine">
<template #label>{{ i18n.ts._searchSite.title }}</template>
<template #caption>{{ i18n.ts._searchSite.description }}</template>
<option value="google">Google</option>
<option value="bing">Bing</option>
<option value="yahoo">Yahoo</option>
<option value="baidu">Baidu</option>
<option value="naver">NAVER</option>
<option value="daum">Daum</option>
<option value="duckduckgo">DuckDuckGo</option>
<option value="other">{{ i18n.ts.other }}</option>
</MkSelect>
<MkInput v-if="defaultStore.state.searchEngine == 'other'" v-model="searchEngineUrl">
<template #label>{{ i18n.ts._searchSite.otherSearchEngine }}</template>
<template #caption>{{ i18n.ts._searchSite.otherDescription }}</template>
</MkInput>
<MkInput v-if="defaultStore.state.searchEngine == 'other'" v-model="searchEngineUrlQuery">
<template #label>{{ i18n.ts._searchSite.query }}</template>
<template #caption>{{ i18n.ts._searchSite.queryDescription }}</template>
</MkInput>
<MkFolder>
<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
<div class="_buttons">
@ -182,6 +202,7 @@ import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/MkLink.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkInput from '@/components/MkInput.vue';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
@ -223,6 +244,9 @@ const useAutoTranslate = computed(defaultStore.makeGetterSetter('useAutoTranslat
const welcomeBackToast = computed(defaultStore.makeGetterSetter('welcomeBackToast'));
const disableNyaize = computed(defaultStore.makeGetterSetter('disableNyaize'));
const externalNavigationWarning = computed(defaultStore.makeGetterSetter('externalNavigationWarning'));
const searchEngine = computed(defaultStore.makeGetterSetter('searchEngine'));
const searchEngineUrl = computed(defaultStore.makeGetterSetter('searchEngineUrl'));
const searchEngineUrlQuery = computed(defaultStore.makeGetterSetter('searchEngineUrlQuery'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);

View File

@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</transition>
<div :class="$style.tl">
<div v-if="!isAvailableBasicTimeline(src)" :class="$style.disabled">
<div v-if="!isAvailableBasicTimeline(src) && !src.startsWith('list:')" :class="$style.disabled">
<p :class="$style.disabledTitle">
<i class="ti ti-circle-minus"></i>
{{ i18n.ts._disabledTimeline.title }}
@ -188,7 +188,7 @@ const alwaysShowCw = ref(defaultStore.state.alwaysShowCw);
watch(src, () => {
queue.value = 0;
queueUpdated(queue);
queueUpdated(queue.value);
});
watch(enableWidgetsArea, (x) => {

View File

@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { defineAsyncComponent, computed, watch, ref } from 'vue';
import * as Misskey from 'cherrypick-js';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import { acct as getAcct } from '@/filters/user.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@ -46,7 +47,7 @@ import { mainRouter } from '@/router/main.js';
import { defaultStore } from '@/store.js';
import { deviceKind } from '@/scripts/device-kind.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { getServerContext } from '@/server-context.js';
import { serverContext, assertServerContext } from '@/server-context.js';
const MOBILE_THRESHOLD = 500;
@ -67,7 +68,8 @@ const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
const CTX_USER = getServerContext('user');
// context
const CTX_USER = !$i && assertServerContext(serverContext, 'user') ? serverContext.user : null;
const props = withDefaults(defineProps<{
acct: string;

View File

@ -33,7 +33,43 @@ export async function lookup(router?: Router) {
uri: query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
os.promiseDialog(promise, null, (err) => {
let title = i18n.ts.somethingHappened;
let text = err.message + '\n' + err.id;
switch (err.id) {
case '974b799e-1a29-4889-b706-18d4dd93e266':
title = i18n.ts._remoteLookupErrors._federationNotAllowed.title;
text = i18n.ts._remoteLookupErrors._federationNotAllowed.description;
break;
case '1a5eab56-e47b-48c2-8d5e-217b897d70db':
title = i18n.ts._remoteLookupErrors._uriInvalid.title;
text = i18n.ts._remoteLookupErrors._uriInvalid.description;
break;
case '81b539cf-4f57-4b29-bc98-032c33c0792e':
title = i18n.ts._remoteLookupErrors._requestFailed.title;
text = i18n.ts._remoteLookupErrors._requestFailed.description;
break;
case '70193c39-54f3-4813-82f0-70a680f7495b':
title = i18n.ts._remoteLookupErrors._responseInvalid.title;
text = i18n.ts._remoteLookupErrors._responseInvalid.description;
break;
case 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a':
title = i18n.ts._remoteLookupErrors._responseInvalid.title;
text = i18n.ts._remoteLookupErrors._responseInvalidIdHostNotMatch.description;
break;
case 'dc94d745-1262-4e63-a17d-fecaa57efc82':
title = i18n.ts._remoteLookupErrors._noSuchObject.title;
text = i18n.ts._remoteLookupErrors._noSuchObject.description;
break;
}
os.alert({
type: 'error',
title,
text,
});
}, i18n.ts.fetchingAsApObject);
const res = await promise;

View File

@ -2,22 +2,20 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'cherrypick-js';
import { $i } from '@/account.js';
const providedContextEl = document.getElementById('cherrypick_clientCtx');
export type ServerContext = {
clip?: Misskey.entities.Clip;
note?: Misskey.entities.Note;
user?: Misskey.entities.UserLite;
user?: Misskey.entities.UserDetailed;
} | null;
export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null;
export function getServerContext<K extends keyof NonNullable<ServerContext>>(entity: K): Required<Pick<NonNullable<ServerContext>, K>> | null {
// contextは非ログイン状態の情報しかないためログイン時は利用できない
if ($i) return null;
return serverContext ? (serverContext[entity] ?? null) : null;
export function assertServerContext<K extends keyof NonNullable<ServerContext>>(ctx: ServerContext, entity: K): ctx is Required<Pick<NonNullable<ServerContext>, K>> {
if (ctx == null) return false;
return entity in ctx && ctx[entity] != null;
}

View File

@ -582,6 +582,18 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
searchEngine: {
where: 'device',
default: 'google' as 'google' | 'bing' | 'yahoo' | 'baidu' | 'naver' | 'daum' | 'duckduckgo' | 'other',
},
searchEngineUrl: {
where: 'device',
default: 'https://www.ecosia.org/search?',
},
searchEngineUrlQuery: {
where: 'device',
default: 'q',
},
// - Settings/Appearance
collapseReplies: {

View File

@ -134,7 +134,7 @@ export function openInstanceMenu(ev: MouseEvent) {
});
}
if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) {
if (instance.impressumUrl != null || instance.tosUrl != null || instance.privacyPolicyUrl != null) {
menuItems.push({ type: 'divider' });
}

View File

@ -75,9 +75,9 @@ const otherMenuItemIndicated = computed(() => {
});
const controlPanelIndicated = ref(false);
const releasesCherryPick = ref(null);
const bannerDisplay = ref('');
const bannerDisplay = ref(defaultStore.state.bannerDisplay);
if ($i.isAdmin ?? $i.isModerator) {
if ($i && ($i.isAdmin ?? $i.isModerator)) {
misskeyApi('admin/abuse-user-reports', {
state: 'unresolved',
limit: 1,

View File

@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</component>
</template>
<div :class="$style.divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
<MkA v-if="$i != null && ($i.isAdmin || $i.isModerator)" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
<span v-if="controlPanelIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
</MkA>
@ -52,10 +52,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.bottom">
<div v-if="['all', 'topBottom', 'bottom'].includes(<string>bannerDisplay)" :class="[$style.banner, $style.bottomBanner]" :style="{ backgroundImage: `url(${ $i.bannerUrl })` }"></div>
<button v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip.noDelay.right="defaultStore.state.renameTheButtonInPostFormToNya ? i18n.ts.nya : i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post">
<button v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip.noDelay.right="defaultStore.state.renameTheButtonInPostFormToNya ? i18n.ts.nya : i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="() => { os.post(); }">
<i class="ti ti-fw" :class="[$style.postIcon, defaultStore.state.renameTheButtonInPostFormToNya ? 'ti-paw-filled' : 'ti-pencil']"></i><span :class="$style.postText">{{ defaultStore.state.renameTheButtonInPostFormToNya ? i18n.ts.nya : i18n.ts.note }}</span>
</button>
<button v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
<button v-if="$i != null" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
<MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/>
</button>
</div>
@ -88,9 +88,13 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
const iconOnly = ref(false);
const forceIconOnly = ref(window.innerWidth <= 1279);
const iconOnly = computed(() => {
return forceIconOnly.value || (defaultStore.reactiveState.menuDisplay.value === 'sideIcon');
});
const menu = computed(() => defaultStore.state.menu);
const otherMenuItemIndicated = computed(() => {
@ -102,9 +106,9 @@ const otherMenuItemIndicated = computed(() => {
});
const controlPanelIndicated = ref(false);
const releasesCherryPick = ref(null);
const bannerDisplay = ref('');
const bannerDisplay = ref(defaultStore.state.bannerDisplay);
if ($i.isAdmin ?? $i.isModerator) {
if ($i && ($i.isAdmin ?? $i.isModerator)) {
misskeyApi('admin/abuse-user-reports', {
state: 'unresolved',
limit: 1,
@ -128,14 +132,10 @@ if ($i.isAdmin ?? $i.isModerator) {
});
}
const forceIconOnly = window.innerWidth <= 1279;
function calcViewState() {
iconOnly.value = forceIconOnly || (defaultStore.state.menuDisplay === 'sideIcon');
forceIconOnly.value = window.innerWidth <= 1279;
}
calcViewState();
window.addEventListener('resize', calcViewState);
watch(defaultStore.reactiveState.menuDisplay, () => {
@ -175,8 +175,10 @@ function openAccountMenu(ev: MouseEvent) {
}
function more(ev: MouseEvent) {
const target = getHTMLElementOrNull(ev.currentTarget ?? ev.target);
if (!target) return;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
src: ev.currentTarget ?? ev.target,
src: target,
}, {
closed: () => dispose(),
});

View File

@ -76,7 +76,7 @@ const otherNavItemIndicated = computed<boolean>(() => {
const controlPanelIndicated = ref(false);
const releasesCherryPick = ref(null);
if ($i.isAdmin ?? $i.isModerator) {
if ($i && ($i.isAdmin ?? $i.isModerator)) {
misskeyApi('admin/abuse-user-reports', {
state: 'unresolved',
limit: 1,

View File

@ -85,7 +85,7 @@ const settingsWindowed = ref(false);
const controlPanelIndicated = ref(false);
const releasesCherryPick = ref(null);
if ($i.isAdmin ?? $i.isModerator) {
if ($i && ($i.isAdmin ?? $i.isModerator)) {
misskeyApi('admin/abuse-user-reports', {
state: 'unresolved',
limit: 1,

View File

@ -77,9 +77,9 @@ const otherMenuItemIndicated = computed(() => {
});
const controlPanelIndicated = ref(false);
const releasesCherryPick = ref(null);
const bannerDisplay = ref('');
const bannerDisplay = ref(defaultStore.state.bannerDisplay);
if ($i.isAdmin ?? $i.isModerator) {
if ($i && ($i.isAdmin ?? $i.isModerator)) {
misskeyApi('admin/abuse-user-reports', {
state: 'unresolved',
limit: 1,

View File

@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</component>
</template>
<div :class="$style.divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
<MkA v-if="$i != null && ($i.isAdmin || $i.isModerator)" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
<span v-if="controlPanelIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
</MkA>
@ -52,14 +52,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.bottom">
<div v-if="['all', 'topBottom', 'bottom'].includes(<string>bannerDisplay)" :class="[$style.banner, $style.bottomBanner]" :style="{ backgroundImage: `url(${ $i.bannerUrl })` }"></div>
<button v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip.noDelay.right="defaultStore.state.renameTheButtonInPostFormToNya ? i18n.ts.nya : i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post">
<button v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip.noDelay.right="defaultStore.state.renameTheButtonInPostFormToNya ? i18n.ts.nya : i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="() => { os.post(); }">
<i class="ti ti-fw" :class="[$style.postIcon, defaultStore.state.renameTheButtonInPostFormToNya ? 'ti-paw-filled' : 'ti-pencil']"></i><span :class="$style.postText">{{ defaultStore.state.renameTheButtonInPostFormToNya ? i18n.ts.nya : i18n.ts.note }}</span>
</button>
<div :class="$style.profile">
<button v-if="iconOnly" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
<MkAvatar :user="$i" :class="$style.avatar"/>
</button>
<button v-else v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openProfile">
<button v-else-if="$i != null" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openProfile">
<MkAvatar :user="$i" :class="$style.avatar"/><MkUserName class="_nowrap" :class="$style.acct" :user="$i"/>
</button>
<button v-if="!iconOnly" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" class="_button" :class="[$style.drawer]" @click="openAccountMenu"><i class="ti ti-chevron-up"/></button>
@ -94,10 +94,14 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { mainRouter } from '@/router/main.js';
const iconOnly = ref(false);
const forceIconOnly = ref(window.innerWidth <= 1279);
const iconOnly = computed(() => {
return forceIconOnly.value || (defaultStore.reactiveState.menuDisplay.value === 'sideIcon');
});
const menu = computed(() => defaultStore.state.menu);
const otherMenuItemIndicated = computed(() => {
@ -109,9 +113,9 @@ const otherMenuItemIndicated = computed(() => {
});
const controlPanelIndicated = ref(false);
const releasesCherryPick = ref(null);
const bannerDisplay = ref('');
const bannerDisplay = ref(defaultStore.state.bannerDisplay);
if ($i.isAdmin ?? $i.isModerator) {
if ($i && ($i.isAdmin ?? $i.isModerator)) {
misskeyApi('admin/abuse-user-reports', {
state: 'unresolved',
limit: 1,
@ -135,14 +139,10 @@ if ($i.isAdmin ?? $i.isModerator) {
});
}
const forceIconOnly = window.innerWidth <= 1279;
function calcViewState() {
iconOnly.value = forceIconOnly || (defaultStore.state.menuDisplay === 'sideIcon');
forceIconOnly.value = window.innerWidth <= 1279;
}
calcViewState();
window.addEventListener('resize', calcViewState);
watch(defaultStore.reactiveState.menuDisplay, () => {
@ -182,8 +182,10 @@ function openAccountMenu(ev: MouseEvent) {
}
function more(ev: MouseEvent) {
const target = getHTMLElementOrNull(ev.currentTarget ?? ev.target);
if (!target) return;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
src: ev.currentTarget ?? ev.target,
src: target,
}, {
closed: () => dispose(),
});

View File

@ -262,6 +262,12 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
data,
}];
case 'scheduleNote':
return [i18n.ts._notification._types.scheduleNote, {
body: data.body.errorType,
data,
}];
case 'app':
return [data.body.header ?? data.body.body, {
body: data.body.header ? data.body.body : '',