0
0

Compare commits

..

38 Commits

Author SHA1 Message Date
d13d354707 Merge remote-tracking branch 'upstream/develop' into develop
Some checks failed
Check SPDX-License-Identifier / check-spdx-license-id (push) Has been cancelled
Check copyright year / check_copyright_year (push) Has been cancelled
Publish Docker image (develop) / Build (linux/amd64) (push) Has been cancelled
Publish Docker image (develop) / Build (linux/arm64) (push) Has been cancelled
Publish Docker image (develop) / merge (push) Has been cancelled
Dockle / dockle (push) Has been cancelled
Storybook / build (push) Has been cancelled
Test (production install and build) / production (20.16.0) (push) Has been cancelled
2024-10-21 12:03:03 +00:00
syuilo
70b2a8f72e fix(frontend): /iのレスポンスに含まれないプロパティが消えずに残り続ける問題を修正 2024-10-21 19:59:20 +09:00
syuilo
c4f1ca2fd9 fix(frontend): MkSelectでmodelValueが更新されない限り値を更新しないように 2024-10-21 19:14:02 +09:00
Kisaragi
9d0f7eeb9c
docs: ActivityPub層の変更を含む場合にやるべきことを明文化 (#14812) 2024-10-21 15:12:28 +09:00
かっこかり
bc1fce9af6
fix(frontend): デッキのタイムラインカラムでwithSensitiveが利用できない問題を修正 (#14772)
* fix(frontend): デッキのタイムラインカラムでwithSensitiveが利用できない問題を修正

* Update Changelog

* Update Changelog

* Update packages/frontend/src/ui/deck/tl-column.vue
2024-10-21 13:22:21 +09:00
かっこかり
5f12bc515d
Update CHANGELOG.md 2024-10-21 13:11:11 +09:00
Yuba
2f9c04b23b
refs#10866 投稿ダイアログでEscキーが押されたときIME入力中ならダイアログは閉じない (#14787) 2024-10-21 12:51:45 +09:00
syuilo
5c79d8db20
feat: ノートの閲覧にログイン必須にする設定 (#14799)
* wip

* wip

* wip

* Update packages/frontend/src/pages/note.vue

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>

* wip

* Update WebhookTestService.ts

* Update privacy.vue

* wip

* rename

* Update locales/ja-JP.yml

Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>

* 🎨

* wip

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
2024-10-21 12:49:29 +09:00
かっこかり
bc0c53b92b
fix(frontend): Captcha のエラーハンドリング (#14811)
* fix(frontend): Captcha のエラーハンドリングを修正 (MisskeyIO#768)

(cherry picked from commit 88912d0f8c63a762fbb1d43e5c1abf4fd9fc05d4)

* Update Changelog

* typo

---------

Co-authored-by: riku6460 <17585784+riku6460@users.noreply.github.com>
2024-10-21 11:44:57 +09:00
かっこかり
d6caa4d9c4
fix(frontend): 通知の範囲指定が必要ない通知設定でも範囲指定がでている問題を修正 (#14798)
* fix(frontend): 通知の範囲指定が必要ない通知設定でも範囲指定がでている問題を修正

* Update Changelog

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-20 17:29:41 +09:00
caipira113
d894fc7f12
Merge tag '2024.10.1' into develop
# Conflicts:
#	package.json
2024-10-17 23:28:02 +09:00
caipira113
654821da00
fix: prevent note federation for localOnly 2024-10-17 23:27:52 +09:00
caipira113
f7d74b74e4
chore: enableCondensedLine -> (default) false 2024-10-13 00:55:35 +09:00
caipira113
b9f4e00072
Merge remote-tracking branch 'mk/develop' into develop
# Conflicts:
#	locales/en-US.yml
#	package.json
#	packages/backend/src/config.ts
#	packages/frontend/src/components/MkCode.core.vue
#	pnpm-lock.yaml
2024-10-13 00:53:26 +09:00
caipira113
134408ced6
Merge tag '2024.9.0' into develop
# Conflicts:
#	package.json
#	pnpm-lock.yaml
2024-09-29 23:46:27 +09:00
caipira113
7e3e62e2de
fix(test): type error 2024-09-29 03:38:59 +09:00
d1af6271b4
Merge upstream 2024-09-29 03:30:56 +09:00
caipira113
9e1b46f160
fix: When updating Note, emojis are not reflected through Streaming 2024-08-26 18:58:30 +09:00
caipira113
b353223941
Merge remote-tracking branch 'mk/develop' into develop
# Conflicts:
#	locales/index.d.ts
#	locales/ja-JP.yml
#	packages/backend/src/server/web/boot.js
2024-08-26 18:43:40 +09:00
caipira113
54122a93dd
fix sw 2024-08-26 18:34:12 +09:00
caipira113
7318200786
Revert "Note click behavior"
This reverts commit cefff6cb92.
2024-08-21 03:30:27 +09:00
caipira113
64abf28bb0
Merge remote-tracking branch 'mk/develop' into develop 2024-08-20 21:20:35 +09:00
caipira113
15388e7732
version prefix 2024-08-20 21:20:16 +09:00
Caipira
317cabe15c
feat: note update federation (#1)
ellelle
2024-08-20 21:12:49 +09:00
caipira113
421d8fb2c2
fix ci 2024-08-20 21:12:49 +09:00
caipira113
285371c639
Migrate from Docker Hub to ghcr.io for CI builds 2024-08-20 21:12:49 +09:00
caipira113
a312a744f9
Remove navbar(about, tools) 2024-08-20 21:12:48 +09:00
caipira113
cefff6cb92
Note click behavior 2024-08-20 21:12:48 +09:00
caipira113
09839ea5a4
squareAvatars: true 2024-08-19 23:36:41 +09:00
caipira113
07553f5f8e
menuDisplay: sideIcon 2024-08-19 23:36:41 +09:00
caipira113
dd58eacac0
Custom Fonts 2024-08-19 23:36:40 +09:00
caipira113
aaf560f747
Post modal - Note Preview - without avatar 2024-08-19 23:36:40 +09:00
caipira113
1a44c33340
Post modal max-width: 520px -> 750px 2024-08-19 23:36:40 +09:00
caipira113
ccb1883e3e
feat(backend): Override the file URL rendering in AP 2024-08-19 23:36:40 +09:00
caipira113
13e7b49f09
Add buttersc.one Theme 2024-08-19 23:36:40 +09:00
caipira113
e56c372de7
Add Byeolvit-Theme
404d1e1593
2024-08-19 23:36:39 +09:00
caipira113
694722c02f
Stella (Branding) 2024-08-19 23:36:39 +09:00
caipira113
727ed9de55
Add volta config 2024-08-19 16:03:50 +09:00
119 changed files with 2151 additions and 821 deletions

View File

@ -186,6 +186,9 @@ id: 'aidx'
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Override the file URL rendering in ActivityPub (Object Storage file only)
#apFileBaseUrl: https://example.com/
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128

View File

@ -275,6 +275,9 @@ id: 'aidx'
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Override the file URL rendering in ActivityPub (Object Storage file only)
#apFileBaseUrl: https://example.com/
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128

View File

@ -179,6 +179,9 @@ id: 'aidx'
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Override the file URL rendering in ActivityPub (Object Storage file only)
#apFileBaseUrl: https://example.com/
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128

View File

@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
REGISTRY_IMAGE: misskey/misskey
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
jobs:
# see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
@ -20,7 +20,8 @@ jobs:
platform:
- linux/amd64
- linux/arm64
if: github.repository == 'misskey-dev/misskey'
outputs:
commit_sha: ${{ steps.git.outputs.commit_sha }}
steps:
- name: Prepare
run: |
@ -28,13 +29,17 @@ jobs:
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v4.1.1
- name: Get commit sha
id: git
run: echo "commit_sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
- name: Log in to Container registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
@ -73,16 +78,18 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
- name: Log in to Container registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create --tag ${{ env.REGISTRY_IMAGE }}:develop \
docker buildx imagetools create --tag ${{ env.REGISTRY_IMAGE }}:develop --tag ${{ env.REGISTRY_IMAGE }}:${{needs.build.outputs.commit_sha}} \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:develop
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{needs.build.outputs.commit_sha}}

View File

@ -1,12 +1,17 @@
## Unreleased
### General
-
- Feat: コンテンツの表示にログインを必須にできるように
### Client
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
- Enhance: ドライブでソートができるように
- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように #10866
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
### Server
-

View File

@ -64,6 +64,22 @@ Thank you for your PR! Before creating a PR, please check the following:
Thanks for your cooperation 🤗
### Additional things for ActivityPub payload changes
*This section is specific to misskey-dev implementation. Other fork or implementation may take different way. A significant difference is that non-"misskey-dev" extension is not described in the misskey-hub's document.*
If PR includes changes to ActivityPub payload, please reflect it in [misskey-hub's document](https://github.com/misskey-dev/misskey-hub-next/blob/master/content/ns.md) by sending PR.
The name of purporsed extension property (referred as "extended property" in later) to ActivityPub shall be prefixed by `_misskey_`. (i.e. `_misskey_quote`)
The extended property in `packages/backend/src/core/activitypub/type.ts` **must** be declared as optional because ActivityPub payloads that comes from older Misskey or other implementation may not contain it.
The extended property must be included in the context definition. Context is defined in `packages/backend/src/core/activitypub/misc/contexts.ts`.
The key shall be same as the name of extended property, and the value shall be same as "short IRI".
"Short IRI" is defined in misskey-hub's document, but usually takes form of `misskey:<name of extended property>`. (i.e. `misskey:_misskey_quote`)
One should not add property that has defined before by other implementation, or add custom variant value to "well-known" property.
## Reviewers guide
Be willing to comment on the good points and not just the things you want fixed 💯

View File

@ -200,6 +200,9 @@ id: "aidx"
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Override the file URL rendering in ActivityPub (Object Storage file only)
#apFileBaseUrl: https://example.com/
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128

View File

@ -1294,6 +1294,7 @@ _abuseUserReport:
accept: "Accept"
reject: "Reject"
resolveTutorial: "If the report is legitimate in content, select \"Accept\" to mark the case as resolved in the affirmative.\nIf the content of the report is not legitimate, select \"Reject\" to mark the case as resolved in the negative."
noteUpdatedAt: "Edited: {date} {time}"
_delivery:
status: "Delivery status"
stop: "Suspended"
@ -1738,6 +1739,7 @@ _role:
gtlAvailable: "Can view the global timeline"
ltlAvailable: "Can view the local timeline"
canPublicNote: "Can send public notes"
canEditNote: "Note editing"
mentionMax: "Maximum number of mentions in a note"
canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit"

40
locales/index.d.ts vendored
View File

@ -5130,6 +5130,10 @@ export interface Locale extends ILocale {
*
*/
"clipNoteLimitExceeded": string;
/**
* : {date} {time}
*/
"noteUpdatedAt": ParameterizedString<"date" | "time">;
/**
*
*/
@ -5190,6 +5194,32 @@ export interface Locale extends ILocale {
* 使
*/
"yourNameContainsProhibitedWordsDescription": string;
/**
* 稿
*/
"thisContentsAreMarkedAsSigninRequiredByAuthor": string;
/**
*
*/
"lockdown": string;
"_accountSettings": {
/**
*
*/
"requireSigninToViewContents": string;
/**
*
*/
"requireSigninToViewContentsDescription1": string;
/**
* URLプレビュー(OGP)Webページへの埋め込み
*/
"requireSigninToViewContentsDescription2": string;
/**
*
*/
"requireSigninToViewContentsDescription3": string;
};
"_abuseUserReport": {
/**
*
@ -6799,6 +6829,10 @@ export interface Locale extends ILocale {
* 稿
*/
"canPublicNote": string;
/**
*
*/
"canEditNote": string;
/**
*
*/
@ -9271,7 +9305,7 @@ export interface Locale extends ILocale {
*/
"youGotQuote": ParameterizedString<"name">;
/**
* {name}Renoteしまし
* {name}
*/
"youRenoted": ParameterizedString<"name">;
/**
@ -9376,7 +9410,7 @@ export interface Locale extends ILocale {
*/
"reply": string;
/**
* Renote
*
*/
"renote": string;
/**
@ -9434,7 +9468,7 @@ export interface Locale extends ILocale {
*/
"reply": string;
/**
* Renote
*
*/
"renote": string;
};

View File

@ -1278,6 +1278,7 @@ fromX: "{x}から"
genEmbedCode: "埋め込みコードを生成"
noteOfThisUser: "このユーザーのノート一覧"
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
noteUpdatedAt: "編集済み: {date} {time}"
performance: "パフォーマンス"
modified: "変更あり"
discard: "破棄"
@ -1293,6 +1294,14 @@ prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)"
prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"
thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています"
lockdown: "ロックダウン"
_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
requireSigninToViewContentsDescription1: "あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーから情報を収集されるのを防ぐ効果が期待できます。"
requireSigninToViewContentsDescription2: "URLプレビュー(OGP)、Webページへの埋め込み、ートの引用に対応していないサーバーからの表示も不可になります。"
requireSigninToViewContentsDescription3: "リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。"
_abuseUserReport:
forward: "転送"
@ -1757,6 +1766,7 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canEditNote: "ノートの編集"
mentionMax: "ノート内の最大メンション数"
canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数"
@ -2448,7 +2458,7 @@ _notification:
youGotMention: "{name}からのメンション"
youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました"
youRenoted: "{name}がリノートしました"
youWereFollowed: "フォローされました"
youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました"
@ -2476,7 +2486,7 @@ _notification:
follow: "フォロー"
mention: "メンション"
reply: "リプライ"
renote: "Renote"
renote: "リノート"
quote: "引用"
reaction: "リアクション"
pollEnded: "アンケートが終了"
@ -2492,7 +2502,7 @@ _notification:
_actions:
followBack: "フォローバック"
reply: "返信"
renote: "Renote"
renote: "リノート"
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"

View File

@ -1271,6 +1271,7 @@ alwaysConfirmFollow: "팔로우일 때 항상 확인하기"
inquiry: "문의하기"
tryAgain: "다시 시도해 주세요."
confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인"
noteUpdatedAt: "편집됨: {date} {time}"
sensitiveMediaRevealConfirm: "민감한 미디어입니다. 표시할까요?"
createdLists: "만든 리스트"
createdAntennas: "만든 안테나"
@ -1745,6 +1746,7 @@ _role:
gtlAvailable: "글로벌 타임라인 보이기"
ltlAvailable: "로컬 타임라인 보이기"
canPublicNote: "공개 노트 허용"
canEditNote: "노트 편집 허용"
mentionMax: "노트에 넣을 수 있는 멘션 수"
canInvite: "서버 초대 코드 발행"
inviteLimit: "초대 한도"

View File

@ -1,12 +1,13 @@
{
"name": "misskey",
"version": "2024.10.1",
"prefix": "sk",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@9.6.0",
"packageManager": "pnpm@9.7.1",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@ -77,5 +78,9 @@
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0"
},
"volta": {
"node": "20.16.0",
"pnpm": "9.7.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 751 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NoteEdit1724072711475 {
name = 'NoteEdit1724072711475'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
}
}

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SigninRequiredForShowContents1729333924409 {
name = 'SigninRequiredForShowContents1729333924409'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "requireSigninToViewContents" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "requireSigninToViewContents"`);
}
}

View File

@ -65,6 +65,8 @@ type Source = {
setupPassword?: string;
apFileBaseUrl?: string;
proxy?: string;
proxySmtp?: string;
proxyBypassHosts?: string[];
@ -132,6 +134,7 @@ export type Config = {
index: string;
scope?: 'local' | 'global' | string[];
} | undefined;
apFileBaseUrl: string | undefined;
proxy: string | undefined;
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
@ -261,6 +264,7 @@ export function loadConfig(): Config {
sentryForBackend: config.sentryForBackend,
sentryForFrontend: config.sentryForFrontend,
id: config.id,
apFileBaseUrl: config.apFileBaseUrl,
proxy: config.proxy,
proxySmtp: config.proxySmtp,
proxyBypassHosts: config.proxyBypassHosts,

View File

@ -43,6 +43,7 @@ import { MetaService } from './MetaService.js';
import { MfmService } from './MfmService.js';
import { ModerationLogService } from './ModerationLogService.js';
import { NoteCreateService } from './NoteCreateService.js';
import { NoteUpdateService } from './NoteUpdateService.js';
import { NoteDeleteService } from './NoteDeleteService.js';
import { NotePiningService } from './NotePiningService.js';
import { NoteReadService } from './NoteReadService.js';
@ -186,6 +187,7 @@ const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaServic
const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService };
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService };
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
@ -337,6 +339,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
MfmService,
ModerationLogService,
NoteCreateService,
NoteUpdateService,
NoteDeleteService,
NotePiningService,
NoteReadService,
@ -484,6 +487,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$MfmService,
$ModerationLogService,
$NoteCreateService,
$NoteUpdateService,
$NoteDeleteService,
$NotePiningService,
$NoteReadService,
@ -632,6 +636,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
MfmService,
ModerationLogService,
NoteCreateService,
NoteUpdateService,
NoteDeleteService,
NotePiningService,
NoteReadService,
@ -778,6 +783,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$MfmService,
$ModerationLogService,
$NoteCreateService,
$NoteUpdateService,
$NoteDeleteService,
$NotePiningService,
$NoteReadService,

View File

@ -119,7 +119,11 @@ export interface NoteEventTypes {
};
updated: {
cw: string | null;
text: string;
text: string | null;
files: Packed<'DriveFile'>[];
fileIds: string[];
poll: any | null;
emojis: Record<string, string>;
};
reacted: {
reaction: string;

View File

@ -124,6 +124,7 @@ type MinimumUser = {
type Option = {
createdAt?: Date | null;
updatedAt?: Date | null;
name?: string | null;
text?: string | null;
reply?: MiNote | null;

View File

@ -0,0 +1,309 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setImmediate } from 'node:timers/promises';
import util from 'util';
import { In, DataSource } from 'typeorm';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as mfm from 'mfm-js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
import type { NotesRepository, UsersRepository } from '@/models/_.js';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { RelayService } from '@/core/RelayService.js';
import { DI } from '@/di-symbols.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { SearchService } from '@/core/SearchService.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { MiDriveFile, MiPollVote } from '@/models/_.js';
import { MiPoll, IPoll } from '@/models/Poll.js';
import { concat } from '@/misc/prelude/array.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { NoteEntityService } from './entities/NoteEntityService.js';
type MinimumUser = {
id: MiUser['id'];
host: MiUser['host'];
username: MiUser['username'];
uri: MiUser['uri'];
};
type Option = {
updatedAt?: Date | null;
files?: MiDriveFile[] | null;
name?: string | null;
text?: string | null;
cw?: string | null;
apHashtags?: string[] | null;
apEmojis?: string[] | null;
poll?: IPoll | null;
};
@Injectable()
export class NoteUpdateService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private driveFileEntityService: DriveFileEntityService,
private globalEventService: GlobalEventService,
private relayService: RelayService,
private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService,
private searchService: SearchService,
private activeUsersChart: ActiveUsersChart,
) { }
@bindThis
public async update(user: {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
isBot: MiUser['isBot'];
}, data: Option, note: MiNote, silent = false): Promise<MiNote | null> {
if (data.updatedAt == null) data.updatedAt = new Date();
if (data.text) {
if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
}
data.text = data.text.trim();
} else {
data.text = null;
}
let tags = data.apHashtags;
let emojis = data.apEmojis;
// Parse MFM if needed
if (!tags || !emojis) {
const tokens = data.text ? mfm.parse(data.text)! : [];
const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
const choiceTokens = data.poll && data.poll.choices
? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
: [];
const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
tags = data.apHashtags ?? extractHashtags(combinedTokens);
emojis = data.apEmojis ?? extractCustomEmojisFromMfm(combinedTokens);
}
tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32);
const updatedNote = await this.updateNote(user, note, data, tags, emojis);
if (updatedNote) {
setImmediate('post updated', { signal: this.#shutdownController.signal }).then(
() => this.postNoteUpdated(updatedNote, user, silent),
() => { /* aborted, ignore this */ },
);
}
return updatedNote;
}
@bindThis
private async updateNote(user: {
id: MiUser['id']; host: MiUser['host'];
}, note: MiNote, data: Option, tags: string[], emojis: string[]): Promise<MiNote | null> {
const values = new MiNote({
updatedAt: data.updatedAt!,
fileIds: data.files ? data.files.map(file => file.id) : [],
text: data.text,
hasPoll: data.poll != null,
cw: data.cw ?? null,
tags: tags.map(tag => normalizeForSearch(tag)),
emojis,
attachedFileTypes: data.files ? data.files.map(file => file.type) : [],
});
// 投稿を更新
try {
if (note.hasPoll && values.hasPoll) {
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.update(MiNote, { id: note.id }, values);
if (values.hasPoll) {
const old_poll = await transactionalEntityManager.findOneBy(MiPoll, { noteId: note.id });
if (old_poll?.choices.toString() !== data.poll?.choices.toString() || old_poll?.multiple !== data.poll?.multiple) {
await transactionalEntityManager.delete(MiPoll, { noteId: note.id });
await transactionalEntityManager.delete(MiPollVote, { noteId: note.id });
const poll = new MiPoll({
noteId: note.id,
choices: data.poll?.choices,
expiresAt: data.poll?.expiresAt,
multiple: data.poll?.multiple,
votes: new Array(data.poll?.choices.length).fill(0),
noteVisibility: note.visibility,
userId: user.id,
userHost: user.host,
});
await transactionalEntityManager.insert(MiPoll, poll);
}
}
});
} else if (!note.hasPoll && values.hasPoll) {
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.update(MiNote, { id: note.id }, values);
if (values.hasPoll) {
const poll = new MiPoll({
noteId: note.id,
choices: data.poll?.choices,
expiresAt: data.poll?.expiresAt,
multiple: data.poll?.multiple,
votes: new Array(data.poll?.choices.length).fill(0),
noteVisibility: note.visibility,
userId: user.id,
userHost: user.host,
});
await transactionalEntityManager.insert(MiPoll, poll);
}
});
} else if (note.hasPoll && !values.hasPoll) {
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.update(MiNote, { id: note.id }, values);
if (!values.hasPoll) {
await transactionalEntityManager.delete(MiPoll, { noteId: note.id });
}
});
} else {
await this.notesRepository.update({ id: note.id }, values);
}
return await this.notesRepository.findOneBy({ id: note.id });
} catch (e) {
console.error(e);
throw e;
}
}
@bindThis
private async postNoteUpdated(note: MiNote, user: {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
isBot: MiUser['isBot'];
}, silent: boolean) {
if (!silent) {
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
const noteObj = await this.noteEntityService.pack(note, user);
console.log(noteObj);
this.globalEventService.publishNoteStream(note.id, 'updated', {
cw: noteObj.cw ?? null,
text: noteObj.text,
files: noteObj.files ?? [],
fileIds: noteObj.fileIds ?? [],
poll: noteObj.poll ?? null,
emojis: noteObj.emojis ?? [],
});
//#region AP deliver
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
await (async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const noteActivity = await this.renderNoteActivity(note, user);
await this.deliverToConcerned(user, note, noteActivity);
})();
}
//#endregion
}
// Register to search database
this.reIndex(note);
}
@bindThis
private async renderNoteActivity(note: MiNote, user: MiUser) {
const content = this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user);
return this.apRendererService.addContext(content);
}
@bindThis
private async getMentionedRemoteUsers(note: MiNote) {
const where = [] as any[];
// mention / reply / dm
const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
if (uris.length > 0) {
where.push(
{ uri: In(uris) },
);
}
// renote / quote
if (note.renoteUserId) {
where.push({
id: note.renoteUserId,
});
}
if (where.length === 0) return [];
return await this.usersRepository.find({
where,
}) as MiRemoteUser[];
}
@bindThis
private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) {
await this.apDeliverManagerService.deliverToFollowers(user, content);
await this.relayService.deliverToRelays(user, content);
const remoteUsers = await this.getMentionedRemoteUsers(note);
for (const remoteUser of remoteUsers) {
await this.apDeliverManagerService.deliverToUser(user, content, remoteUser);
}
}
@bindThis
private reIndex(note: MiNote) {
if (note.text == null && note.cw == null) return;
this.searchService.unindexNote(note);
this.searchService.indexNote(note);
}
@bindThis
public dispose(): void {
this.#shutdownController.abort();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@ -35,6 +35,7 @@ export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
canPublicNote: boolean;
canEditNote: boolean;
mentionLimit: number;
canInvite: boolean;
inviteLimit: number;
@ -69,6 +70,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true,
ltlAvailable: true,
canPublicNote: true,
canEditNote: true,
mentionLimit: 20,
canInvite: false,
inviteLimit: 0,
@ -374,6 +376,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canEditNote: calc('canEditNote', vs => vs.some(v => v === true)),
mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),

View File

@ -83,6 +83,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
isExplorable: true,
isHibernated: false,
isDeleted: false,
requireSigninToViewContents: false,
emojis: [],
score: 0,
host: null,
@ -134,6 +135,7 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
replyUserHost: null,
renoteUserId: null,
renoteUserHost: null,
updatedAt: null,
...override,
};
}

View File

@ -14,6 +14,7 @@ import { NotePiningService } from '@/core/NotePiningService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js';
@ -74,6 +75,7 @@ export class ApInboxService {
private notePiningService: NotePiningService,
private userBlockingService: UserBlockingService,
private noteCreateService: NoteCreateService,
private noteUpdateService: NoteUpdateService,
private noteDeleteService: NoteDeleteService,
private appLockService: AppLockService,
private apResolverService: ApResolverService,
@ -751,11 +753,13 @@ export class ApInboxService {
@bindThis
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
const uri = getApId(activity);
if (actor.uri !== activity.actor) {
return 'skip: invalid actor';
}
this.logger.debug('Update');
this.logger.debug(`Update: ${uri}`);
const resolver = this.apResolverService.createResolver();
@ -767,6 +771,9 @@ export class ApInboxService {
if (isActor(object)) {
await this.apPersonService.updatePerson(actor.uri, resolver, object);
return 'ok: Person updated';
} else if (getApType(object) === 'Note') {
await this.updateNote(resolver, actor, object, false, activity);
return 'ok: Note updated';
} else if (getApType(object) === 'Question') {
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
return 'ok: Question updated';
@ -775,6 +782,40 @@ export class ApInboxService {
}
}
@bindThis
private async updateNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: IUpdate): Promise<string> {
const uri = getApId(note);
if (typeof note === 'object') {
if (actor.uri !== note.attributedTo) {
return 'skip: actor.uri !== note.attributedTo';
}
if (typeof note.id === 'string') {
if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) {
return 'skip: host in actor.uri !== note.id';
}
}
}
const unlock = await this.appLockService.getApLock(uri);
try {
const target = await this.notesRepository.findOneBy({ uri: uri });
if (!target) return `skip: target note not located: ${uri}`;
await this.apNoteService.updateNote(note, target, resolver, silent);
return 'ok';
} catch (err) {
if (err instanceof StatusError && err.isClientError) {
return `skip ${err.statusCode}`;
} else {
throw err;
}
} finally {
unlock();
}
}
@bindThis
private async move(actor: MiRemoteUser, activity: IMove): Promise<string> {
// fetch the new and old accounts

View File

@ -108,6 +108,7 @@ export class ApRendererService {
actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Announce',
published: this.idService.parse(note.id).date.toISOString(),
updated: note.updatedAt?.toISOString() ?? undefined,
to,
cc,
object,
@ -164,7 +165,7 @@ export class ApRendererService {
return {
type: 'Document',
mediaType: file.webpublicType ?? file.type,
url: this.driveFileEntityService.getPublicUrl(file),
url: this.driveFileEntityService.getPublicUrl(file, undefined, true),
name: file.comment,
sensitive: file.isSensitive,
};
@ -244,7 +245,7 @@ export class ApRendererService {
public renderImage(file: MiDriveFile): IApImage {
return {
type: 'Image',
url: this.driveFileEntityService.getPublicUrl(file),
url: this.driveFileEntityService.getPublicUrl(file, undefined, true),
sensitive: file.isSensitive,
name: file.comment,
};
@ -438,6 +439,7 @@ export class ApRendererService {
_misskey_quote: quote,
quoteUrl: quote,
published: this.idService.parse(note.id).date.toISOString(),
updated: note.updatedAt?.toISOString() ?? undefined,
to,
cc,
inReplyTo,
@ -495,6 +497,7 @@ export class ApRendererService {
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
_misskey_summary: profile.description,
_misskey_followedMessage: profile.followedMessage,
_misskey_requireSigninToViewContents: user.requireSigninToViewContents,
icon: avatar ? this.renderImage(avatar) : null,
image: banner ? this.renderImage(banner) : null,
tag,

View File

@ -555,6 +555,7 @@ const extension_context_definition = {
'_misskey_votes': 'misskey:_misskey_votes',
'_misskey_summary': 'misskey:_misskey_summary',
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
'_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents',
'isCat': 'misskey:isCat',
// vcard
vcard: 'http://www.w3.org/2006/vcard/ns#',

View File

@ -6,7 +6,7 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js';
import type { NotesRepository, PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
@ -36,6 +36,7 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js';
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
@Injectable()
export class ApNoteService {
@ -54,6 +55,9 @@ export class ApNoteService {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private idService: IdService,
private apMfmService: ApMfmService,
private apResolverService: ApResolverService,
@ -70,6 +74,7 @@ export class ApNoteService {
private appLockService: AppLockService,
private pollService: PollService,
private noteCreateService: NoteCreateService,
private noteUpdateService: NoteUpdateService,
private apDbResolverService: ApDbResolverService,
private apLoggerService: ApLoggerService,
) {
@ -297,6 +302,7 @@ export class ApNoteService {
try {
return await this.noteCreateService.create(actor, {
createdAt: note.published ? new Date(note.published) : null,
updatedAt: note.updated ? new Date(note.updated) : null,
files,
reply,
renote: quote,
@ -326,6 +332,85 @@ export class ApNoteService {
}
}
@bindThis
public async updateNote(value: string | IObject, target: MiNote, resolver?: Resolver, silent = false): Promise<MiNote | null> {
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value);
const entryUri = getApId(value);
const err = this.validateNote(object, entryUri);
if (err) {
this.logger.error(err.message, {
resolver: { history: resolver.getHistory() },
value,
object,
});
throw new Error('invalid note');
}
const note = object as IPost;
// 投稿者をフェッチ
if (note.attributedTo == null) {
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
}
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new Error('actor has been suspended');
}
const files: MiDriveFile[] = [];
for (const attach of toArray(note.attachment)) {
attach.sensitive ??= note.sensitive;
const file = await this.apImageService.resolveImage(actor, attach);
if (file) files.push(file);
}
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
text = note.source.content;
} else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content;
} else if (typeof note.content === 'string') {
text = this.apMfmService.htmlToMfm(note.content, note.tag);
}
const apHashtags = extractApHashtags(note.tag);
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`);
return [];
});
const apEmojis = emojis.map(emoji => emoji.name);
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
try {
return await this.noteUpdateService.update(actor, {
updatedAt: note.updated ? new Date(note.updated) : null,
files,
name: note.name,
cw,
text,
apHashtags,
apEmojis,
poll,
}, target, silent);
} catch (err: any) {
this.logger.warn(`note update failed: ${err}`);
return err;
}
}
/**
* Noteを解決します
*

View File

@ -256,12 +256,12 @@ export class ApPersonService implements OnModuleInit {
return {
...( avatar ? {
avatarId: avatar.id,
avatarUrl: avatar.url ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
avatarUrl: avatar.url ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar', true) : null,
avatarBlurhash: avatar.blurhash,
} : {}),
...( banner ? {
bannerId: banner.id,
bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner) : null,
bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner, undefined, true) : null,
bannerBlurhash: banner.blurhash,
} : {}),
};
@ -356,6 +356,7 @@ export class ApPersonService implements OnModuleInit {
tags,
isBot,
isCat: (person as any).isCat === true,
requireSigninToViewContents: (person as any).requireSigninToViewContents === true,
emojis,
})) as MiRemoteUser;

View File

@ -14,7 +14,9 @@ export interface IObject {
summary?: string;
_misskey_summary?: string;
_misskey_followedMessage?: string | null;
_misskey_requireSigninToViewContents?: boolean;
published?: string;
updated?: string;
cc?: ApObject;
to?: ApObject;
attributedTo?: ApObject;

View File

@ -108,7 +108,7 @@ export class DriveFileEntityService {
}
@bindThis
public getPublicUrl(file: MiDriveFile, mode?: 'avatar'): string { // static = thumbnail
public getPublicUrl(file: MiDriveFile, mode?: 'avatar', ap?: boolean): string { // static = thumbnail
// リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
return this.getProxiedUrl(file.uri, mode);
@ -130,6 +130,16 @@ export class DriveFileEntityService {
if (mode === 'avatar') {
return this.getProxiedUrl(url, 'avatar');
}
if (ap && this.config.apFileBaseUrl) {
const baseUrl = this.config.apFileBaseUrl;
const isValidBaseUrl = /^https?:\/\/[\w.-]+\.[a-zA-Z]{2,}(\/.*)?$/i.test(baseUrl);
if (isValidBaseUrl) {
const trimmedBaseUrl = baseUrl.replace(/\/$/, '');
return url.replace(/^https?:\/\/[\w.-]+\.[a-zA-Z]{2,}/, trimmedBaseUrl);
}
}
return url;
}

View File

@ -149,6 +149,10 @@ export class NoteEntityService implements OnModuleInit {
}
}
if (packedNote.user.requireSigninToViewContents && meId == null) {
hide = true;
}
if (hide) {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
@ -365,6 +369,7 @@ export class NoteEntityService implements OnModuleInit {
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
createdAt: this.idService.parse(note.id).date.toISOString(),
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
userId: note.userId,
user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
text: text,

View File

@ -490,6 +490,7 @@ export class UserEntityService implements OnModuleInit {
}))) : [],
isBot: user.isBot,
isCat: user.isCat,
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,

View File

@ -229,6 +229,11 @@ export class MiNote {
comment: '[Denormalized]',
})
public renoteUserHost: string | null;
@Column('timestamp with time zone', {
default: null,
})
public updatedAt: Date | null;
//#endregion
constructor(data: Partial<MiNote>) {

View File

@ -202,6 +202,11 @@ export class MiUser {
})
public isHibernated: boolean;
@Column('boolean', {
default: false,
})
public requireSigninToViewContents: boolean;
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
@Column('boolean', {
default: false,

View File

@ -17,6 +17,11 @@ export const packedNoteSchema = {
optional: false, nullable: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
optional: true, nullable: true,
format: 'date-time',
},
deletedAt: {
type: 'string',
optional: true, nullable: true,

View File

@ -180,6 +180,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
canEditNote: {
type: 'boolean',
optional: false, nullable: true,
},
mentionLimit: {
type: 'integer',
optional: false, nullable: false,

View File

@ -115,6 +115,10 @@ export const packedUserLiteSchema = {
type: 'boolean',
nullable: false, optional: true,
},
requireSigninToViewContents: {
type: 'boolean',
nullable: false, optional: true,
},
instance: {
type: 'object',
nullable: false, optional: true,

View File

@ -282,6 +282,7 @@ import * as ep___notes_children from './endpoints/notes/children.js';
import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_update from './endpoints/notes/update.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
@ -669,6 +670,7 @@ const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep__
const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default };
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default };
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
@ -1060,6 +1062,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_clips,
$notes_conversation,
$notes_create,
$notes_update,
$notes_delete,
$notes_favorites_create,
$notes_favorites_delete,
@ -1445,6 +1448,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_clips,
$notes_conversation,
$notes_create,
$notes_update,
$notes_delete,
$notes_favorites_create,
$notes_favorites_delete,

View File

@ -39,6 +39,17 @@ export class GetterService {
return note;
}
@bindThis
public async getNoteWithUser(noteId: MiNote['id']) {
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
if (note == null) {
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
}
return note;
}
/**
* Get user for API processing
*/

View File

@ -288,6 +288,7 @@ import * as ep___notes_children from './endpoints/notes/children.js';
import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_update from './endpoints/notes/update.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
@ -673,6 +674,7 @@ const eps = [
['notes/clips', ep___notes_clips],
['notes/conversation', ep___notes_conversation],
['notes/create', ep___notes_create],
['notes/update', ep___notes_update],
['notes/delete', ep___notes_delete],
['notes/favorites/create', ep___notes_favorites_create],
['notes/favorites/delete', ep___notes_favorites_delete],

View File

@ -179,6 +179,7 @@ export const paramDef = {
autoAcceptFollowed: { type: 'boolean' },
noCrawle: { type: 'boolean' },
preventAiLearning: { type: 'boolean' },
requireSigninToViewContents: { type: 'boolean' },
isBot: { type: 'boolean' },
isCat: { type: 'boolean' },
injectFeaturedNote: { type: 'boolean' },
@ -334,6 +335,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
if (typeof ps.requireSigninToViewContents === 'boolean') updates.requireSigninToViewContents = ps.requireSigninToViewContents;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;

View File

@ -26,6 +26,12 @@ export const meta = {
code: 'NO_SUCH_NOTE',
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
},
signinRequired: {
message: 'Signin required.',
code: 'SIGNIN_REQUIRED',
id: '8e75455b-738c-471d-9f80-62693f33372e',
},
},
} as const;
@ -44,11 +50,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => {
const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});
if (note.user!.requireSigninToViewContents && me == null) {
throw new ApiError(meta.errors.signinRequired);
}
return await this.noteEntityService.pack(note, me, {
detail: true,
});

View File

@ -0,0 +1,165 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import type { DriveFilesRepository, MiDriveFile } from '@/models/_.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canEditNote',
kind: 'write:notes',
limit: {
duration: ms('1hour'),
max: 10,
minInterval: ms('1sec'),
},
errors: {
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474',
},
noSuchFile: {
message: 'Some files are not found.',
code: 'NO_SUCH_FILE',
id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
},
cannotCreateAlreadyExpiredPoll: {
message: 'Poll is already expired.',
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
id: '04da457d-b083-4055-9082-955525eda5a5',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' },
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: true,
},
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
mediaIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
poll: {
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array',
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean' },
expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
required: ['choices'],
},
cw: { type: 'string', nullable: true, maxLength: 100 },
disableRightClick: { type: 'boolean', default: false },
},
required: ['noteId', 'text', 'cw'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private getterService: GetterService,
private noteEntityService: NoteEntityService,
private noteUpdateService: NoteUpdateService,
) {
super({
...meta,
requireRolePolicy: 'canEditNote',
}, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});
if (note.userId !== me.id) {
throw new ApiError(meta.errors.noSuchNote);
}
let files: MiDriveFile[] = [];
const fileIds = ps.fileIds ?? ps.mediaIds ?? null;
if (fileIds != null) {
files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
userId: me.id,
fileIds,
})
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
.setParameters({ fileIds })
.getMany();
if (files.length !== fileIds.length) {
throw new ApiError(meta.errors.noSuchFile);
}
}
if (ps.poll) {
if (typeof ps.poll.expiresAt === 'number') {
if (ps.poll.expiresAt < Date.now()) {
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
}
} else if (typeof ps.poll.expiredAfter === 'number') {
ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
}
}
const data = {
text: ps.text,
files: files,
cw: ps.cw,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
};
const updatedNote = await this.noteUpdateService.update(me, data, note, false);
return {
updatedNote: await this.noteEntityService.pack(updatedNote!, me),
};
});
}
}

View File

@ -42,6 +42,12 @@ export const meta = {
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
},
signinRequired: {
message: 'Signin required.',
code: 'SIGNIN_REQUIRED',
id: 'd1588a9e-4b4d-4c07-807f-16f1486577a2',
},
},
} as const;

View File

@ -141,27 +141,34 @@ export class ClientServerService {
'name': this.meta.name || this.config.host,
'start_url': '/',
'display': 'standalone',
'background_color': '#313a42',
'background_color': '#1b1a25',
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'theme_color': this.meta.themeColor || '#86b300',
'theme_color': this.meta.themeColor || '#b8b9f7',
'icons': [{
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': this.meta.app192IconUrl || '/static-assets/icons/192.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'maskable',
}, {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': this.meta.app512IconUrl || '/static-assets/icons/512.png',
'src': '/static-assets/icons/None-512.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'maskable',
}, {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': '/static-assets/icons/None-769.png',
'sizes': '769x769',
'type': 'image/png',
'purpose': 'maskable',
}, {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': '/static-assets/icons/None-1024.png',
'sizes': '1024x1024',
'type': 'image/png',
'purpose': 'maskable',
}, {
'src': '/static-assets/splash.png',
'sizes': '300x300',
'sizes': '1024x1024',
'type': 'image/png',
'purpose': 'any',
}],
@ -191,7 +198,7 @@ export class ClientServerService {
return {
instanceName: meta.name ?? 'Misskey',
icon: meta.iconUrl,
appleTouchIcon: meta.app512IconUrl,
appleTouchIcon: '/apple-touch-icon.png',
themeColor: meta.themeColor,
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
@ -601,12 +608,15 @@ export class ClientServerService {
fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => {
vary(reply.raw, 'Accept');
const note = await this.notesRepository.findOneBy({
id: request.params.note,
visibility: In(['public', 'home']),
const note = await this.notesRepository.findOne({
where: {
id: request.params.note,
visibility: In(['public', 'home']),
},
relations: ['user'],
});
if (note) {
if (note && !note.user!.requireSigninToViewContents) {
const _note = await this.noteEntityService.pack(note);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
reply.header('Cache-Control', 'public, max-age=15');

View File

@ -4,16 +4,24 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
@import url("https://fonts.bunny.net/css?family=jetbrains-mono");
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.css");
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-jp-dynamic-subset.css");
* {
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-family: "JetBrains Mono", "Pretendard JP", "Pretendard", Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
h1, header {
color: rgb(242, 238, 252);
}
html {
background: #ffb4e1;
background: rgb(27, 26, 37);
}
main {
background: #dedede;
background: rgb(33, 32, 41);
}
main > .tabs {
padding: 16px;

View File

@ -169,7 +169,7 @@
<p>Disable an adblocker / アドブロッカーを無効にする</p>
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
<details style="color: #86b300;">
<details style="color: rgb(242, 238, 252);">
<summary>Other options / その他のオプション</summary>
<a href="/flush">
<button class="button-small">
@ -204,8 +204,12 @@
<code>${details.toString()} ${JSON.stringify(details)}</code>`;
errorsElement.appendChild(detailsElement);
addStyle(`
@import url("https://fonts.bunny.net/css?family=jetbrains-mono");
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.css");
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-jp-dynamic-subset.css");
* {
font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
font-family: "Pretendard JP", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Hiragino Sans", "Apple SD Gothic Neo", Meiryo, "Noto Sans JP", "Noto Sans KR", "Malgun Gothic", Osaka, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
}
#misskey_app,
@ -215,8 +219,8 @@
body,
html {
background-color: #222;
color: #dfddcc;
background-color: rgb(27, 26, 37);
color: rgb(242, 238, 252);
justify-content: center;
margin: auto;
padding: 10px;
@ -232,21 +236,21 @@
}
.button-big {
background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
background: linear-gradient(90deg, rgb(184, 185, 247), rgb(204, 184, 247));
line-height: 50px;
}
.button-big:hover {
background: rgb(153, 204, 0);
background: rgb(230, 230, 252);
}
.button-small {
background: #444;
background: transparent;
line-height: 40px;
}
.button-small:hover {
background: #555;
background: rgba(184, 185, 247, 0.15);
}
.button-label-big {
@ -257,7 +261,7 @@
}
.button-label-small {
color: rgb(153, 204, 0);
color: rgba(184, 185, 247);
font-size: 16px;
padding: 12px;
}
@ -278,17 +282,23 @@
padding-top: 2rem;
}
body > details > summary {
margin-bottom: 16px;
}
h1 {
font-size: 1.5em;
margin: 1em;
}
code {
font-family: Fira, FiraCode, monospace;
color: #f8f8f2;
text-shadow: 0 1px rgba(0,0,0,.3);
font-family: "JetBrains Mono", "Pretendard JP", Pretendard, Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
#errorInfo {
background: #333;
background: #272822;
margin-bottom: 2rem;
padding: 0.5rem 1rem;
width: 40rem;

View File

@ -4,16 +4,24 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
@import url("https://fonts.bunny.net/css?family=jetbrains-mono");
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.css");
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-jp-dynamic-subset.css");
* {
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-family: "JetBrains Mono", "Pretendard JP", "Pretendard", Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
h1, header, div {
color: rgb(242, 238, 252);
}
html {
background: #ffb4e1;
background: rgb(27, 26, 37);
}
main {
background: #dedede;
background: rgb(33, 32, 41);
}
#tl > div {

View File

@ -3,24 +3,30 @@
"name": "Misskey",
"start_url": "/",
"display": "standalone",
"background_color": "#313a42",
"theme_color": "#86b300",
"background_color": "#1b1a25",
"theme_color": "#b8b9f7",
"icons": [
{
"src": "/static-assets/icons/192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static-assets/icons/512.png",
"src": "/static-assets/icons/None-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static-assets/icons/None-769.png",
"sizes": "769x769",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static-assets/icons/None-1024.png",
"sizes": "1024x1024",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static-assets/splash.png",
"sizes": "300x300",
"sizes": "1024x1024",
"type": "image/png",
"purpose": "any"
}

View File

@ -60,6 +60,7 @@ describe('NoteCreateService', () => {
replyUserHost: null,
renoteUserId: null,
renoteUserHost: null,
updatedAt: null,
};
const poll: IPoll = {

View File

@ -43,6 +43,7 @@ const base: MiNote = {
replyUserHost: null,
renoteUserId: null,
renoteUserHost: null,
updatedAt: null,
};
describe('misc:is-renote', () => {

View File

@ -78,6 +78,7 @@ export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'canEditNote',
'mentionLimit',
'canInvite',
'inviteLimit',

View File

@ -0,0 +1,94 @@
{
id: 'e2f17041-23e2-49d7-a86b-273284e0a440',
base: 'dark',
desc: '푸른 별빛이 자아내는 잔향, Byeolvit Noctiluca(별빛 녹틸루카)는 Byeolvit의 기본 다크 모드 테마입니다.',
name: 'Byeolvit Noctiluca Rev.1',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',
X4: 'rgba(255, 255, 255, 0.1)',
X5: 'rgba(255, 255, 255, 0.05)',
X6: 'rgba(255, 255, 255, 0.15)',
X7: 'rgba(255, 255, 255, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
bg: '#121417',
fg: '#E4ECEA',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.3)',
X12: 'rgba(255, 255, 255, 0.1)',
X13: 'rgba(255, 255, 255, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
cwBg: ':alpha<0.06<@accent',
cwFg: ':alpha<0.7<@renote',
link: ':lighten<15<@accent',
warn: '@infoWarnFg',
badge: '@infoFg',
error: ':lighten<10<@infoWarnFg',
focus: ':alpha<0.10<@accent',
navBg: '@panel',
navFg: '@fg',
panel: '#1A1E23',
popup: ':lighten<3<@panel',
accent: '#3FFFD1',
deckBg: ':darken<4<@bg',
header: ':alpha<0.7<@panel',
infoBg: ':alpha<0.05<@infoFg',
infoFg: '#A192FF',
renote: '#ACFCE9',
shadow: 'rgba(0, 0, 0, 0.3)',
divider: '#A8F0DE26',
hashtag: ':lighten<15<@accent',
mention: '@renote',
modalBg: 'rgba(0, 0, 0, 0.8)',
success: ':darken<10<@accent',
buttonBg: 'rgba(174, 219, 233, 0.1)',
switchBg: 'rgba(255, 255, 255, 0.15)',
MessageBg: '@bg',
acrylicBg: ':alpha<0.5<@bg',
cwHoverBg: ':alpha<0.1<@accent',
fgOnWhite: '@bg',
indicator: '@fg',
mentionMe: '@mention',
navActive: '@accent',
accentedBg: ':alpha<0.08<@accent',
codeNumber: ':lighten<10<infoFg',
codeString: '#FFA39D',
fgOnAccent: '@bg',
infoWarnBg: ':alpha<0.05<@infoWarnFg',
infoWarnFg: '#FF76E9',
navHoverFg: ':lighten<17<@fg',
switchOnBg: '@accentedBg',
switchOnFg: '@accent',
codeBoolean: ':lighten<10<infoWarnFg',
dateLabelFg: ':alpha<0.7<@fg',
inputBorder: ':alpha<0.1<@renote',
panelBorder: 'solid 1px var(--divider)',
switchOffBg: ':alpha<0.1<@renote',
switchOffFg: ':alpha<0.8<@fg',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
windowHeader: ':alpha<0.85<@panel',
accentLighten: ':lighten<10<@accent',
buttonHoverBg: 'rgba(174, 219, 233, 0.14)',
driveFolderBg: ':alpha<0.1<@renote',
fgHighlighted: '#FFF',
fgTransparent: ':alpha<0.5<@fg',
panelHeaderBg: ':lighten<6<@panel',
buttonGradateA: '@accent',
buttonGradateB: ':hue<10<@accent',
panelHighlight: ':lighten<6<@panel',
listItemHoverBg: ':alpha<0.03<@fg',
scrollbarHandle: ':alpha<0.1<@fg',
inputBorderHover: ':alpha<0.4<@renote',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
fgTransparentWeak: ':alpha<0.75<@fg',
panelHeaderDivider: '@divider',
scrollbarHandleHover: ':alpha<0.2<@fg',
},
author: '@atLuminon@byeolvit.space',
}

View File

@ -0,0 +1,92 @@
{
id: '3ac6daed-52b4-4d38-a248-eaeb3f6be8c3',
base: 'dark',
desc: '버터스콘이 까맣게 탔습니다. 밤에 먹으면 탄 줄도 모르니 괜찮습니다.',
name: 'scone.color 0.0.1',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',
X4: 'rgba(255, 255, 255, 0.1)',
X5: 'rgba(255, 255, 255, 0.05)',
X6: 'rgba(255, 255, 255, 0.15)',
X7: 'rgba(255, 255, 255, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
bg: '#211b19',
fg: '#fffbe7',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.3)',
X12: 'rgba(255, 255, 255, 0.1)',
X13: 'rgba(255, 255, 255, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
link: '#eedb97',
warn: '#ecb637',
badge: '#fadda1',
error: '#ec4137',
focus: ':alpha<0.3<@accent',
navBg: '@panel',
navFg: '@fg',
panel: ':lighten<3<@bg',
popup: ':lighten<3<@panel',
accent: '#fae7a1',
header: ':alpha<0.7<@panel',
infoBg: '#3a312e',
infoFg: '#fff',
renote: '#fae7a1',
shadow: 'rgba(0, 0, 0, 0.3)',
divider: 'rgba(255, 255, 255, 0.1)',
hashtag: '#ffcc00',
mention: '#e8ce8e',
modalBg: 'rgba(0, 0, 0, 0.5)',
success: '#fae7a1',
buttonBg: 'rgba(255, 255, 255, 0.05)',
switchBg: 'rgba(255, 255, 255, 0.15)',
acrylicBg: ':alpha<0.5<@bg',
indicator: '@accent',
mentionMe: '#8d86ff',
messageBg: '@bg',
navActive: '@accent',
accentedBg: ':alpha<0.15<@accent',
codeNumber: '#fae7a1',
codeString: '#00cccc',
fgOnAccent: '#fff',
infoWarnBg: '#211b19',
infoWarnFg: '#fadda1',
navHoverFg: ':lighten<17<@fg',
swutchOnBg: '@accentedBg',
swutchOnFg: '@accent',
codeBoolean: '#8d86ff',
dateLabelFg: '@fg',
deckDivider: '#000',
inputBorder: 'rgba(255, 255, 255, 0.1)',
panelBorder: '" solid 1px var(--divider)',
swutchOffBg: 'rgba(255, 255, 255, 0.1)',
swutchOffFg: '@fg',
accentDarken: '#ffdd5f',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@indicator',
windowHeader: ':alpha<0.85<@panel',
accentLighten: '#fff6be',
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':lighten<3<@fg',
fgTransparent: ':alpha<0.5<@fg',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
buttonGradateA: '#f5af59',
buttonGradateB: '#fff6be',
htmlThemeColor: '@bg',
panelHighlight: '#f5af59',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
fgTransparentWeak: ':alpha<0.75<@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
},
author: '@g0n9yu@buttersc.one',
}

View File

@ -0,0 +1,19 @@
{
id: 'd932db14-5aed-49d4-85b2-cfea32884044',
name: 'Stella Night R2',
author: 'caipira113',
base: 'dark',
props: {
accent: 'rgb(184, 185, 247)',
bg: 'rgb(27, 26, 37)',
fg: 'rgb(242, 238, 252)',
fgOnAccent: '@panel',
panel: 'rgb(33, 32, 41)',
renote: '@accent',
link: 'rgb(247, 217, 255)',
mention: '@link',
hashtag: 'rgb(100, 179, 255)',
driveFolderBg: 'rgb(73, 71, 96)',
divider: 'rgb(48, 47, 61)',
},
}

View File

@ -0,0 +1,94 @@
{
id: 'daf4f8ed-0468-44f3-a141-d566fb62c1fe',
base: 'light',
desc: '새하얀 별빛의 이정표, Byeolvit Polaris(별빛 폴라리스)는 Byeolvit의 기본 라이트 모드 테마입니다.',
name: 'Byeolvit Polaris Rev.1',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(0, 0, 0, 0.05)',
X4: 'rgba(0, 0, 0, 0.1)',
X5: 'rgba(0, 0, 0, 0.05)',
X6: 'rgba(0, 0, 0, 0.25)',
X7: 'rgba(0, 0, 0, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
bg: '#E9ECEC',
fg: '#053328',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.1)',
X12: 'rgba(0, 0, 0, 0.1)',
X13: 'rgba(0, 0, 0, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
cwBg: ':alpha<0.06<@accent',
cwFg: ':alpha<0.7<@renote',
link: ':lighten<2.5<@accent',
warn: '@infoWarnFg',
badge: '@infoFg',
error: ':darken<5<@infoWarnFg',
focus: ':alpha<0.10<@accent',
navBg: '@panel',
navFg: '@fg',
panel: '#F8F9F9',
popup: ':lighten<1<@panel',
accent: '#00795C',
deckBg: ':darken<10<@bg',
header: ':alpha<0.7<@panel',
infoBg: ':alpha<0.05<@infoFg',
infoFg: '#5A47CF',
renote: '#49665F',
shadow: 'rgba(0, 0, 0, 0.3)',
divider: '#083B2F33',
hashtag: ':lighten<2.5<@accent',
mention: '@accent',
modalBg: 'rgba(0, 0, 0, 0.8)',
success: ':darken<10<@accent',
buttonBg: 'rgba(8, 59, 47, 0.08)',
switchBg: 'rgba(0, 0, 0, 0.15)',
MessageBg: '@bg',
acrylicBg: ':alpha<0.5<@bg',
cwHoverBg: ':alpha<0.1<@accent',
fgOnWhite: '@bg',
indicator: '@fg',
mentionMe: '@mention',
navActive: '@accent',
accentedBg: ':alpha<0.07<@accent',
codeNumber: ':darken<10<infoFg',
codeString: '#BB4D46',
fgOnAccent: '@bg',
infoWarnBg: ':alpha<0.05<@infoWarnFg',
infoWarnFg: '#9C324B',
navHoverFg: ':lighten<17<@fg',
switchOnBg: '@accentedBg',
switchOnFg: '@accent',
codeBoolean: ':lighten<20<infoWarnFg',
dateLabelFg: ':alpha<0.7<@fg',
inputBorder: ':alpha<0.1<@renote',
panelBorder: 'solid 1px var(--divider)',
switchOffBg: ':alpha<0.1<@renote',
switchOffFg: ':alpha<0.5<@fg',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
windowHeader: ':alpha<0.85<@panel',
accentLighten: ':lighten<10<@accent',
buttonHoverBg: 'rgba(8, 59, 47, 0.15)',
driveFolderBg: ':alpha<0.1<@renote',
fgHighlighted: '#000',
fgTransparent: ':alpha<0.5<@fg',
panelHeaderBg: ':lighten<6<@panel',
buttonGradateA: '@accent',
buttonGradateB: ':hue<20<@accent',
panelHighlight: ':darken<10<@panel',
listItemHoverBg: ':alpha<0.03<@fg',
scrollbarHandle: ':alpha<0.1<@fg',
inputBorderHover: ':alpha<0.4<@renote',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
fgTransparentWeak: ':alpha<0.75<@fg',
panelHeaderDivider: '@divider',
scrollbarHandleHover: ':alpha<0.2<@fg',
},
author: '@atLuminon@byeolvit.space',
}

View File

@ -0,0 +1,91 @@
{
id: '02f40388-a33b-4385-8f8f-850865f35673',
base: 'light',
name: 'buttersconedefault',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(0, 0, 0, 0.05)',
X4: 'rgba(0, 0, 0, 0.1)',
X5: 'rgba(0, 0, 0, 0.05)',
X6: 'rgba(0, 0, 0, 0.25)',
X7: 'rgba(0, 0, 0, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
bg: '#FFFBE7',
fg: '#736955',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.1)',
X12: 'rgba(0, 0, 0, 0.1)',
X13: 'rgba(0, 0, 0, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
link: '#44a4c1',
warn: '#ecb637',
badge: '#31b1ce',
error: '#ec4137',
focus: ':alpha<0.3<@accent',
navBg: '@panel',
navFg: '@fg',
panel: ':lighten<3<@bg',
popup: ':lighten<3<@panel',
accent: '#EFB33A',
header: ':alpha<0.7<@panel',
infoBg: '#e5f5ff',
infoFg: '#72818a',
renote: '#229e82',
shadow: 'rgba(0, 0, 0, 0.1)',
divider: 'rgba(0, 0, 0, 0.1)',
hashtag: '#ff9156',
mention: '@accent',
modalBg: 'rgba(0, 0, 0, 0.3)',
success: '#86b300',
buttonBg: 'rgba(0, 0, 0, 0.05)',
switchBg: 'rgba(0, 0, 0, 0.15)',
acrylicBg: ':alpha<0.5<@bg',
indicator: '@accent',
mentionMe: '@mention',
messageBg: '@bg',
navActive: '@accent',
accentedBg: ':alpha<0.15<@accent',
codeNumber: '#0fbbbb',
codeString: '#b98710',
fgOnAccent: '#fff',
infoWarnBg: '#fff0db',
infoWarnFg: '#8f6e31',
navHoverFg: ':darken<17<@fg',
swutchOnBg: '@accent',
swutchOnFg: '@fgOnAccent',
codeBoolean: '#62b70c',
dateLabelFg: '@fg',
deckDivider: ':darken<3<@bg',
inputBorder: 'rgba(0, 0, 0, 0.1)',
panelBorder: '" solid 1px var(--divider)',
swutchOffBg: 'rgba(0, 0, 0, 0.1)',
swutchOffFg: '@panel',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@indicator',
windowHeader: ':alpha<0.85<@panel',
accentLighten: ':lighten<10<@accent',
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':#EFB33A',
fgTransparent: ':alpha<0.5<@fg',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
buttonGradateA: '#ffde80',
buttonGradateB: '#ffde80',
htmlThemeColor: '@bg',
panelHighlight: ':darken<3<@panel',
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
fgTransparentWeak: ':alpha<0.75<@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
},
author: '@pijon@buttersc.one',
}

View File

@ -0,0 +1,19 @@
{
id: '5a079dd7-d741-4ef6-8292-2ca8204e6f55',
name: 'Stella Light R2',
author: 'caipira113',
base: 'light',
props: {
accent: 'rgb(137, 131, 226)',
bg: 'rgb(234, 230, 253)',
fg: 'rgb(91, 87, 150)',
fgOnAccent: '@panel',
panel: 'rgb(249, 247, 255)',
renote: '@accent',
link: 'rgb(137, 105, 151)',
mention: '@link',
hashtag: 'rgb(0, 43, 255)',
driveFolderBg: 'rgba(137, 131, 226, 0.3)',
divider: 'rgb(208, 205, 217)',
},
}

View File

@ -18,6 +18,7 @@
},
"dependencies": {
"@discordapp/twemoji": "15.1.0",
"@fontsource/jetbrains-mono": "^5.0.21",
"@github/webauthn-json": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@misskey-dev/browser-image-resizer": "2024.1.0",
@ -56,6 +57,8 @@
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"pretendard": "^1.3.9",
"pretendard-jp": "^1.3.9",
"punycode": "2.3.1",
"rollup": "4.22.5",
"sanitize-html": "2.13.1",

View File

@ -5,12 +5,12 @@
import { defineAsyncComponent, reactive, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { apiUrl } from '@@/js/config.js';
import type { MenuItem, MenuButton } from '@/types/menu.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
import type { MenuItem, MenuButton } from '@/types/menu.js';
import { del, get, set } from '@/scripts/idb-proxy.js';
import { apiUrl } from '@@/js/config.js';
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
@ -165,7 +165,18 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
});
}
export function updateAccount(accountData: Partial<Account>) {
export function updateAccount(accountData: Account) {
if (!$i) return;
for (const key of Object.keys($i)) {
delete $i[key];
}
for (const [key, value] of Object.entries(accountData)) {
$i[key] = value;
}
miLocalStorage.setItem('account', JSON.stringify($i));
}
export function updateAccountPartial(accountData: Partial<Account>) {
if (!$i) return;
for (const [key, value] of Object.entries(accountData)) {
$i[key] = value;

View File

@ -4,14 +4,14 @@
*/
import { createApp, defineAsyncComponent, markRaw } from 'vue';
import { ui } from '@@/js/config.js';
import { common } from './common.js';
import type * as Misskey from 'misskey-js';
import { ui } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
import { $i, signout, updateAccount } from '@/account.js';
import { $i, signout, updateAccountPartial } from '@/account.js';
import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
@ -291,11 +291,11 @@ export async function mainBoot() {
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
updateAccount(i);
updateAccountPartial(i);
});
main.on('readAllNotifications', () => {
updateAccount({
updateAccountPartial({
hasUnreadNotification: false,
unreadNotificationsCount: 0,
});
@ -303,39 +303,39 @@ export async function mainBoot() {
main.on('unreadNotification', () => {
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
updateAccount({
updateAccountPartial({
hasUnreadNotification: true,
unreadNotificationsCount,
});
});
main.on('unreadMention', () => {
updateAccount({ hasUnreadMentions: true });
updateAccountPartial({ hasUnreadMentions: true });
});
main.on('readAllUnreadMentions', () => {
updateAccount({ hasUnreadMentions: false });
updateAccountPartial({ hasUnreadMentions: false });
});
main.on('unreadSpecifiedNote', () => {
updateAccount({ hasUnreadSpecifiedNotes: true });
updateAccountPartial({ hasUnreadSpecifiedNotes: true });
});
main.on('readAllUnreadSpecifiedNotes', () => {
updateAccount({ hasUnreadSpecifiedNotes: false });
updateAccountPartial({ hasUnreadSpecifiedNotes: false });
});
main.on('readAllAntennas', () => {
updateAccount({ hasUnreadAntenna: false });
updateAccountPartial({ hasUnreadAntenna: false });
});
main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true });
updateAccountPartial({ hasUnreadAntenna: true });
sound.playMisskeySfx('antenna');
});
main.on('readAllAnnouncements', () => {
updateAccount({ hasUnreadAnnouncement: false });
updateAccountPartial({ hasUnreadAnnouncement: false });
});
// 個人宛てお知らせが発行されたとき

View File

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

View File

@ -161,7 +161,7 @@ function openPostForm() {
}
.fontMonospace {
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-family: "JetBrains Mono", "Pretendard JP", Pretendard, Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
.postForm {

View File

@ -117,8 +117,8 @@ async function requestRender() {
sitekey: props.sitekey,
theme: defaultStore.state.darkMode ? 'dark' : 'light',
callback: callback,
'expired-callback': callback,
'error-callback': callback,
'expired-callback': () => callback(undefined),
'error-callback': () => callback(undefined),
});
} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
const { default: Widget } = await import('@mcaptcha/vanilla-glue');

View File

@ -78,7 +78,7 @@ watch(() => props.lang, (to) => {
overflow: auto;
border-radius: 8px;
border: 1px solid var(--MI_THEME-divider);
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
font-family: "JetBrains Mono", "Pretendard JP", Pretendard, Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
color: var(--shiki-fallback);
background-color: var(--shiki-fallback-bg);
@ -90,7 +90,7 @@ watch(() => props.lang, (to) => {
& pre,
& code {
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
font-family: "JetBrains Mono", "Pretendard JP", Pretendard, Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
}
}

View File

@ -79,7 +79,7 @@ function copy() {
}
.codeBlockFallbackCode {
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
font-family: "JetBrains Mono", "Pretendard JP", Pretendard, Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
}
.codePlaceholderRoot {

View File

@ -163,7 +163,7 @@ watch(v, newValue => {
color: var(--MI_THEME-fg);
border: solid 1px var(--MI_THEME-panel);
transition: border-color 0.1s ease-out;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
font-family: "JetBrains Mono", "Pretendard JP", Pretendard, Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
&:hover {
border-color: var(--MI_THEME-inputBorderHover) !important;
}
@ -207,7 +207,7 @@ watch(v, newValue => {
padding: 12px;
line-height: 1.5em;
font-size: 1em;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
font-family: "JetBrains Mono", "Pretendard JP", Pretendard, Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
}
.textarea::selection {

View File

@ -16,7 +16,7 @@ const props = defineProps<{
<style module lang="scss">
.root {
display: inline-block;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
font-family: "JetBrains Mono", "Pretendard JP", Pretendard, Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
overflow-wrap: anywhere;
background: var(--MI_THEME-bg);
padding: .1em;

View File

@ -37,13 +37,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { host } from '@@/js/config.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
@ -80,7 +80,7 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
}
async function onClick() {
pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` });
pleaseLogin({ openOnRemote: { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` } });
wait.value = true;

View File

@ -227,6 +227,7 @@ const emit = defineEmits<{
}>();
const inTimeline = inject<boolean>('inTimeline', false);
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(false));
const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
@ -299,7 +300,7 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
if (checkOnly) return false;
if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
if (inTimeline && !tl_withSensitive.value && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
return false;
}
@ -419,7 +420,7 @@ if (!props.mock) {
}
function renote(viaKeyboard = false) {
pleaseLogin(undefined, pleaseLoginContext.value);
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
@ -429,7 +430,7 @@ function renote(viaKeyboard = false) {
}
function reply(): void {
pleaseLogin(undefined, pleaseLoginContext.value);
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
if (props.mock) {
return;
}
@ -442,7 +443,7 @@ function reply(): void {
}
function react(): void {
pleaseLogin(undefined, pleaseLoginContext.value);
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction');
@ -563,7 +564,7 @@ function showRenoteMenu(): void {
}
if (isMyRenote) {
pleaseLogin(undefined, pleaseLoginContext.value);
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
os.popupMenu([
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' },

View File

@ -207,6 +207,7 @@ import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
import { host } from '@@/js/config.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@ -230,7 +231,6 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { host } from '@@/js/config.js';
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
@ -404,7 +404,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
}
function renote() {
pleaseLogin(undefined, pleaseLoginContext.value);
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
@ -412,7 +412,7 @@ function renote() {
}
function reply(): void {
pleaseLogin(undefined, pleaseLoginContext.value);
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
os.post({
reply: appearNote.value,
@ -423,7 +423,7 @@ function reply(): void {
}
function react(): void {
pleaseLogin(undefined, pleaseLoginContext.value);
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction');
@ -499,7 +499,7 @@ async function clip(): Promise<void> {
function showRenoteMenu(): void {
if (!isMyRenote) return;
pleaseLogin(undefined, pleaseLoginContext.value);
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
os.popupMenu([{
text: i18n.ts.unrenote,
icon: 'ti ti-trash',

View File

@ -17,19 +17,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
</div>
<div :class="$style.info">
<span v-if="note.updatedAt" style="margin-right: 0.5em;"><i v-tooltip="i18n.tsx.noteUpdatedAt({ date: (new Date(note.updatedAt)).toLocaleDateString(), time: (new Date(note.updatedAt)).toLocaleTimeString() })" class="ti ti-pencil"></i></span>
<span v-if="note.visibility !== 'public'" style="margin-right: 0.5em;">
<i v-if="note.visibility === 'home'" v-tooltip="i18n.ts._visibility[note.visibility]" class="ti ti-home"></i>
<i v-else-if="note.visibility === 'followers'" v-tooltip="i18n.ts._visibility[note.visibility]" class="ti ti-lock"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" v-tooltip="i18n.ts._visibility[note.visibility]" class="ti ti-mail"></i>
</span>
<span v-if="note.reactionAcceptance != null" style="margin-right: 0.5em;" :class="{ [$style.danger]: ['nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', 'likeOnly'].includes(<string>note.reactionAcceptance) }" :title="i18n.ts.reactionAcceptance">
<i v-if="note.reactionAcceptance === 'likeOnlyForRemote'" v-tooltip="i18n.ts.likeOnlyForRemote" class="ti ti-heart-plus"></i>
<i v-else-if="note.reactionAcceptance === 'nonSensitiveOnly'" v-tooltip="i18n.ts.nonSensitiveOnly" class="ti ti-icons"></i>
<i v-else-if="note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'" v-tooltip="i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote" class="ti ti-heart-plus"></i>
<i v-else-if="note.reactionAcceptance === 'likeOnly'" v-tooltip="i18n.ts.likeOnly" class="ti ti-heart"></i>
</span>
<span v-if="note.localOnly" style="margin-right: 0.5em;"><i v-tooltip="i18n.ts._visibility['disableFederation']" class="ti ti-rocket-off"></i></span>
<span v-if="note.channel" style="margin-right: 0.5em;"><i v-tooltip="note.channel.name" class="ti ti-device-tv"></i></span>
<div v-if="mock">
<MkTime :time="note.createdAt" colored/>
</div>
<MkA v-else :to="notePage(note)">
<MkTime :time="note.createdAt" colored/>
</MkA>
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
</div>
</header>
</template>

View File

@ -5,11 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
<MkAvatar :class="$style.avatar" :user="user"/>
<div :class="$style.main">
<div :class="$style.header">
<MkUserName :user="user" :nowrap="true"/>
</div>
<div>
<p v-if="useCw" :class="$style.cw">
<Mfm v-if="cw != null && cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
@ -50,16 +46,6 @@ const props = defineProps<{
font-size: 0.95em;
}
.avatar {
flex-shrink: 0 !important;
display: block !important;
margin: 0 10px 0 0 !important;
width: 40px !important;
height: 40px !important;
border-radius: 8px !important;
pointer-events: none !important;
}
.main {
flex: 1;
min-width: 0;
@ -80,20 +66,4 @@ const props = defineProps<{
overflow: clip;
text-overflow: ellipsis;
}
@container (min-width: 350px) {
.avatar {
margin: 0 10px 0 0 !important;
width: 44px !important;
height: 44px !important;
}
}
@container (min-width: 500px) {
.avatar {
margin: 0 12px 0 0 !important;
width: 48px !important;
height: 48px !important;
}
}
</style>

View File

@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { sum } from '@/scripts/array.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { host } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
const props = defineProps<{
noteId: string;
@ -85,7 +85,7 @@ if (props.poll.expiresAt) {
const vote = async (id) => {
if (props.readOnly || closed.value || isVoted.value) return;
pleaseLogin(undefined, pleaseLoginContext.value);
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
const { canceled } = await os.confirm({
type: 'question',

View File

@ -65,10 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
@ -151,6 +151,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
updateMode?: boolean;
mock?: boolean;
}>(), {
initialVisibleUsers: () => [],
@ -201,6 +202,7 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'
const imeText = ref('');
const showingOptions = ref(false);
const textAreaReadOnly = ref(false);
const justEndedComposition = ref(false);
const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
@ -573,7 +575,13 @@ function clear() {
function onKeydown(ev: KeyboardEvent) {
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
if (ev.key === 'Escape') emit('esc');
// justEndedComposition.value is for Safari, which keyDown occurs after compositionend.
// ev.isComposing is for another browsers.
if (ev.key === 'Escape' && !justEndedComposition.value && !ev.isComposing) emit('esc');
}
function onKeyup(ev: KeyboardEvent) {
justEndedComposition.value = false;
}
function onCompositionUpdate(ev: CompositionEvent) {
@ -582,6 +590,7 @@ function onCompositionUpdate(ev: CompositionEvent) {
function onCompositionEnd(ev: CompositionEvent) {
imeText.value = '';
justEndedComposition.value = true;
}
async function onPaste(ev: ClipboardEvent) {
@ -707,6 +716,7 @@ function saveDraft() {
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
quoteId: quoteId.value,
reactionAcceptance: reactionAcceptance.value,
noteId: props.updateMode ? props.initialNote?.id : undefined,
},
};
@ -788,6 +798,7 @@ async function post(ev?: MouseEvent) {
visibility: visibility.value,
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
reactionAcceptance: reactionAcceptance.value,
noteId: props.updateMode ? props.initialNote?.id : undefined,
};
if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
@ -824,7 +835,7 @@ async function post(ev?: MouseEvent) {
}
posting.value = true;
misskeyApi('notes/create', postData, token).then(() => {
misskeyApi(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) {
posted.value = true;
} else {
@ -1055,7 +1066,7 @@ defineExpose({
&.modal {
width: 100%;
max-width: 520px;
max-width: 750px;
}
}
@ -1198,7 +1209,7 @@ defineExpose({
.preview {
padding: 16px 20px 0 20px;
min-height: 75px;
min-height: auto;
max-height: 150px;
overflow: auto;
background-size: auto auto;

View File

@ -31,6 +31,7 @@ const props = withDefaults(defineProps<{
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;
updateMode?: boolean;
}>(), {
initialLocalOnly: undefined,
});

View File

@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
@keydown.space.enter="show"
>
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
<select
<div
ref="inputEl"
v-model="v"
v-adaptive-border
tabindex="-1"
:class="$style.inputCore"
@ -26,27 +25,25 @@ SPDX-License-Identifier: AGPL-3.0-only
:required="required"
:readonly="readonly"
:placeholder="placeholder"
@input="onInput"
@mousedown.prevent="() => {}"
@keydown.prevent="() => {}"
>
<slot></slot>
</select>
<div style="pointer-events: none;">{{ currentValueText ?? '' }}</div>
<div style="display: none;">
<slot></slot>
</div>
</div>
<div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script lang="ts" setup>
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { useInterval } from '@@/js/use-interval.js';
import { i18n } from '@/i18n.js';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
const props = defineProps<{
modelValue: string | null;
@ -56,25 +53,20 @@ const props = defineProps<{
placeholder?: string;
autofocus?: boolean;
inline?: boolean;
manualSave?: boolean;
small?: boolean;
large?: boolean;
}>();
const emit = defineEmits<{
(ev: 'changeByUser', value: string | null): void;
(ev: 'update:modelValue', value: string | null): void;
(ev: 'update:modelValue', value: string | number | null): void;
}>();
const slots = useSlots();
const { modelValue, autofocus } = toRefs(props);
const v = ref(modelValue.value);
const focused = ref(false);
const opening = ref(false);
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const currentValueText = ref<string | null>(null);
const inputEl = ref<HTMLObjectElement | null>(null);
const prefixEl = ref<HTMLElement | null>(null);
const suffixEl = ref<HTMLElement | null>(null);
@ -85,26 +77,6 @@ const height =
36;
const focus = () => container.value?.focus();
const onInput = (ev) => {
changed.value = true;
};
const updated = () => {
changed.value = false;
emit('update:modelValue', v.value);
};
watch(modelValue, newValue => {
v.value = newValue;
});
watch(v, () => {
if (!props.manualSave) {
updated();
}
invalid.value = inputEl.value?.validity.badInput ?? true;
});
//
// 0
@ -134,6 +106,31 @@ onMounted(() => {
});
});
watch(modelValue, () => {
const scanOptions = (options: VNodeChild[]) => {
for (const vnode of options) {
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
if (vnode.type === 'optgroup') {
const optgroup = vnode;
if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
} else if (Array.isArray(vnode.children)) { //
const fragment = vnode;
if (Array.isArray(fragment.children)) scanOptions(fragment.children);
} else if (vnode.props == null) { // v-if false
// nop?
} else {
const option = vnode;
if (option.props?.value === modelValue.value) {
currentValueText.value = option.children as string;
break;
}
}
}
};
scanOptions(slots.default!());
}, { immediate: true });
function show() {
if (opening.value) return;
focus();
@ -146,11 +143,9 @@ function show() {
const pushOption = (option: VNode) => {
menu.push({
text: option.children as string,
active: computed(() => v.value === option.props?.value),
active: computed(() => modelValue.value === option.props?.value),
action: () => {
v.value = option.props?.value;
changed.value = true;
emit('changeByUser', v.value);
emit('update:modelValue', option.props?.value);
},
});
};
@ -248,7 +243,8 @@ function show() {
.inputCore {
appearance: none;
-webkit-appearance: none;
display: block;
display: flex;
align-items: center;
height: v-bind("height + 'px'");
width: 100%;
margin: 0;

View File

@ -38,6 +38,7 @@ const props = withDefaults(defineProps<{
sound?: boolean;
withRenotes?: boolean;
withReplies?: boolean;
withSensitive?: boolean;
onlyFiles?: boolean;
}>(), {
withRenotes: true,
@ -51,6 +52,7 @@ const emit = defineEmits<{
}>();
provide('inTimeline', true);
provide('tl_withSensitive', computed(() => props.withSensitive));
provide('inChannel', computed(() => props.src === 'channel'));
type TimelineQueryType = {
@ -248,6 +250,9 @@ function refreshEndpointAndChannel() {
// IDTL
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
// withSensitiveOK
watch(() => props.withSensitive, reloadTimeline);
//
refreshEndpointAndChannel();

View File

@ -150,20 +150,6 @@ export const navbarItemDef = reactive({
}], ev.currentTarget ?? ev.target);
},
},
about: {
title: i18n.ts.about,
icon: 'ti ti-info-circle',
action: (ev) => {
openInstanceMenu(ev);
},
},
tools: {
title: i18n.ts.tools,
icon: 'ti ti-tool',
action: (ev) => {
openToolsMenu(ev);
},
},
reload: {
title: i18n.ts.reload,
icon: 'ti ti-refresh',

View File

@ -688,14 +688,16 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
}
export function post(props: Record<string, any> = {}): Promise<void> {
pleaseLogin(undefined, (props.initialText || props.initialNote ? {
type: 'share',
params: {
text: props.initialText ?? props.initialNote.text,
visibility: props.initialVisibility ?? props.initialNote?.visibility,
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
},
} : undefined));
pleaseLogin({
openOnRemote: (props.initialText || props.initialNote ? {
type: 'share',
params: {
text: props.initialText ?? props.initialNote.text,
visibility: props.initialVisibility ?? props.initialNote?.visibility,
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
},
} : undefined),
});
showMovedDialog();
return new Promise(resolve => {

View File

@ -160,6 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])">
<template #label>{{ i18n.ts._role._options.canEditNote }}</template>
<template #suffix>
<span v-if="role.policies.canEditNote.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canEditNote.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canEditNote)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canEditNote.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canEditNote.value" :disabled="role.policies.canEditNote.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canEditNote.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
<template #suffix>

View File

@ -51,6 +51,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])">
<template #label>{{ i18n.ts._role._options.canEditNote }}</template>
<template #suffix>{{ policies.canEditNote ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canEditNote">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
<template #suffix>{{ policies.mentionLimit }}</template>

View File

@ -55,7 +55,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i, updateAccount } from '@/account.js';
import { $i, updateAccountPartial } from '@/account.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
@ -90,7 +90,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
target.isRead = true;
await misskeyApi('i/read-announcement', { announcementId: target.id });
if ($i) {
updateAccount({
updateAccountPartial({
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
});
}

View File

@ -56,7 +56,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i, updateAccount } from '@/account.js';
import { $i, updateAccountPartial } from '@/account.js';
const paginationCurrent = {
endpoint: 'announcements' as const,
@ -94,7 +94,7 @@ async function read(target) {
return a;
});
misskeyApi('i/read-announcement', { announcementId: target.id });
updateAccount({
updateAccountPartial({
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
});
}

View File

@ -24,7 +24,7 @@ const props = defineProps<{
}>();
if (props.showLoginPopup) {
pleaseLogin('/');
pleaseLogin({ path: '/' });
}
const headerActions = computed(() => []);

View File

@ -61,6 +61,7 @@ import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import MkClipPreview from '@/components/MkClipPreview.vue';
import { defaultStore } from '@/store.js';
import { pleaseLogin } from '@/scripts/please-login.js';
const props = defineProps<{
noteId: string;
@ -128,6 +129,11 @@ function fetchNote() {
});
}
}).catch(err => {
if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
pleaseLogin({
message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
});
}
error.value = err;
});
}

View File

@ -84,7 +84,7 @@ import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkLink from '@/components/MkLink.vue';
import * as os from '@/os.js';
import { signinRequired, updateAccount } from '@/account.js';
import { signinRequired, updateAccountPartial } from '@/account.js';
import { i18n } from '@/i18n.js';
const $i = signinRequired();
@ -123,7 +123,7 @@ async function unregisterTOTP(): Promise<void> {
password: auth.result.password,
token: auth.result.token,
}).then(res => {
updateAccount({
updateAccountPartial({
twoFactorEnabled: false,
});
}).catch(error => {

View File

@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<MkSelect v-model="type">
<option value="all">{{ i18n.ts.all }}</option>
<option value="following">{{ i18n.ts.following }}</option>
<option value="follower">{{ i18n.ts.followers }}</option>
<option value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
<option value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option>
<option value="list">{{ i18n.ts.userList }}</option>
<option value="never">{{ i18n.ts.none }}</option>
<option v-for="type in props.configurableTypes ?? notificationConfigTypes" :key="type" :value="type">{{ notificationConfigTypesI18nMap[type] }}</option>
</MkSelect>
<MkSelect v-if="type === 'list'" v-model="userListId">
@ -21,31 +15,61 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect>
<div class="_buttons">
<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
<MkButton inline primary :disabled="type === 'list' && userListId === null" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</template>
<script lang="ts">
const notificationConfigTypes = [
'all',
'following',
'follower',
'mutualFollow',
'followingOrFollower',
'list',
'never'
] as const;
export type NotificationConfig = {
type: Exclude<typeof notificationConfigTypes[number], 'list'>;
} | {
type: 'list';
userListId: string;
};
</script>
<script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import { ref } from 'vue';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{
value: any;
value: NotificationConfig;
userLists: Misskey.entities.UserList[];
configurableTypes?: NotificationConfig['type'][]; // If not specified, all types are configurable
}>();
const emit = defineEmits<{
(ev: 'update', result: any): void;
(ev: 'update', result: NotificationConfig): void;
}>();
const notificationConfigTypesI18nMap: Record<typeof notificationConfigTypes[number], string> = {
all: i18n.ts.all,
following: i18n.ts.following,
follower: i18n.ts.followers,
mutualFollow: i18n.ts.mutualFollow,
followingOrFollower: i18n.ts.followingOrFollower,
list: i18n.ts.userList,
never: i18n.ts.none,
};
const type = ref(props.value.type);
const userListId = ref(props.value.userListId);
const userListId = ref(props.value.type === 'list' ? props.value.userListId : null);
function save() {
emit('update', { type: type.value, userListId: userListId.value });
emit('update', type.value === 'list' ? { type: type.value, userListId: userListId.value! } : { type: type.value });
}
</script>

View File

@ -22,7 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only
}}
</template>
<XNotificationConfig :userLists="userLists" :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }" @update="(res) => updateReceiveConfig(type, res)"/>
<XNotificationConfig
:userLists="userLists"
:value="$i.notificationRecieveConfig[type] ?? { type: 'all' }"
:configurableTypes="onlyOnOrOffNotificationTypes.includes(type) ? ['all', 'never'] : undefined"
@update="(res) => updateReceiveConfig(type, res)"
/>
</MkFolder>
</div>
</FormSection>
@ -58,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { shallowRef, computed } from 'vue';
import XNotificationConfig from './notifications.notification-config.vue';
import XNotificationConfig, { type NotificationConfig } from './notifications.notification-config.vue';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
@ -73,7 +78,9 @@ import { notificationTypes } from '@@/js/const.js';
const $i = signinRequired();
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[];
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login'] satisfies (typeof notificationTypes[number])[] as string[];
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
@ -88,7 +95,7 @@ async function readAllNotifications() {
await os.apiWithDialog('notifications/mark-all-as-read');
}
async function updateReceiveConfig(type, value) {
async function updateReceiveConfig(type: typeof notificationTypes[number], value: NotificationConfig) {
await os.apiWithDialog('i/update', {
notificationRecieveConfig: {
...$i.notificationRecieveConfig,

View File

@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
</MkSwitch>
<MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
{{ i18n.ts.preventAiLearning }}<span class="_beta">{{ i18n.ts.beta }}</span>
{{ i18n.ts.preventAiLearning }}
<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
</MkSwitch>
<MkSwitch v-model="isExplorable" @update:modelValue="save()">
@ -44,6 +44,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
</MkSwitch>
<FormSection>
<template #label>{{ i18n.ts.lockdown }}</template>
<div class="_gaps_m">
<MkSwitch v-model="requireSigninToViewContents" @update:modelValue="save()">
{{ i18n.ts._accountSettings.requireSigninToViewContents }}<span class="_beta">{{ i18n.ts.beta }}</span>
<template #caption>
<div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
</template>
</MkSwitch>
</div>
</FormSection>
<FormSection>
<div class="_gaps_m">
<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
@ -90,6 +105,7 @@ const autoAcceptFollowed = ref($i.autoAcceptFollowed);
const noCrawle = ref($i.noCrawle);
const preventAiLearning = ref($i.preventAiLearning);
const isExplorable = ref($i.isExplorable);
const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false);
const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions);
const followingVisibility = ref($i.followingVisibility);
@ -107,6 +123,7 @@ function save() {
noCrawle: !!noCrawle.value,
preventAiLearning: !!preventAiLearning.value,
isExplorable: !!isExplorable.value,
requireSigninToViewContents: !!requireSigninToViewContents.value,
hideOnlineStatus: !!hideOnlineStatus.value,
publicReactions: !!publicReactions.value,
followingVisibility: followingVisibility.value,

View File

@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:list="src.split(':')[1]"
:withRenotes="withRenotes"
:withReplies="withReplies"
:withSensitive="withSensitive"
:onlyFiles="onlyFiles"
:sound="true"
@queue="queueUpdated"
@ -121,11 +122,6 @@ watch(src, () => {
queue.value = 0;
});
watch(withSensitive, () => {
//
tlComponent.value?.reloadTimeline();
});
function queueUpdated(q: number): void {
queue.value = q;
}

View File

@ -14,9 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<img :src="misskeysvg" class="misskey"/>
</div>
<div class="emojis">
<MkEmoji :normal="true" :noStyle="true" emoji="👍"/>
<MkEmoji :normal="true" :noStyle="true" emoji="🍮"/>
<MkEmoji :normal="true" :noStyle="true" emoji="❤"/>
<MkEmoji :normal="true" :noStyle="true" emoji="😆"/>
<MkEmoji :normal="true" :noStyle="true" emoji="🍮"/>
<MkEmoji :normal="true" :noStyle="true" emoji="🎉"/>
<MkEmoji :normal="true" :noStyle="true" emoji="🍮"/>
</div>

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